diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..97c291b --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,13 @@ +#### Description + +#### Steps/Code to Reproduce + +#### Expected Behavior + +#### Actual Behavior + +#### Operating System + +#### Python Version + +#### Samila Version (Use : `samila.__version__`) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..590816d --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,7 @@ +#### Reference Issues/PRs + +#### What does this implement/fix? Explain your changes. + + +#### Any other comments? + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8c045f2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [Unreleased] +## [0.1] - 2021-09-30 +### Added +- `GenerativeImage` class +- `plot` method +- `generate` method + +[Unreleased]: https://github.com/sepandhaghighi/samila/compare/v0.1...dev +[0.1]: https://github.com/sepandhaghighi/samila/compare/1058677...v0.1 + + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..96f71c9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +MIT License + +Copyright (c) 2021 Sepand Haghighi + +Copyright (c) 2021 Sadra Sabouri + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..610bdaf --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +matplotlib>=3.0.0 + diff --git a/samila/__init__.py b/samila/__init__.py index fdca66d..65a5b55 100644 --- a/samila/__init__.py +++ b/samila/__init__.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- """Samila modules.""" from .genimage import GenerativeImage +from .params import Projection diff --git a/samila/functions.py b/samila/functions.py index e684a9e..dca707b 100644 --- a/samila/functions.py +++ b/samila/functions.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """Samila functions.""" +from .params import Projection, DEFAULT_PROJECTION def float_range(start, stop, step): """ @@ -17,3 +18,64 @@ def float_range(start, stop, step): while start < stop: yield float(start) start += step + + +def distance_calc(s1, s2): + """ + Calculate Levenshtein distance between two words. + + :param s1: first string + :type s1 : str + :param s2: second string + :type s2 : str + :return: distance between two string + + References : + 1- https://stackoverflow.com/questions/2460177/edit-distance-in-python + 2- https://en.wikipedia.org/wiki/Levenshtein_distance + """ + if len(s1) > len(s2): + s1, s2 = s2, s1 + + distances = range(len(s1) + 1) + for i2, c2 in enumerate(s2): + distances_ = [i2 + 1] + for i1, c1 in enumerate(s1): + if c1 == c2: + distances_.append(distances[i1]) + else: + distances_.append( + 1 + min((distances[i1], distances[i1 + 1], distances_[-1]))) + distances = distances_ + return distances[-1] + + +def filter_color(color): + """ + Filter given color and return it + + :param color: given color + :type color: str or tuple + :return: filtered version of color + """ + if isinstance(color, tuple): + return color + if isinstance(color, str): + from .params import VALID_COLORS + distance_list = list(map(lambda x: distance_calc(color, x), + VALID_COLORS)) + min_distance = min(distance_list) + return VALID_COLORS[distance_list.index(min_distance)] + return None + +def filter_projection(projection): + """ + Filter given projection. + + :param projection: given projection + :type projection: Projection enum + :return: filtered version of projection + """ + if isinstance(projection, Projection): + return projection.value + return DEFAULT_PROJECTION diff --git a/samila/genimage.py b/samila/genimage.py index 23fdd52..d8585df 100644 --- a/samila/genimage.py +++ b/samila/genimage.py @@ -3,7 +3,7 @@ import random import itertools import matplotlib.pyplot as plt -from .functions import float_range +from .functions import float_range, filter_color, filter_projection from .params import * @@ -35,17 +35,20 @@ class GenerativeImage: self.data1 = [] self.data2 = [] self.seed = seed - random.seed(self.seed) + if seed is None: + self.seed = random.randint(0, 2 ** 20) range1 = list(float_range(start, stop, step)) range2 = list(float_range(start, stop, step)) range_prod = list(itertools.product(range1, range2)) for item in range_prod: + random.seed(self.seed) self.data1.append(self.function1(item[0], item[1])) self.data2.append(self.function2(item[0], item[1])) def plot( self, color=DEFAULT_COLOR, + bgcolor=DEFAULT_BACKGROUND_COLOR, spot_size=DEFAULT_SPOT_SIZE, size=DEFAULT_IMAGE_SIZE, projection=DEFAULT_PROJECTION): @@ -54,6 +57,8 @@ class GenerativeImage: :param color: point colors :type color: str + :param bgcolor: background color + :type bgcolor: str :param spot_size: point spot size :type spot_size: float :param size: figure size @@ -62,8 +67,21 @@ class GenerativeImage: :type projection: str :return: None """ + color = filter_color(color) if not None else DEFAULT_COLOR + bgcolor = filter_color( + bgcolor) if not None else DEFAULT_BACKGROUND_COLOR + projection = filter_projection(projection) fig = plt.figure() fig.set_size_inches(size[0], size[1]) + fig.set_facecolor(bgcolor) ax = fig.add_subplot(111, projection=projection) - ax.scatter(self.data2, self.data1, alpha=0.1, c=color, s=spot_size) - ax.axis('off') + ax.set_facecolor(bgcolor) + ax.scatter( + self.data2, + self.data1, + alpha=DEFAULT_ALPHA, + edgecolors=color, + s=spot_size) + ax.set_axis_off() + ax.patch.set_zorder(-1) + ax.add_artist(ax.patch) diff --git a/samila/params.py b/samila/params.py index 17cb286..e055c90 100644 --- a/samila/params.py +++ b/samila/params.py @@ -1,11 +1,26 @@ # -*- coding: utf-8 -*- """Samila params.""" import math +from enum import Enum +from matplotlib import colors as mcolors DEFAULT_START = -1 * math.pi DEFAULT_STOP = math.pi DEFAULT_STEP = 0.01 DEFAULT_COLOR = "black" +DEFAULT_BACKGROUND_COLOR = "white" +DEFAULT_ALPHA = 0.1 DEFAULT_IMAGE_SIZE = (10, 10) DEFAULT_SPOT_SIZE = 0.01 DEFAULT_PROJECTION = None +VALID_COLORS = list(dict(mcolors.BASE_COLORS, **mcolors.CSS4_COLORS).keys()) + + +class Projection(Enum): + DEFAULT = DEFAULT_PROJECTION + POLAR = "polar" + AITOFF = "aitoff" + HAMMER = "hammer" + LAMBERT = "lambert" + MOLLWEIDE = "mollweide" + RECTILINEAR = "rectilinear"