diff --git a/src/maths/__init__.py b/src/maths/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/maths/diagrams/__init__.py b/src/maths/diagrams/__init__.py new file mode 100644 index 0000000..6b4eff7 --- /dev/null +++ b/src/maths/diagrams/__init__.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# IMPORTS +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +from src.maths.diagrams.sets import *; + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# EXPORTS +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +__all__ = [ + 'Function', + 'Functions', +]; diff --git a/src/maths/diagrams/sets.py b/src/maths/diagrams/sets.py new file mode 100644 index 0000000..d6effbb --- /dev/null +++ b/src/maths/diagrams/sets.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# IMPORTS +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +from __future__ import annotations; + +from src.thirdparty.code import *; +from src.thirdparty.types import *; +from src.thirdparty.maths import *; +from src.thirdparty.plots import *; + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# EXPORTS +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +__all__ = [ + 'Function', + 'Functions', +]; + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# CONSTANTS / VARIABLES +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +T1 = TypeVar('T1'); +T2 = TypeVar('T2'); + +SCALE = (1., 4.); +OFFSET = (3., 0.); +MARGIN = 0.1; +N_RESOLUTION = 100; +ANNOTATE_OFFSET = (0, 10); +FONTSIZE_PTS = 10; +FONTSIZE_FCT = 14; +FONTSIZE_SETS = 14; + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Classes +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +@dataclass +class Function(Generic[T1,T2]): + name: tuple[str, str, str] = field(); + domain: list[T1] = field(); + codomain: list[T2] = field(); + fct: list[tuple[T1,T2]] = field(); + + @property + def range(self) -> list[T2]: + return [y for x, y in self.fct]; + + @property + def indexes(self) -> list[tuple[int, int]]: + # prevent repeated computation: + if not hasattr(self, '_indexes'): + self._indexes = [ + (self.domain.index(x), self.codomain.index(y)) + for x, y in self.fct + ]; + return getattr(self, '_indexes'); + + def draw(self) -> Figure: + return Functions(self).draw(); + +class Functions: + fcts: list[Function]; + + def __init__(self, *f: Function): + self.fcts = list(f); + + def draw(self, show_labels: bool = True) -> Figure: + N = len(self.fcts); + obj = mplot.subplots(1, 1, constrained_layout=True); + fig: Figure = obj[0]; + axs: Axes = obj[1]; + axs.tick_params(axis='both', which='both', left=False, right=False, top=False, bottom=False, labelbottom=False, labelleft=False); + mplot.title(''); + mplot.xlabel(''); + mplot.ylabel(''); + mplot.margins(x=MARGIN, y=MARGIN); + + origin = np.asarray((0., 0.)); + offset = np.asarray(OFFSET); + + p_set = oval(nr_points=N_RESOLUTION, scale=SCALE, centre=origin); + for k in range(N+1): + axs.plot(p_set[:, 0] + k*offset[0], p_set[:, 1] + k*offset[1], label='', color='blue'); + + p_domain = []; + p_codomain = []; + comp_range = []; + + anchors = [ + [ + # function name + origin + (k + 0.5)*offset + (0, -1.1*SCALE[1]), + # sets + origin + k * offset + (0, 1.1 * SCALE[1]), + origin + (k + 1) * offset + (0, 1.1 * SCALE[1]), + # arrow start -> end + origin + (k + 0.05) * offset + (0, -1.1 * SCALE[1]), + origin + (k + 1 - 0.05) * offset + (0, -1.1 * SCALE[1]), + + ] + for k in range(N) + ]; + + for k, f in enumerate(self.fcts): + if k == 0: + comp_range = f.domain; + p_domain = random_points(nr_points=len(f.domain), scale=SCALE, centre=origin + k*offset); + else: + p_domain = p_codomain; + p_codomain = random_points(nr_points=len(f.codomain), scale=SCALE, centre=origin + (k+1)*offset); + # range of composition so far: + comp_range_next = [y for x, y in f.fct if x in comp_range]; + + if k == 0: + axs.scatter(p_domain[:, 0], p_domain[:, 1], label='', color='black', marker='o'); + if show_labels: + for i, p in enumerate(p_domain): + x_name = f.domain[i]; + axs.annotate(text=f'{x_name}', xy = p, textcoords='offset points', xytext=ANNOTATE_OFFSET, ha='center', size=FONTSIZE_PTS); + + for j, p in enumerate(p_codomain): + y = f.codomain[j]; + marker = 'o' if (y in comp_range_next) else 'x'; + axs.scatter([p[0]], [p[1]], label='', color='black', marker=marker); + y_name = f.codomain[j]; + if show_labels: + axs.annotate(text=f'{y_name}', xy=p, textcoords='offset points', xytext=ANNOTATE_OFFSET, ha='center', size=FONTSIZE_PTS); + + for i, j in f.indexes: + p = p_domain[i]; + q = p_codomain[j]; + x = f.domain[i]; + if k == 0 or (x in comp_range): + axs.plot([p[0], q[0]], [p[1], q[1]], label='', color='g', linewidth=2); + else: + axs.plot([p[0], q[0]], [p[1], q[1]], label='', color='g', linestyle='--', linewidth=1); + + anchor = anchors[k]; + fct_name, X_name, Y_name = f.name; + axs.annotate(text=f'{fct_name}', xy=anchor[0], ha='center', size=FONTSIZE_FCT); + if k == 0: + axs.annotate(text=f'{X_name}', xy=anchor[1], ha='center', size=FONTSIZE_FCT); + axs.annotate(text=f'{Y_name}', xy=anchor[2], ha='center', size=FONTSIZE_FCT); + axs.add_patch(FancyArrowPatch( + anchor[3], anchor[4], + connectionstyle = 'arc3,rad=0.5', + arrowstyle = 'Simple, tail_width=0.5, head_width=4, head_length=8', + color = 'black', + )); + + # update range of composition: + comp_range = comp_range_next; + + return fig; + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# AUXILIARY +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +def oval( + nr_points: int, + scale: tuple[float, float] = (1., 1.), + centre: tuple[float, float] = (0., 0.), +) -> NDArray[Shape['*, 2'], Float]: + theta = np.linspace(start=0, stop=2*np.pi, num=nr_points, endpoint=True); + P = np.zeros(shape=(nr_points, 2), dtype=float); + P[:, 0] = centre[0] + scale[0] * np.cos(theta); + P[:, 1] = centre[1] + scale[1] * np.sin(theta); + P[-1, :] = P[0, :]; + return P; + + + +def random_points( + nr_points: int, + scale: tuple[float, float] = (1., 1.), + centre: tuple[float, float] = (0., 0.), + force: bool = False, + tol: float = 0.2, +) -> NDArray[Shape['*, 2'], Float]: + theta = np.linspace(start=0, stop=2*np.pi, num=nr_points, endpoint=False); + r_min = 0.25; + r_max = 1; + while True: + u = np.random.random(size=(nr_points,)); + u_max = max(u); + if u_max == 0.: + continue; + if force: + u = np.minimum((1 + tol) * u / u_max, 1); + else: + u = (1 - tol) * u / u_max; + break; + r = r_min + (r_max - r_min) * u; + P = np.zeros(shape=(nr_points, 2), dtype=float); + P[:, 0] = centre[0] + scale[0] * r * np.cos(theta); + P[:, 1] = centre[1] + scale[1] * r * np.sin(theta); + return P; diff --git a/src/maths/sets/__init__.py b/src/maths/sets/__init__.py new file mode 100644 index 0000000..e1903e7 --- /dev/null +++ b/src/maths/sets/__init__.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# IMPORTS +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +from src.maths.sets.random import *; + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# EXPORTS +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +__all__ = [ + 'random_function', + 'randomset_alphabet', + 'randomset_greek', + 'randomset_integers', +]; diff --git a/src/maths/sets/random.py b/src/maths/sets/random.py new file mode 100644 index 0000000..f0c4561 --- /dev/null +++ b/src/maths/sets/random.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# IMPORTS +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +from __future__ import annotations; + +from src.thirdparty.types import *; +from src.thirdparty.maths import *; + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# EXPORTS +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +__all__ = [ + 'randomset_integers', + 'randomset_alphabet', + 'randomset_greek', + 'random_function', +]; + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# CONSTANTS / VARIABLES +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +ALPHA = 'abcdefghijklmnopqrstuvwxyz'; +GREEK = 'αβγδεζηθικλμνξοπρςτυφχψω'; +T1 = TypeVar('T1'); +T2 = TypeVar('T2'); + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# METHODS +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +def randomset_integers(low: int, high: int) -> list[int]: + N = random.randint(low, high); + return list(range(1, N+1)); + +def randomset_alphabet(low: int, high: int) -> list[int]: + N = random.randint(low, high); + return list([a for k, a in enumerate(ALPHA) if k < N]); + +def randomset_greek(low: int, high: int) -> list[int]: + N = random.randint(low, high); + return list([a for k, a in enumerate(GREEK) if k < N]); + +def random_function( + X: list[T1], + Y: list[T2], + injective: Optional[bool] = None, + surjective: Optional[bool] = None, +) -> list[tuple[T1, T2]]: + # TODO: add feature to force injectivity/surjectivity, if possible. + # m = len(X); + # n = len(Y); + # if m > n: + # injective = False; + # if m < n: + # surjective = False; + return [ (x, random.choice(Y)) for x in X ]; diff --git a/src/thirdparty/misc.py b/src/thirdparty/misc.py index af351b7..13fa4eb 100644 --- a/src/thirdparty/misc.py +++ b/src/thirdparty/misc.py @@ -7,9 +7,62 @@ from datetime import datetime; from datetime import timedelta; +from functools import wraps; import lorem; import re; -from textwrap import dedent; +from textwrap import dedent as textwrap_dedent; +from typing import Callable; +from typing import TypeVar; + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# MODIFICATIONS +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +def prestrip(first: bool = True, last: bool = True, all: bool = False): + ''' + Returns a decorator that modifies string -> string methods + ''' + T = TypeVar('T'); + def dec(method: Callable[[str], T]) -> Callable[[str], T]: + ''' + Performs method but first strips initial/final (empty) lines. + ''' + @wraps(method) + def wrapped_method(text: str) -> T: + lines = re.split(pattern=r'\n', string=text); + if all: + if first: + while len(lines) > 0 and lines[0].strip() == '': + lines = lines[1:]; + if last: + while len(lines) > 0 and lines[-1].strip() == '': + lines = lines[:-1]; + else: + if first: + lines = lines[1:]; + if last: + lines = lines[:-1]; + text = '\n'.join(lines); + return method(text); + return wrapped_method; + return dec; + +@prestrip(all=False) +def dedent(text: str) -> str: + ''' + Remove any common leading whitespace from every line in `text`. + + This can be used to make triple-quoted strings line up with the left + edge of the display, while still presenting them in the source code + in indented form. + + Note that tabs and spaces are both treated as whitespace, but they + are not equal: the lines " hello" and "\\thello" are + considered to have no common leading whitespace. + + Entirely blank lines are normalized to a newline character. + ''' + return textwrap_dedent(text); # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # EXPORTS diff --git a/src/thirdparty/plots.py b/src/thirdparty/plots.py new file mode 100644 index 0000000..e97dfd4 --- /dev/null +++ b/src/thirdparty/plots.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# IMPORTS +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +from matplotlib import pyplot as mplot; +from matplotlib import colors as mcolours; +from matplotlib.figure import Figure; +from matplotlib.axes import Axes; +from matplotlib.patches import FancyArrowPatch; + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# EXPORTS +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +__all__ = [ + 'mplot', + 'mcolours', + 'Figure', + 'Axes', + 'FancyArrowPatch', +];