ograph 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- docs/source/conf.py +78 -0
- ograph/__init__.py +0 -0
- ograph/applications/__init__.py +3 -0
- ograph/applications/swarm.py +131 -0
- ograph/oconfig.py +95 -0
- ograph/ofig.py +180 -0
- ograph/ofunc.py +48 -0
- ograph/oplot.py +563 -0
- ograph/py.typed +1 -0
- ograph-1.0.0.dist-info/METADATA +76 -0
- ograph-1.0.0.dist-info/RECORD +14 -0
- ograph-1.0.0.dist-info/WHEEL +5 -0
- ograph-1.0.0.dist-info/licenses/LICENSE.md +9 -0
- ograph-1.0.0.dist-info/top_level.txt +2 -0
docs/source/conf.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
from importlib.metadata import metadata
|
|
4
|
+
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
|
5
|
+
|
|
6
|
+
# -- Project information -----------------------------------------------------
|
|
7
|
+
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
|
8
|
+
|
|
9
|
+
project = metadata('ograph')['Name']
|
|
10
|
+
copyright = f"2024-2025, {metadata('ograph')['Author-email']}"
|
|
11
|
+
description = f"{metadata('ograph')['Summary']}"
|
|
12
|
+
|
|
13
|
+
# -- General configuration ---------------------------------------------------
|
|
14
|
+
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
|
15
|
+
|
|
16
|
+
# Add path to source directory
|
|
17
|
+
print(f"Append path: {os.path.abspath(os.path.join('..', '..'))}")
|
|
18
|
+
|
|
19
|
+
sys.path.append(os.path.abspath(os.path.join('..', '..')))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
extensions = ['nbsphinx',
|
|
23
|
+
'sphinx.ext.todo',
|
|
24
|
+
'sphinx.ext.viewcode',
|
|
25
|
+
'sphinx.ext.autodoc',
|
|
26
|
+
'sphinx.ext.napoleon',
|
|
27
|
+
'sphinx.ext.autosummary']
|
|
28
|
+
|
|
29
|
+
# -- Options for Typing ------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
language = 'en-uk'
|
|
32
|
+
|
|
33
|
+
autoclass_content = 'class'
|
|
34
|
+
autosummary_generate = True
|
|
35
|
+
|
|
36
|
+
autodoc_default_options = {
|
|
37
|
+
'undoc-members': True,
|
|
38
|
+
# Note: `autodoc_class_signature='separated'` causes `ClassDocumenter` to
|
|
39
|
+
# register both `__init__` and `__new__` as special members.
|
|
40
|
+
# This overrides the default behaviour of not documenting private
|
|
41
|
+
# members -- even if `__new__` is marked as private, Sphinx still
|
|
42
|
+
# documents it.
|
|
43
|
+
# Override the override with `'exclude-members'`, so that `__new__`
|
|
44
|
+
# is ABSOLUTELY not be documented ... until another patch breaks it.
|
|
45
|
+
'exclude-members': '__new__',
|
|
46
|
+
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
autodoc_class_signature = 'separated'
|
|
51
|
+
autodoc_inherit_docstrings = False
|
|
52
|
+
|
|
53
|
+
autodoc_member_order = 'bysource'
|
|
54
|
+
autodoc_typehints = 'signature'
|
|
55
|
+
autodoc_typehints_description_target = 'all'
|
|
56
|
+
|
|
57
|
+
napoleon_use_rtype = False
|
|
58
|
+
|
|
59
|
+
# -- Options for HTML output -------------------------------------------------
|
|
60
|
+
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
|
61
|
+
|
|
62
|
+
html_theme = 'sphinx_rtd_theme'
|
|
63
|
+
html_static_path = ['_static']
|
|
64
|
+
html_css_files = ['styles.css',]
|
|
65
|
+
templates_path = ['_templates']
|
|
66
|
+
exclude_patterns: list[str] = []
|
|
67
|
+
|
|
68
|
+
napoleon_include_special_with_doc = True
|
|
69
|
+
|
|
70
|
+
rst_prolog = """
|
|
71
|
+
.. role:: python(code)
|
|
72
|
+
:language: python
|
|
73
|
+
:class: highlight
|
|
74
|
+
|
|
75
|
+
.. role:: arg(code)
|
|
76
|
+
:class: highlight
|
|
77
|
+
|
|
78
|
+
"""
|
ograph/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from typing import Optional, Callable
|
|
2
|
+
import numpy as np
|
|
3
|
+
from ..oplot import contour
|
|
4
|
+
from ..oplot import Vec2D, Vec3D
|
|
5
|
+
import matplotlib as mpl
|
|
6
|
+
import matplotlib.pyplot as plt
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def plot_positions(data: Vec2D | Vec3D,
|
|
10
|
+
objective: Optional[Callable[[float, float], float]] = None,
|
|
11
|
+
*,
|
|
12
|
+
override_region: Optional[Vec2D] = None,
|
|
13
|
+
override_margin: Optional[float] = None):
|
|
14
|
+
"""Plot a sequence of collections of points as they
|
|
15
|
+
change over time.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
data: Either a 2-D tensor or a 3-D tensor.
|
|
19
|
+
* If :arg:`data` is 2-D: data[t] is a single point. Plot
|
|
20
|
+
its change over time.
|
|
21
|
+
* If :arg:`data` is 3-D: data[t] is a collection oof
|
|
22
|
+
points. Plot the change of these points over
|
|
23
|
+
time.
|
|
24
|
+
|
|
25
|
+
objective: An objective function to be plotted as background.
|
|
26
|
+
|
|
27
|
+
override_region: Only plot points that fall in this region.
|
|
28
|
+
|
|
29
|
+
override_margin: A small margin to add to override_region.
|
|
30
|
+
Might make the plot prettier.
|
|
31
|
+
"""
|
|
32
|
+
mort = np.array(data)
|
|
33
|
+
|
|
34
|
+
margin: float = 1 if override_margin is None\
|
|
35
|
+
else override_margin
|
|
36
|
+
|
|
37
|
+
x_min: float
|
|
38
|
+
x_max: float
|
|
39
|
+
y_min: float
|
|
40
|
+
y_max: float
|
|
41
|
+
|
|
42
|
+
x_min = np.min(mort.T[0])
|
|
43
|
+
x_max = np.max(mort.T[0])
|
|
44
|
+
y_min = np.min(mort.T[1])
|
|
45
|
+
y_max = np.max(mort.T[1])
|
|
46
|
+
|
|
47
|
+
if (override_region is not None):
|
|
48
|
+
x_min = override_region[0][0]
|
|
49
|
+
x_max = override_region[0][1]
|
|
50
|
+
y_min = override_region[1][0]
|
|
51
|
+
y_max = override_region[1][1]
|
|
52
|
+
|
|
53
|
+
mort =\
|
|
54
|
+
np.apply_along_axis(func1d=lambda arr: [
|
|
55
|
+
arr[0] if x_min < arr[0] < x_max else np.nan,
|
|
56
|
+
arr[1] if y_min < arr[1] < y_max else np.nan],
|
|
57
|
+
axis=2 if len(mort.shape) == 3 else 1,
|
|
58
|
+
arr=mort)
|
|
59
|
+
|
|
60
|
+
# Adjust margins. Matplotlib does not plot ticks that intersect
|
|
61
|
+
# with borders of the graph; this trick makes plots prettier.
|
|
62
|
+
x_min -= margin
|
|
63
|
+
y_min -= margin
|
|
64
|
+
x_max += margin
|
|
65
|
+
y_max += margin
|
|
66
|
+
|
|
67
|
+
if objective is not None:
|
|
68
|
+
contour(fun=lambda x, y: objective(x, y),
|
|
69
|
+
x_range=(x_min, x_max),
|
|
70
|
+
y_range=(y_min, y_max)) # type: ignore
|
|
71
|
+
|
|
72
|
+
for i in range(len(mort)):
|
|
73
|
+
_xs: float
|
|
74
|
+
_ys: float
|
|
75
|
+
_xs, _ys = np.array(mort[i]).T
|
|
76
|
+
_intensity: float = i / len(mort)
|
|
77
|
+
plt.scatter(_xs, _ys, s=9,
|
|
78
|
+
color=mpl.colormaps['RdYlGn'](_intensity),
|
|
79
|
+
alpha=0.5) # type: ignore
|
|
80
|
+
|
|
81
|
+
plt.xlim((x_min + margin, x_max - margin))
|
|
82
|
+
plt.ylim((y_min + margin, y_max - margin))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def plot_bests(data: Vec2D,
|
|
86
|
+
best_selector: Callable[[Vec2D],
|
|
87
|
+
np.float64] = np.max,
|
|
88
|
+
best_label: str = "Best value",
|
|
89
|
+
all_label: str = "All values",) -> None:
|
|
90
|
+
"""Plot a sequence of numbers against the best one.
|
|
91
|
+
The "best value" is plotted as a horizontal line.
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
data: A sequence of points to be plotted. Each
|
|
96
|
+
data[i] should be a 2-D point.
|
|
97
|
+
|
|
98
|
+
best_selector: A :class:`Callable` that returns the best
|
|
99
|
+
value in :arg:`data`. To plot a pre-determined, constant
|
|
100
|
+
best value, let :arg:`best_selector` always return that value.
|
|
101
|
+
|
|
102
|
+
best_label: Label for the best value.
|
|
103
|
+
|
|
104
|
+
all_label: Label for other values.
|
|
105
|
+
"""
|
|
106
|
+
mort = data
|
|
107
|
+
|
|
108
|
+
plt.scatter(range(len(mort)),
|
|
109
|
+
mort,
|
|
110
|
+
s=12,
|
|
111
|
+
color="tab:blue",
|
|
112
|
+
facecolors="white",)
|
|
113
|
+
|
|
114
|
+
plt.vlines(x=range(len(mort)),
|
|
115
|
+
ymin=np.repeat(a=np.min(mort), repeats=len(mort)),
|
|
116
|
+
ymax=mort,
|
|
117
|
+
zorder=-9,
|
|
118
|
+
linewidth=0.8)
|
|
119
|
+
|
|
120
|
+
plt.hlines(best_selector(mort),
|
|
121
|
+
xmin=0,
|
|
122
|
+
xmax=len(mort),
|
|
123
|
+
color="tab:green",
|
|
124
|
+
label=best_label)
|
|
125
|
+
|
|
126
|
+
plt.plot([],
|
|
127
|
+
[],
|
|
128
|
+
color="tab:blue",
|
|
129
|
+
label=all_label)
|
|
130
|
+
|
|
131
|
+
plt.legend()
|
ograph/oconfig.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Utilities that configure the current plot
|
|
2
|
+
or all plots in the current session.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import matplotlib.pyplot as plt
|
|
8
|
+
import matplotlib as mlp
|
|
9
|
+
import matplotlib.pylab as pylab
|
|
10
|
+
from mpl_toolkits.mplot3d.axes3d import Axes3D # type: ignore[import-untyped]
|
|
11
|
+
|
|
12
|
+
from .ofig import ensure_axes_dimension
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def clear_axis_labels(enable: bool = False) -> None:
|
|
16
|
+
plt.gca().get_yaxis().set_ticks([])
|
|
17
|
+
plt.gca().get_xaxis().set_ticks([])
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def use_axis_lines(enable: bool = True) -> None:
|
|
21
|
+
plt.gca().axhline(y=0, color='DimGrey', linestyle="--")
|
|
22
|
+
plt.gca().axvline(x=0, color='DimGrey', linestyle="--")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def set_font_size(size: int) -> None:
|
|
26
|
+
"""Set the font size to :arg:`size`.
|
|
27
|
+
|
|
28
|
+
Effect:
|
|
29
|
+
Configure the current plot.
|
|
30
|
+
"""
|
|
31
|
+
mlp.rc("font", size=size)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def use_high_res() -> None:
|
|
35
|
+
"""Set figures to render in higher resolution.
|
|
36
|
+
|
|
37
|
+
Set runtime configuration ``figure.dpi`` to 200.
|
|
38
|
+
|
|
39
|
+
Effect:
|
|
40
|
+
Configure all plots in the current session.
|
|
41
|
+
"""
|
|
42
|
+
pylab.rcParams.update({'figure.dpi': 200})
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def use_low_res() -> None:
|
|
46
|
+
"""Set figures to render in the default resolution.
|
|
47
|
+
|
|
48
|
+
Set runtime configuration ``figure.dpi`` to 100, the default value.
|
|
49
|
+
|
|
50
|
+
Effect:
|
|
51
|
+
Configure all plots in the current session.
|
|
52
|
+
"""
|
|
53
|
+
pylab.rcParams.update({'figure.dpi': 100})
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def view_rotate(h_rotate: float, v_rotate: float) -> None:
|
|
57
|
+
"""Rotate the current `Axes3D`.
|
|
58
|
+
|
|
59
|
+
@param h_rotate the degree to rotate vertically
|
|
60
|
+
@param v_rotate the degree to rotate horizontally
|
|
61
|
+
"""
|
|
62
|
+
ax: Axes3D = plt.gca() # type: ignore[no-any-unimported]
|
|
63
|
+
ensure_axes_dimension(ax, 3)
|
|
64
|
+
if (isinstance(ax, Axes3D)): # Make mypy happy
|
|
65
|
+
ax.view_init(h_rotate, v_rotate)
|
|
66
|
+
else:
|
|
67
|
+
raise Exception("This should not happen."
|
|
68
|
+
"The exception has been checked.")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def view_axis_pos(pos: Optional[str]) -> None:
|
|
72
|
+
"""Position labels and ticks of the current Axes3D.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
pos: The position, one of :python:`'lower'`,
|
|
76
|
+
:python:`'upper'`,
|
|
77
|
+
:python:`'default'`,
|
|
78
|
+
:python:`'both'`,
|
|
79
|
+
:python:`'none'`,
|
|
80
|
+
and :python:`None`.
|
|
81
|
+
"""
|
|
82
|
+
accepted_values: list[str] = ['lower', 'upper', 'default', 'both', 'none']
|
|
83
|
+
ax: Axes3D = plt.gca() # type: ignore[no-any-unimported]
|
|
84
|
+
ensure_axes_dimension(ax, 3)
|
|
85
|
+
match pos:
|
|
86
|
+
case None:
|
|
87
|
+
ax.axis('off')
|
|
88
|
+
case other:
|
|
89
|
+
if other in accepted_values:
|
|
90
|
+
for axis in ax.xaxis, ax.yaxis, ax.zaxis:
|
|
91
|
+
axis.set_label_position(other)
|
|
92
|
+
axis.set_ticks_position(other)
|
|
93
|
+
else:
|
|
94
|
+
raise ValueError(f"The input {other} is not one of"
|
|
95
|
+
f"{str(accepted_values)}.")
|
ograph/ofig.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Utilities that create and check figures
|
|
2
|
+
of different dimensions.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
from matplotlib.axes import Axes
|
|
7
|
+
from matplotlib.figure import Figure
|
|
8
|
+
import matplotlib.pylab as pylab
|
|
9
|
+
from mpl_toolkits.mplot3d.axes3d import Axes3D # type: ignore[import-untyped]
|
|
10
|
+
from typing import Tuple
|
|
11
|
+
from typing import Optional
|
|
12
|
+
from typing import Any
|
|
13
|
+
import logging
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def fig2(xlims: Optional[Tuple[float, float]] = None,
|
|
17
|
+
ylims: Optional[Tuple[float, float]] = None,
|
|
18
|
+
arg: None | tuple[float, float, float, float] = None,
|
|
19
|
+
**kwargs: Any) -> Tuple[Figure, Axes]:
|
|
20
|
+
"""Create then return a figure with a 2-dimensional axis.
|
|
21
|
+
|
|
22
|
+
Invoke :meth:`.pyplot.figure` to create figure, then
|
|
23
|
+
invoke :meth:`.pyplot.axes` to create an :class:`Axes`.
|
|
24
|
+
Return both objects in a tuple.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
arg: :python:`None` or 4-tuple.
|
|
28
|
+
|
|
29
|
+
- :python:`None`: A new full window Axes is added using
|
|
30
|
+
``subplot(**kwargs)``.
|
|
31
|
+
|
|
32
|
+
- 4-tuple of floats *rect* = ``(left, bottom, width, height)``:
|
|
33
|
+
Add a new Axes with dimensions *rect* in normalized
|
|
34
|
+
(0, 1) units, using :meth:`Figure.add_axes` on the current
|
|
35
|
+
figure.
|
|
36
|
+
|
|
37
|
+
xlims: Size of figure along the X axis
|
|
38
|
+
ylims: Size of figure along the Y axis
|
|
39
|
+
*args: Positional arguments to pass to :meth:`.pyplot.axes`
|
|
40
|
+
*kwargs: Keyword arguments to pass to :meth:`.pyplot.axes`
|
|
41
|
+
"""
|
|
42
|
+
fig = plt.figure()
|
|
43
|
+
ax = plt.axes(arg, projection='rectilinear', **kwargs)
|
|
44
|
+
if (xlims is not None):
|
|
45
|
+
ax.set_xlim(*xlims)
|
|
46
|
+
if (ylims is not None):
|
|
47
|
+
ax.set_ylim(*ylims)
|
|
48
|
+
return (fig, ax)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def fig3(xlims: Optional[Tuple[float, float]] = None, # type: ignore[no-any-unimported] # noqa: E501
|
|
52
|
+
ylims: Optional[Tuple[float, float]] = None,
|
|
53
|
+
zlims: Optional[Tuple[float, float]] = None,
|
|
54
|
+
arg: None | tuple[float, float, float, float] = None,
|
|
55
|
+
**kwargs: Any) \
|
|
56
|
+
-> Tuple[Figure, Axes3D]:
|
|
57
|
+
"""Create then return a figure with a 3-dimensional axis.
|
|
58
|
+
|
|
59
|
+
Invoke :meth:`.pyplot.figure` to create figure, then
|
|
60
|
+
invoke :meth:`.pyplot.axes` to create an :class:`Axes3D`.
|
|
61
|
+
Return both objects in a tuple.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
arg: :python:`None` or 4-tuple.
|
|
65
|
+
|
|
66
|
+
- :python:`None`: A new full window Axes is added using
|
|
67
|
+
``subplot(**kwargs)``.
|
|
68
|
+
|
|
69
|
+
- 4-tuple of floats *rect* = ``(left, bottom, width, height)``:
|
|
70
|
+
Add a new Axes with dimensions *rect* in normalized
|
|
71
|
+
(0, 1) units, using :meth:`Figure.add_axes` on the current
|
|
72
|
+
figure.
|
|
73
|
+
|
|
74
|
+
xlims: Size of figure along the X axis
|
|
75
|
+
ylims: Size of figure along the Y axis
|
|
76
|
+
ylims: Size of figure along the Y axis
|
|
77
|
+
*kwargs: Keyword arguments to pass to :meth:`.pyplot.axes`
|
|
78
|
+
"""
|
|
79
|
+
fig = plt.figure()
|
|
80
|
+
ax: Axes3D = plt.axes(arg, # type: ignore[no-any-unimported]
|
|
81
|
+
projection='3d',
|
|
82
|
+
**kwargs)
|
|
83
|
+
|
|
84
|
+
if (xlims is not None):
|
|
85
|
+
ax.set_xlim(*xlims)
|
|
86
|
+
if (ylims is not None):
|
|
87
|
+
ax.set_ylim(*ylims)
|
|
88
|
+
if (zlims is not None):
|
|
89
|
+
ax.set_zlim(*zlims)
|
|
90
|
+
return (fig, ax)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class DimensionError(ValueError):
|
|
94
|
+
"""Raised when the dimension of a figure does not agree
|
|
95
|
+
with the plot.
|
|
96
|
+
|
|
97
|
+
When this happens, something is going seriously wrong.
|
|
98
|
+
"""
|
|
99
|
+
def __init__(self, expected: str, actual: str):
|
|
100
|
+
"""
|
|
101
|
+
Args:
|
|
102
|
+
expected: Dimensions of the plot
|
|
103
|
+
actual: Dimensions of the figure
|
|
104
|
+
"""
|
|
105
|
+
super().__init__(f"Dimension mismatch: expected {expected},"
|
|
106
|
+
f" got {actual}")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def ensure_axes_dimension(axes: Axes | Axes3D, # type: ignore[no-any-unimported] # noqa: E501
|
|
110
|
+
dim: int) -> None:
|
|
111
|
+
"""Assert if the given axes is of the specified dimension.
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
:class:`DimensionError`: if :arg:`dim` does not agree with the
|
|
115
|
+
dimension of :arg:`axes:.`
|
|
116
|
+
"""
|
|
117
|
+
dimension_to_name = {2: "rectilinear", 3: "3d"}
|
|
118
|
+
if dim in dimension_to_name:
|
|
119
|
+
if (axes.name != dimension_to_name[dim]):
|
|
120
|
+
raise DimensionError(dimension_to_name[dim], axes.name)
|
|
121
|
+
else:
|
|
122
|
+
fig3() if dim == 3 else fig2()
|
|
123
|
+
logging.warning(f"The current projection is not `{dim}d`."
|
|
124
|
+
f"A new {"Axes" if dim == 2 else "Axes3D"}"
|
|
125
|
+
" is created instead.")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def annotate(title: str,
|
|
129
|
+
xlabel: str,
|
|
130
|
+
ylabel: str,
|
|
131
|
+
zlabel: Optional[str] = None) -> None:
|
|
132
|
+
"""Annotate the current figure.
|
|
133
|
+
|
|
134
|
+
Set :arg:`title`, :arg:`xlabel`, and :arg:`ylabel` of the current axes.
|
|
135
|
+
Also set runtime configurations that style annotations.
|
|
136
|
+
|
|
137
|
+
To reset the parameters, call:
|
|
138
|
+
:python:`matplotlib.rcParams.update(matplotlib.rcParamsDefault)`
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
title: Title of the figure
|
|
142
|
+
xlabel: Labels for the x axis
|
|
143
|
+
ylabel: Labels for the y axis
|
|
144
|
+
zlabel: Labels for the z axis
|
|
145
|
+
|
|
146
|
+
Effect:
|
|
147
|
+
Plot to the current active figure.
|
|
148
|
+
Change runtime configurations.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
axes_label_size: str = "x-large"
|
|
152
|
+
plot_title_size: str = "x-large"
|
|
153
|
+
|
|
154
|
+
font = {'legend.fontsize': 'x-large',
|
|
155
|
+
'axes.titlesize': plot_title_size,
|
|
156
|
+
'axes.labelsize': axes_label_size,
|
|
157
|
+
'xtick.labelsize': axes_label_size,
|
|
158
|
+
'ytick.labelsize': axes_label_size,
|
|
159
|
+
'text.usetex': False,
|
|
160
|
+
'font.family': 'Open Sans',
|
|
161
|
+
'axes.titlepad': 15, }
|
|
162
|
+
|
|
163
|
+
ax: Axes | Axes3D = plt.gca() # type: ignore[no-any-unimported]
|
|
164
|
+
|
|
165
|
+
pylab.rcParams.update(font)
|
|
166
|
+
ax.set_xlabel(xlabel, fontname='PT Serif')
|
|
167
|
+
ax.set_ylabel(ylabel, fontname='PT Serif')
|
|
168
|
+
if (zlabel is not None):
|
|
169
|
+
ensure_axes_dimension(ax, 3)
|
|
170
|
+
if (isinstance(ax, Axes3D)):
|
|
171
|
+
ax.set_zlabel(zlabel, fontname='PT Serif')
|
|
172
|
+
else:
|
|
173
|
+
raise Exception("This should not happen.")
|
|
174
|
+
|
|
175
|
+
my_fig = ax.get_figure()
|
|
176
|
+
|
|
177
|
+
if (my_fig is not None):
|
|
178
|
+
my_fig.suptitle(title, fontname='PT Serif')
|
|
179
|
+
else:
|
|
180
|
+
raise Exception("Somehow the Axes is not attached to a Figure. How?")
|
ograph/ofunc.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Test functions and matrices. Examples are
|
|
2
|
+
the Himmelblau function and the unit cube.
|
|
3
|
+
"""
|
|
4
|
+
from numpy import array
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def rosenbrock(*args: float) -> float:
|
|
8
|
+
"""Rosenbrock function.
|
|
9
|
+
|
|
10
|
+
Multi-dimensional test function where the global minimum is located
|
|
11
|
+
inside a flat valley.
|
|
12
|
+
"""
|
|
13
|
+
result: float = 0
|
|
14
|
+
for i in range(len(args) - 1):
|
|
15
|
+
result += 100 * (args[i + 1] - args[i]**2)**2 + (1 - args[i])**2
|
|
16
|
+
return result
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def himmelblau(x: float, y: float) -> float:
|
|
20
|
+
"""Himmelblau function.
|
|
21
|
+
|
|
22
|
+
2-dimensional test function with several minimum.
|
|
23
|
+
"""
|
|
24
|
+
return (x ** 2 + y - 11) ** 2 + (x + y ** 2 - 7) ** 2
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
#! Immutable 2-dimensional unit square.
|
|
28
|
+
unit_square = array(
|
|
29
|
+
[[0, 0],
|
|
30
|
+
[0, 1],
|
|
31
|
+
[1, 0],
|
|
32
|
+
[1, 1]]
|
|
33
|
+
)
|
|
34
|
+
unit_square.flags.writeable = False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
#! Immutable 3-dimensional unit cube. Fancy!
|
|
38
|
+
unit_cube = array(
|
|
39
|
+
[[0, 0, 0],
|
|
40
|
+
[1, 0, 0],
|
|
41
|
+
[0, 1, 0],
|
|
42
|
+
[0, 0, 1],
|
|
43
|
+
[1, 1, 0],
|
|
44
|
+
[0, 1, 1],
|
|
45
|
+
[1, 0, 1],
|
|
46
|
+
[1, 1, 1],]
|
|
47
|
+
)
|
|
48
|
+
unit_cube.flags.writeable = False
|
ograph/oplot.py
ADDED
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
"""Utilities that plot to the current Axes.
|
|
2
|
+
"""
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
import matplotlib.colors as colors
|
|
5
|
+
import numpy as np
|
|
6
|
+
import matplotlib as mpl
|
|
7
|
+
from typing import overload
|
|
8
|
+
|
|
9
|
+
from .ofig import ensure_axes_dimension
|
|
10
|
+
|
|
11
|
+
from matplotlib.typing import ColorType
|
|
12
|
+
|
|
13
|
+
from typing import Any, Self
|
|
14
|
+
import math
|
|
15
|
+
from numpy.typing import ArrayLike
|
|
16
|
+
|
|
17
|
+
from typing import Optional, Sequence, Callable
|
|
18
|
+
|
|
19
|
+
from scipy.spatial import ConvexHull, distance # type: ignore[import-untyped]
|
|
20
|
+
|
|
21
|
+
from matplotlib.patches import FancyArrowPatch
|
|
22
|
+
|
|
23
|
+
from mpl_toolkits.mplot3d.art3d import Poly3DCollection # type: ignore[import-untyped] # noqa: E501
|
|
24
|
+
from mpl_toolkits.mplot3d.axes3d import Axes3D # type: ignore[import-untyped]
|
|
25
|
+
from mpl_toolkits.mplot3d.art3d import Line3DCollection
|
|
26
|
+
from mpl_toolkits.mplot3d.proj3d import proj_transform # type: ignore[import-untyped] # noqa: E501
|
|
27
|
+
|
|
28
|
+
from numpy import ndarray
|
|
29
|
+
|
|
30
|
+
from matplotlib.patches import Patch
|
|
31
|
+
from matplotlib.text import Text
|
|
32
|
+
from matplotlib.legend import Legend
|
|
33
|
+
|
|
34
|
+
type Array1D = np.ndarray[tuple[Any],
|
|
35
|
+
np.dtype[np.float64]]
|
|
36
|
+
type Array2D = np.ndarray[tuple[Any, Any],
|
|
37
|
+
np.dtype[np.float64]]
|
|
38
|
+
type Array3D = np.ndarray[tuple[Any, Any, Any],
|
|
39
|
+
np.dtype[np.float64]]
|
|
40
|
+
|
|
41
|
+
type Sequence1D = Sequence[float]
|
|
42
|
+
type Sequence2D = Sequence[Sequence[float]]
|
|
43
|
+
type Sequence3D = Sequence[Sequence[Sequence[float]]]
|
|
44
|
+
|
|
45
|
+
type Vec1D = Array1D | Sequence1D
|
|
46
|
+
type Vec2D = Array2D | Sequence2D
|
|
47
|
+
type Vec3D = Array3D | Sequence3D
|
|
48
|
+
|
|
49
|
+
type Point2D = Vec1D
|
|
50
|
+
type Point3D = Vec1D
|
|
51
|
+
|
|
52
|
+
type Points2D = Vec2D
|
|
53
|
+
type Points3D = Vec2D
|
|
54
|
+
|
|
55
|
+
FILL_CONFIG: dict[str, Any] = {"alpha": 0.5}
|
|
56
|
+
EDGE_CONFIG: dict[str, Any] = {"color": "black", "alpha": 1}
|
|
57
|
+
NODE_CONFIG: dict[str, Any] = {"color": "black", "alpha": 0.5}
|
|
58
|
+
CONTOUR_CMAP: str = "viridis"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def heatmap(data: Vec2D,
|
|
62
|
+
xlabels: Optional[Sequence[str]] = None,
|
|
63
|
+
ylabels: Optional[Sequence[str]] = None) -> None:
|
|
64
|
+
"""Plot a matrix to the current active axis as a heatmap.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
data: A Numpy matrix.
|
|
68
|
+
xlabels: labels for cells along the x axis.
|
|
69
|
+
ylabels: labels for cells along the x axis.
|
|
70
|
+
|
|
71
|
+
Effects:
|
|
72
|
+
Plot at the current active axis.
|
|
73
|
+
"""
|
|
74
|
+
if not isinstance(data, ndarray):
|
|
75
|
+
# If the input is not an numpy array, attempt to cast it into one.
|
|
76
|
+
data = np.array(data)
|
|
77
|
+
|
|
78
|
+
# Configure the size of the plot to accommodate the size of each cell
|
|
79
|
+
plt.rcParams["figure.figsize"] = [len(data) * math.sqrt(len(data)),
|
|
80
|
+
len(data[0]) * math.sqrt(len(data[0]))]
|
|
81
|
+
|
|
82
|
+
xlabels = xlabels if (xlabels is not None)\
|
|
83
|
+
else ["X" + str(i) for i, _ in enumerate(data[0])]
|
|
84
|
+
ylabels = ylabels if (ylabels is not None)\
|
|
85
|
+
else ["Y" + str(i) for i, _ in enumerate(data)]
|
|
86
|
+
|
|
87
|
+
ax = plt.gca()
|
|
88
|
+
|
|
89
|
+
ensure_axes_dimension(ax, 2)
|
|
90
|
+
ax.imshow(data, cmap="Greys")
|
|
91
|
+
|
|
92
|
+
ax.set_xticks(np.arange(len(xlabels)), labels=xlabels)
|
|
93
|
+
ax.set_yticks(np.arange(len(ylabels)), labels=ylabels)
|
|
94
|
+
|
|
95
|
+
plt.setp(ax.get_xticklabels(),
|
|
96
|
+
rotation=45,
|
|
97
|
+
ha="right",
|
|
98
|
+
rotation_mode="anchor")
|
|
99
|
+
|
|
100
|
+
max_cell_value = data.max()
|
|
101
|
+
min_cell_value = data.min()
|
|
102
|
+
|
|
103
|
+
for i in range(len(ylabels)):
|
|
104
|
+
for j in range(len(xlabels)):
|
|
105
|
+
cell_value_scale = max_cell_value - min_cell_value
|
|
106
|
+
ratio = (data[i, j] - min_cell_value) / cell_value_scale
|
|
107
|
+
ax.text(j, i, "{:.2f}".format(data[i, j]),
|
|
108
|
+
ha="center",
|
|
109
|
+
va="center",
|
|
110
|
+
color="w" if ratio > .6 else "k")
|
|
111
|
+
|
|
112
|
+
my_fig = ax.get_figure()
|
|
113
|
+
if (my_fig is not None):
|
|
114
|
+
my_fig.tight_layout() # type: ignore[union-attr]
|
|
115
|
+
# (When called on an `Axes`, ->get_figure(.) always returns a `Figure`)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def chull(shape: Points2D | Points3D) -> None:
|
|
119
|
+
"""Plot a convex hull to the current active axes.
|
|
120
|
+
|
|
121
|
+
Detect the shape of points by inspecting the first in the sequence.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
shape: A sequence of 2- or 3-dimensional points.
|
|
125
|
+
|
|
126
|
+
Effects:
|
|
127
|
+
Plot at the current active axis.
|
|
128
|
+
"""
|
|
129
|
+
# The checker only checks if the first element has the correct dimension.
|
|
130
|
+
# This is bad, but performant.
|
|
131
|
+
match len(shape[0]):
|
|
132
|
+
case 2:
|
|
133
|
+
_chull_2d(np.array(shape)) # type: ignore[arg-type]
|
|
134
|
+
case 3:
|
|
135
|
+
_chull_3d(np.array(shape)) # type: ignore
|
|
136
|
+
case _:
|
|
137
|
+
raise ValueError("Input must be either 2 or 3")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _chull_3d(shape: Array2D) -> None:
|
|
141
|
+
ax: Axes3D = plt.gca() # type: ignore[no-any-unimported]
|
|
142
|
+
ensure_axes_dimension(ax, 3)
|
|
143
|
+
|
|
144
|
+
hull = ConvexHull(shape)
|
|
145
|
+
for s in hull.simplices:
|
|
146
|
+
tri = Poly3DCollection([shape[s]])
|
|
147
|
+
|
|
148
|
+
if "alpha" in FILL_CONFIG:
|
|
149
|
+
tri.set_alpha(FILL_CONFIG["alpha"])
|
|
150
|
+
if "color" in FILL_CONFIG:
|
|
151
|
+
tri.set_color(FILL_CONFIG["color"])
|
|
152
|
+
|
|
153
|
+
tri.set_edgecolor('none')
|
|
154
|
+
ax.add_collection3d(tri)
|
|
155
|
+
edges = []
|
|
156
|
+
if distance.euclidean(shape[s[0]], shape[s[1]])\
|
|
157
|
+
< distance.euclidean(shape[s[1]], shape[s[2]]):
|
|
158
|
+
edges.append((s[0], s[1]))
|
|
159
|
+
if distance.euclidean(shape[s[1]], shape[s[2]])\
|
|
160
|
+
< distance.euclidean(shape[s[2]], shape[s[0]]):
|
|
161
|
+
edges.append((s[1], s[2]))
|
|
162
|
+
else:
|
|
163
|
+
edges.append((s[2], s[0]))
|
|
164
|
+
else:
|
|
165
|
+
edges.append((s[1], s[2]))
|
|
166
|
+
if distance.euclidean(shape[s[0]], shape[s[1]]) <\
|
|
167
|
+
distance.euclidean(shape[s[2]], shape[s[0]]):
|
|
168
|
+
edges.append((s[0], s[1]))
|
|
169
|
+
else:
|
|
170
|
+
edges.append((s[2], s[0]))
|
|
171
|
+
for v0, v1 in edges:
|
|
172
|
+
ax.plot(xs=shape[[v0, v1], 0],
|
|
173
|
+
ys=shape[[v0, v1], 1],
|
|
174
|
+
zs=shape[[v0, v1], 2],
|
|
175
|
+
**EDGE_CONFIG)
|
|
176
|
+
|
|
177
|
+
ax.scatter(shape[:, 0],
|
|
178
|
+
shape[:, 1],
|
|
179
|
+
shape[:, 2],
|
|
180
|
+
marker='o',
|
|
181
|
+
**NODE_CONFIG)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _chull_2d(points: Array2D) -> None:
|
|
185
|
+
ax = plt.gca()
|
|
186
|
+
ensure_axes_dimension(ax, 2)
|
|
187
|
+
hull = ConvexHull(points)
|
|
188
|
+
ax.plot(points[:, 0], points[:, 1], 'o', **NODE_CONFIG) # type: ignore[arg-type] # noqa: E501
|
|
189
|
+
for simplex in hull.simplices:
|
|
190
|
+
ax.plot(points[simplex, 0],
|
|
191
|
+
points[simplex, 1],
|
|
192
|
+
**EDGE_CONFIG) # type: ignore[arg-type]
|
|
193
|
+
|
|
194
|
+
ax.fill(points[hull.vertices, 0],
|
|
195
|
+
points[hull.vertices, 1],
|
|
196
|
+
lw=2,
|
|
197
|
+
**FILL_CONFIG)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class BigArrow(FancyArrowPatch):
|
|
201
|
+
"""An 2- or 3-dimensional arrow.
|
|
202
|
+
|
|
203
|
+
:meta private:
|
|
204
|
+
"""
|
|
205
|
+
def __init__(self,
|
|
206
|
+
start: Vec1D,
|
|
207
|
+
end: Vec1D,
|
|
208
|
+
*args: Any,
|
|
209
|
+
**kwargs: Any):
|
|
210
|
+
default_styles = {
|
|
211
|
+
"mutation_scale": 30,
|
|
212
|
+
"arrowstyle": "-|>",
|
|
213
|
+
"linestyle": "--"
|
|
214
|
+
}
|
|
215
|
+
# start[i] for example can be either a number or a vector.
|
|
216
|
+
super().__init__((start[0], start[1]), # type: ignore[arg-type]
|
|
217
|
+
(end[0], end[1]),
|
|
218
|
+
*args,
|
|
219
|
+
**(kwargs | default_styles))
|
|
220
|
+
# Note that two copies of `start` and `end` are preserved:
|
|
221
|
+
# One copy is passed to ->_posA_posB of the parent class; this copy
|
|
222
|
+
# is used in `draw`.
|
|
223
|
+
# the other copy is passed to ->start and ->end of this class; this
|
|
224
|
+
# copt is used in do_3d_projections.
|
|
225
|
+
self.start = start
|
|
226
|
+
self.end = end
|
|
227
|
+
|
|
228
|
+
def draw(self: Self, renderer: Any) -> None:
|
|
229
|
+
super().draw(renderer)
|
|
230
|
+
|
|
231
|
+
def do_3d_projection(self: Self, renderer: Any = None) -> Any:
|
|
232
|
+
# The reference
|
|
233
|
+
# https://github.com/matplotlib/matplotlib/blob/v3.8.2/lib/
|
|
234
|
+
# mpl_toolkits/mplot3d/art3d.py#L998-L1065
|
|
235
|
+
# appears to return np.min(tzs).
|
|
236
|
+
# Removing it does not seem to change anything. Still, just to be safe.
|
|
237
|
+
if self.axes is None or not isinstance(self.axes, Axes3D):
|
|
238
|
+
raise Exception("Rendered without axes")
|
|
239
|
+
else:
|
|
240
|
+
txs, tys, tzs = proj_transform(*zip(self.start, self.end),
|
|
241
|
+
self.axes.M)
|
|
242
|
+
self.set_positions((txs[0], tys[0]), (txs[1], tys[1]))
|
|
243
|
+
return np.min(tzs)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@overload
|
|
247
|
+
def arrow(start: Point2D, end: Point2D,
|
|
248
|
+
*args: Any,
|
|
249
|
+
**kwargs: Any) -> None:
|
|
250
|
+
pass
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# Suppress the error. This overload will never be matched
|
|
254
|
+
# since :class:`Point3D` and :class:`Point2D` alias
|
|
255
|
+
# the same thing. The overload is there for the user's knowledge.
|
|
256
|
+
@overload
|
|
257
|
+
def arrow(start: Point3D, # type: ignore[overload-cannot-match]
|
|
258
|
+
end: Point3D,
|
|
259
|
+
*args: Any,
|
|
260
|
+
**kwargs: Any) -> None:
|
|
261
|
+
pass
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def arrow(start: Point2D | Point3D,
|
|
265
|
+
end: Point2D | Point3D,
|
|
266
|
+
*args: Any,
|
|
267
|
+
**kwargs: Any) -> None:
|
|
268
|
+
'''Plot an arrow to the current Axes or Axes3D.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
start: The starting point of the arrow.
|
|
272
|
+
end: The ending point of the arrow.
|
|
273
|
+
|
|
274
|
+
'''
|
|
275
|
+
ax = plt.gca()
|
|
276
|
+
# Type checking `ax` is necessary, since the arrow
|
|
277
|
+
# class can handle the difference.
|
|
278
|
+
# Plotting to 2D (projection='rectilinear') Axes calls `draw`.
|
|
279
|
+
# Plotting to 3D (projection='3d') calls do_3d_projection.
|
|
280
|
+
# Still, this function might not work for other projections.
|
|
281
|
+
arrow = BigArrow(start, end, *args, **kwargs)
|
|
282
|
+
ax.add_artist(arrow)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def plot(fun: Callable[[float], float],
|
|
286
|
+
x_range: tuple[float, float],
|
|
287
|
+
density: int = 1000,
|
|
288
|
+
*args: ArrayLike,
|
|
289
|
+
**kwargs: Any) -> None:
|
|
290
|
+
'''Plot a contour map to the current Axes or Axes3D.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
fun: The function to plot.
|
|
294
|
+
x_range: A tuple of the beginning and end of the x axis.
|
|
295
|
+
density: Number of data point to plot. These points are
|
|
296
|
+
evenly spaced over (:arg:`x_range` * :arg:`y_range`).
|
|
297
|
+
'''
|
|
298
|
+
ax = plt.gca()
|
|
299
|
+
x_max: float = max(x_range)
|
|
300
|
+
x_min: float = min(x_range)
|
|
301
|
+
xs, ys = _make_ys(fun=fun,
|
|
302
|
+
x_range=(x_min, x_max),
|
|
303
|
+
density=density)
|
|
304
|
+
ax.plot(xs, ys, *args, **kwargs)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _make_zs(fun: Callable[[float, float], float],
|
|
308
|
+
x_range: tuple[float, float],
|
|
309
|
+
y_range: tuple[float, float],
|
|
310
|
+
density: int = 100,) -> tuple[ndarray, ndarray, ndarray]:
|
|
311
|
+
|
|
312
|
+
x_max: float = max(x_range)
|
|
313
|
+
x_min: float = min(x_range)
|
|
314
|
+
y_max: float = max(y_range)
|
|
315
|
+
y_min: float = min(y_range)
|
|
316
|
+
|
|
317
|
+
xs = np.arange(x_min, x_max, step=(x_max - x_min) / density)
|
|
318
|
+
ys = np.arange(y_min, y_max, step=(y_max - y_min) / density)
|
|
319
|
+
|
|
320
|
+
xs, ys = np.meshgrid(xs, ys)
|
|
321
|
+
zs = np.vectorize(fun)(xs, ys)
|
|
322
|
+
|
|
323
|
+
# This code is for functions that cannot be properly vectorised.
|
|
324
|
+
# zs = np.empty(shape=(len(ys), len(xs)))
|
|
325
|
+
# for i, x in enumerate(xs):
|
|
326
|
+
# for j, y in enumerate(ys):
|
|
327
|
+
# zs[j][i] = fun(x, y)
|
|
328
|
+
|
|
329
|
+
return (xs, ys, zs)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def contour(fun: Callable[[float, float], float],
|
|
333
|
+
x_range: tuple[float, float],
|
|
334
|
+
y_range: tuple[float, float],
|
|
335
|
+
density: int = 100,
|
|
336
|
+
levels: int = 50,
|
|
337
|
+
cmap: str = CONTOUR_CMAP,
|
|
338
|
+
colorbar: bool = True,
|
|
339
|
+
alpha: float = 0.5) -> None:
|
|
340
|
+
'''Plot a contour map to the current Axes3D.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
fun: The function to plot.
|
|
344
|
+
x_range: A tuple of the beginning and end of the x axis.
|
|
345
|
+
y_range: A tuple of the beginning and end of the y axis.
|
|
346
|
+
density: Number of point to plot. These points are
|
|
347
|
+
evenly spaced over (:arg:`x_range` * :arg:`y_range`).
|
|
348
|
+
levels: The number of contour lines.
|
|
349
|
+
cmap: The colour map used by the contour map.
|
|
350
|
+
colorbar: If True, draw the colour bar.
|
|
351
|
+
'''
|
|
352
|
+
ax = plt.gca()
|
|
353
|
+
xs, ys, zs = _make_zs(fun, x_range, y_range, density)
|
|
354
|
+
cs = ax.contour(xs, ys, zs, levels=levels, cmap=cmap,
|
|
355
|
+
norm=colors.Normalize(vmin=zs.min(),
|
|
356
|
+
vmax=zs.max()),
|
|
357
|
+
alpha=alpha)
|
|
358
|
+
|
|
359
|
+
current_figure = ax.get_figure()
|
|
360
|
+
if colorbar and current_figure is not None:
|
|
361
|
+
current_figure.colorbar(cs)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def wireframe(fun: # type: ignore[no-any-unimported]
|
|
365
|
+
# Reason: I'm not sure which import is causing this.
|
|
366
|
+
Callable[[float, float], float],
|
|
367
|
+
x_range: tuple[float, float],
|
|
368
|
+
y_range: tuple[float, float],
|
|
369
|
+
density: int = 100,
|
|
370
|
+
cmap: str = CONTOUR_CMAP,
|
|
371
|
+
alpha: float = 0.9,
|
|
372
|
+
**kwargs: dict[str, Any]) -> Line3DCollection:
|
|
373
|
+
'''Plot a wireframe map to the current Axes or Axes3D.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
fun: The function to plot.
|
|
377
|
+
x_range: A tuple of the beginning and end of the x axis.
|
|
378
|
+
y_range: A tuple of the beginning and end of the y axis.
|
|
379
|
+
density: Number of point to plot. These points are
|
|
380
|
+
evenly spaced over :arg:`x_range`.
|
|
381
|
+
cmap: The colour map used by the contour map.
|
|
382
|
+
alpha: Alpha value (transparency) of the frame.
|
|
383
|
+
'''
|
|
384
|
+
ax: Axes3D = plt.gca() # type: ignore[no-any-unimported]
|
|
385
|
+
xs, ys, zs = _make_zs(fun, x_range, y_range, density)
|
|
386
|
+
return ax.plot_wireframe(xs, ys, zs,
|
|
387
|
+
cmap=cmap,
|
|
388
|
+
norm=colors.Normalize(vmin=zs.min(),
|
|
389
|
+
vmax=zs.max()),
|
|
390
|
+
alpha=alpha,
|
|
391
|
+
**kwargs)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def surface(fun: Callable[[float, float], float], # type: ignore[no-any-unimported] # noqa: E501
|
|
395
|
+
x_range: tuple[float, float],
|
|
396
|
+
y_range: tuple[float, float],
|
|
397
|
+
density: int = 100,
|
|
398
|
+
cmap: str = CONTOUR_CMAP,
|
|
399
|
+
colorbar: bool = True,
|
|
400
|
+
alpha: float = 0.9,) -> Line3DCollection:
|
|
401
|
+
ax: Axes3D = plt.gca() # type: ignore[no-any-unimported]
|
|
402
|
+
xs, ys, zs = _make_zs(fun, x_range, y_range, density)
|
|
403
|
+
cs = ax.plot_surface(xs, ys, zs,
|
|
404
|
+
cmap=cmap,
|
|
405
|
+
norm=colors.Normalize(vmin=zs.min(), vmax=zs.max()),
|
|
406
|
+
alpha=alpha,
|
|
407
|
+
rstride=1,
|
|
408
|
+
cstride=1,
|
|
409
|
+
edgecolor='none')
|
|
410
|
+
|
|
411
|
+
if colorbar:
|
|
412
|
+
ax.get_figure().colorbar(cs)
|
|
413
|
+
return cs
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _make_ys(fun: Callable[[float], float],
|
|
417
|
+
x_range: tuple[float, float],
|
|
418
|
+
density: int = 20) -> tuple[Vec1D, Vec1D]:
|
|
419
|
+
x_max: float = max(x_range)
|
|
420
|
+
x_min: float = min(x_range)
|
|
421
|
+
xs = np.linspace(x_min, x_max, num=density, dtype=np.float64)
|
|
422
|
+
ys = np.array([fun(x) for x in xs])
|
|
423
|
+
return xs, ys
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def stems(fun: Callable[[float], float],
|
|
427
|
+
x_range: tuple[float, float],
|
|
428
|
+
density: int = 30,
|
|
429
|
+
plot_links: bool = False,
|
|
430
|
+
*,
|
|
431
|
+
edge_args: dict[str, Any] = {},
|
|
432
|
+
node_args: dict[str, Any] = {},
|
|
433
|
+
fill_args: dict[str, Any] = {}) -> None:
|
|
434
|
+
"""PLot a stem plot to the current Axes.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
x_range: A tuple of the beginning and end of the x axis.
|
|
438
|
+
density: Number of point to plot. These points are
|
|
439
|
+
evenly spaced over :arg:`x_range`.
|
|
440
|
+
plot_links: If ``True``, then plot links.
|
|
441
|
+
"""
|
|
442
|
+
|
|
443
|
+
xs, ys = _make_ys(fun, x_range, density)
|
|
444
|
+
|
|
445
|
+
stem(xs=xs,
|
|
446
|
+
ys=ys,
|
|
447
|
+
plot_links=plot_links,
|
|
448
|
+
edge_args=edge_args,
|
|
449
|
+
node_args=node_args,
|
|
450
|
+
fill_args=fill_args)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def stem(xs: Vec1D, ys: Vec1D,
|
|
454
|
+
plot_links: bool = False,
|
|
455
|
+
*,
|
|
456
|
+
edge_args: dict[str, Any] = {},
|
|
457
|
+
node_args: dict[str, Any] = {},
|
|
458
|
+
fill_args: dict[str, Any] = {}) -> None:
|
|
459
|
+
"""Plot a stem plot to the current Axes.
|
|
460
|
+
Similar to :meth:`Axes.stem`, but offers more control over style.
|
|
461
|
+
"""
|
|
462
|
+
|
|
463
|
+
edge_args = EDGE_CONFIG | edge_args
|
|
464
|
+
# The default node style now borrows from edge style.
|
|
465
|
+
node_args = node_args
|
|
466
|
+
fill_args = FILL_CONFIG | fill_args
|
|
467
|
+
|
|
468
|
+
ax = plt.gca()
|
|
469
|
+
|
|
470
|
+
# Incorporate alpha into edge color. Because `->scatter(.)` does not allow
|
|
471
|
+
# alpha to be specified for `edgecolors`, this is necessary.
|
|
472
|
+
|
|
473
|
+
# Suppressing this error because `.get` is maltyped to return
|
|
474
|
+
# (Any | None). This is not very helpful.
|
|
475
|
+
edge_color: Optional[ColorType]\
|
|
476
|
+
= edge_args.get("color") # type: ignore[assignment]
|
|
477
|
+
edge_alpha: Optional[float]\
|
|
478
|
+
= edge_args.get("alpha") # type: ignore[assignment]
|
|
479
|
+
if edge_color is not None and edge_alpha is not None:
|
|
480
|
+
line_color = mpl.colors.colorConverter.to_rgba(
|
|
481
|
+
edge_color,
|
|
482
|
+
edge_alpha
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
old_x = old_y = None
|
|
486
|
+
|
|
487
|
+
if (plot_links):
|
|
488
|
+
ax.plot((min(xs), max(xs)), (0, 0),
|
|
489
|
+
color=line_color,
|
|
490
|
+
linewidth=1,
|
|
491
|
+
zorder=-1)
|
|
492
|
+
|
|
493
|
+
for (x, y) in zip(xs, ys):
|
|
494
|
+
ax.plot([x, x], [0, y],
|
|
495
|
+
zorder=0,
|
|
496
|
+
linewidth=1,
|
|
497
|
+
**edge_args)
|
|
498
|
+
|
|
499
|
+
if (plot_links):
|
|
500
|
+
if old_x is not None and old_y is not None:
|
|
501
|
+
#Hull it
|
|
502
|
+
ax.plot((old_x, x), (old_y, y), color="#696969", linewidth=0.5,
|
|
503
|
+
zorder=-1)
|
|
504
|
+
ax.fill_between((old_x, x), (old_y, y),
|
|
505
|
+
zorder=-2,
|
|
506
|
+
color="#F5F5F5", **fill_args)
|
|
507
|
+
old_x = x
|
|
508
|
+
old_y = y
|
|
509
|
+
|
|
510
|
+
ax.scatter(xs, ys, zorder=4,
|
|
511
|
+
facecolor=fill_args.get("color", "white"),
|
|
512
|
+
edgecolors=line_color,
|
|
513
|
+
linewidth=1.5,
|
|
514
|
+
**node_args)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
#: Legacy alias for :meth:`stem`.
|
|
518
|
+
shatter = stem
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def _add_patch_to_current_legend(patch: Patch, label: str) -> None:
|
|
522
|
+
ax = plt.gca()
|
|
523
|
+
legend = [c for c in ax.get_children() if isinstance(c, Legend)][0]
|
|
524
|
+
|
|
525
|
+
handles = legend.legend_handles
|
|
526
|
+
labels = legend.texts
|
|
527
|
+
|
|
528
|
+
handles.append(patch)
|
|
529
|
+
labels.append(Text(0, 0, label))
|
|
530
|
+
|
|
531
|
+
plt.legend(handles)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def patch(facecolor: ColorType,
|
|
535
|
+
label: str,
|
|
536
|
+
alpha: float = 1,
|
|
537
|
+
width: float = 1,
|
|
538
|
+
height: float = 0.8) -> None:
|
|
539
|
+
"""Add and label a rectangular colour patch to the current
|
|
540
|
+
plot.
|
|
541
|
+
|
|
542
|
+
Args:
|
|
543
|
+
label: Label to the patch.
|
|
544
|
+
alpha: Alpha value of the patch.
|
|
545
|
+
width: Width of the patch.
|
|
546
|
+
height: Height of the patch.
|
|
547
|
+
"""
|
|
548
|
+
new_patch = Patch(facecolor=facecolor,
|
|
549
|
+
edgecolor=facecolor,
|
|
550
|
+
label=label,
|
|
551
|
+
alpha=alpha)
|
|
552
|
+
|
|
553
|
+
if plt.gca().get_legend() is None:
|
|
554
|
+
plt.gca().legend(handles=[new_patch],
|
|
555
|
+
handlelength=width,
|
|
556
|
+
handleheight=height,
|
|
557
|
+
loc="lower left")
|
|
558
|
+
else:
|
|
559
|
+
_add_patch_to_current_legend(Patch(facecolor=facecolor,
|
|
560
|
+
edgecolor=facecolor,
|
|
561
|
+
label=label,
|
|
562
|
+
alpha=alpha),
|
|
563
|
+
label=label)
|
ograph/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Whatever
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ograph
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Experimental Matplotlib API
|
|
5
|
+
Author-email: Lyodine <lyodine@github.com>
|
|
6
|
+
Project-URL: Documentation, https://yidingli.com/docs/ograph/
|
|
7
|
+
Project-URL: Repository, https://yidingli.com/projects/ograph/
|
|
8
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Python: >=3.12.0
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE.md
|
|
18
|
+
Requires-Dist: matplotlib>=3.8.4
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
<p align="center">
|
|
22
|
+
<img src="./media/alternative_logo.png" width=100>
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# OGraph
|
|
27
|
+
|
|
28
|
+
A lightweight Matplotlib wrapper.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
Install from PyPI:
|
|
33
|
+
|
|
34
|
+
```shell
|
|
35
|
+
pip install ograph
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Install from source:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
pip install .
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Please see [documentation](https://yidingli.com/projects/ograph/docs/install-and-build.html) for detailed instructions.
|
|
45
|
+
|
|
46
|
+
## Components
|
|
47
|
+
|
|
48
|
+
The library have the following modules:
|
|
49
|
+
|
|
50
|
+
| Component | Description |
|
|
51
|
+
| -------------------------------------------------- | -------------------------- |
|
|
52
|
+
| [ofig](./ograph/ofig/) | Create and check the dimensions of plots |
|
|
53
|
+
| [oplot](./ograph/oplot/) | Plot to the current axes |
|
|
54
|
+
| [oconfig](./ograph/oconfig/) | Configure plots |
|
|
55
|
+
| [ofunc](./ograph/ofunc/) | Supply test functions and matrices |
|
|
56
|
+
| [applications.swarm](./ograph/applications/swarm/) | Plot point clusters |
|
|
57
|
+
|
|
58
|
+
The ograph.oplot library contains the following plotters:
|
|
59
|
+
|
|
60
|
+
| Plotter | Dimension of Plot | Data |
|
|
61
|
+
| --------- | ----------------- | ------------------ |
|
|
62
|
+
| heatmap | 2 | $M\times N$ matrix |
|
|
63
|
+
| chull | 2, 3 | $R^2$ or $R^3$ |
|
|
64
|
+
| arrow | 2, 3 | $R^2$ or $R^3$ |
|
|
65
|
+
| plot | 2 | $R\rightarrow R$ |
|
|
66
|
+
| wireframe | 3 | $R^2\rightarrow R$ |
|
|
67
|
+
| surface | 3 | $R^2\rightarrow R$ |
|
|
68
|
+
| stems | 2 | $R\rightarrow R$ |
|
|
69
|
+
| stem | 2 | `xs` and `ys |
|
|
70
|
+
| patch | Any | Colours and labels |
|
|
71
|
+
|
|
72
|
+
The `application.swarm` module contains the following plotters:
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
plot_positions 2 list[R^2] or list[list[R^2]]
|
|
76
|
+
plot_bests 2 list[R^2]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
docs/source/conf.py,sha256=_UfMQZptjyv84FrdYGo3yI6ED5DdtmKD6xnP9PG4CuQ,2473
|
|
2
|
+
ograph/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
ograph/oconfig.py,sha256=AegLHVKt9yOzTaSG-7qARmUinISJsLh3FjdUcxcF8ck,2883
|
|
4
|
+
ograph/ofig.py,sha256=fPw6fuWsY5rh5xnds0GP0jZrA199GASl1w5rHN3bisc,6325
|
|
5
|
+
ograph/ofunc.py,sha256=Y8yrnU8KSIe82vHRJZXnHuYig9n3lDuyOgcDBRjwcuc,1068
|
|
6
|
+
ograph/oplot.py,sha256=yGxyR2X9kZRRsXaHkpAyakvo-hEMZzcMu3f3Uu9gW1k,19435
|
|
7
|
+
ograph/py.typed,sha256=5JcTXlyUgcObw15iknvFO3ytTtMZPxgx5j7maXO5cLE,8
|
|
8
|
+
ograph/applications/__init__.py,sha256=TTgep3kTCzWGUQL1Bg2rprpyoP8tT7Ru5emWw5tgYUc,109
|
|
9
|
+
ograph/applications/swarm.py,sha256=R5UyhGPehEhCq3sdT99DTPhw4r1QI7dYtZT2TC6yW78,4165
|
|
10
|
+
ograph-1.0.0.dist-info/licenses/LICENSE.md,sha256=nQuuPcBjE5l5-ARuWaTQBbL0AIqq5-P_5myIcxqajfE,1086
|
|
11
|
+
ograph-1.0.0.dist-info/METADATA,sha256=0Rzj6DYTnMwZ7c4GEHoM59r_FYvKA7_aI34sJuM8Wjo,2590
|
|
12
|
+
ograph-1.0.0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
|
|
13
|
+
ograph-1.0.0.dist-info/top_level.txt,sha256=cPMrKsahSsD8lnF-ud1xpxH8vbs22gfyrTWq6EH1fk0,12
|
|
14
|
+
ograph-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2025 Lyodine
|
|
4
|
+
|
|
5
|
+
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:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
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.
|