master > master: src

This commit is contained in:
RD 2022-10-20 16:21:50 +02:00
parent ef0265c86d
commit 5e307eb355
7 changed files with 381 additions and 1 deletions

0
src/maths/__init__.py Normal file
View File

View File

@ -0,0 +1,17 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# IMPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
from src.maths.diagrams.sets import *;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# EXPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
__all__ = [
'Function',
'Functions',
];

205
src/maths/diagrams/sets.py Normal file
View File

@ -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;

View File

@ -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',
];

62
src/maths/sets/random.py Normal file
View File

@ -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 ];

View File

@ -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

24
src/thirdparty/plots.py vendored Normal file
View File

@ -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',
];