ograph 0.1__tar.gz
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.
- ograph-0.1/LICENSE.md +1 -0
- ograph-0.1/PKG-INFO +19 -0
- ograph-0.1/README.md +1 -0
- ograph-0.1/pyproject.toml +36 -0
- ograph-0.1/setup.cfg +4 -0
- ograph-0.1/src/__init__.py +0 -0
- ograph-0.1/src/ofig.py +45 -0
- ograph-0.1/src/ofunc.py +9 -0
- ograph-0.1/src/ograph.egg-info/PKG-INFO +19 -0
- ograph-0.1/src/ograph.egg-info/SOURCES.txt +12 -0
- ograph-0.1/src/ograph.egg-info/dependency_links.txt +1 -0
- ograph-0.1/src/ograph.egg-info/requires.txt +1 -0
- ograph-0.1/src/ograph.egg-info/top_level.txt +4 -0
- ograph-0.1/src/oplot.py +481 -0
ograph-0.1/LICENSE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
All rights reserved for the time being.
|
ograph-0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ograph
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: Experimental Matplotlib API
|
|
5
|
+
Author-email: Lyodine <lyodine@github.com>
|
|
6
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
7
|
+
Classifier: Intended Audience :: Developers
|
|
8
|
+
Classifier: Intended Audience :: Science/Research
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
11
|
+
Classifier: Natural Language :: English
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Python: >=3.12.0
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE.md
|
|
17
|
+
Requires-Dist: matplotlib>=3.8.4
|
|
18
|
+
|
|
19
|
+
This is README.
|
ograph-0.1/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This is README.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
|
|
3
|
+
name = "ograph"
|
|
4
|
+
version = "0.1"
|
|
5
|
+
description = "Experimental Matplotlib API"
|
|
6
|
+
dependencies = ["matplotlib>=3.8.4"]
|
|
7
|
+
requires-python = ">= 3.12.0"
|
|
8
|
+
|
|
9
|
+
authors = [
|
|
10
|
+
{name = "Lyodine", email = "lyodine@github.com"},
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
|
|
15
|
+
license = {file = "LICENSE.txt"}
|
|
16
|
+
|
|
17
|
+
keywords = []
|
|
18
|
+
|
|
19
|
+
# Overtagging
|
|
20
|
+
classifiers = [
|
|
21
|
+
"Development Status :: 2 - Pre-Alpha",
|
|
22
|
+
|
|
23
|
+
"Intended Audience :: Developers",
|
|
24
|
+
"Intended Audience :: Science/Research",
|
|
25
|
+
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: Implementation :: CPython",
|
|
28
|
+
|
|
29
|
+
"Natural Language :: English ",
|
|
30
|
+
"Topic :: Software Development :: Libraries :: Python Modules ",
|
|
31
|
+
"Typing :: Typed",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[build-system]
|
|
35
|
+
requires = ["setuptools > 60.1.1"]
|
|
36
|
+
build-backend = "setuptools.build_meta"
|
ograph-0.1/setup.cfg
ADDED
|
File without changes
|
ograph-0.1/src/ofig.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom plotting library.
|
|
3
|
+
A point of improvement: create a global manager pool, so that the manager count does
|
|
4
|
+
not leak.
|
|
5
|
+
"""
|
|
6
|
+
import matplotlib.pyplot as plt
|
|
7
|
+
from matplotlib.axes import Axes
|
|
8
|
+
from matplotlib.figure import Figure
|
|
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
|
+
|
|
14
|
+
|
|
15
|
+
def fig2(xlims: Optional[Tuple[float, float]] = None,
|
|
16
|
+
ylims: Optional[Tuple[float, float]] = None,
|
|
17
|
+
*args: tuple, **kwargs: Any) -> Tuple[Figure, Axes]:
|
|
18
|
+
fig = plt.figure()
|
|
19
|
+
ax = plt.axes(*args, projection='rectilinear', **kwargs)
|
|
20
|
+
if (xlims is not None):
|
|
21
|
+
ax.set_xlim(*xlims)
|
|
22
|
+
if (ylims is not None):
|
|
23
|
+
ax.set_ylim(*ylims)
|
|
24
|
+
return (fig, ax)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def fig3(xlims: Optional[Tuple[float, float]] = None, # type: ignore[no-any-unimported]
|
|
28
|
+
ylims: Optional[Tuple[float, float]] = None,
|
|
29
|
+
zlims: Optional[Tuple[float, float]] = None,
|
|
30
|
+
arg: tuple[float, float, float, float] | None = None,
|
|
31
|
+
**kwargs: Any) \
|
|
32
|
+
-> Tuple[Figure, Axes3D]:
|
|
33
|
+
|
|
34
|
+
fig = plt.figure()
|
|
35
|
+
ax: Axes3D = plt.axes(arg, # type: ignore[no-any-unimported]
|
|
36
|
+
projection='3d',
|
|
37
|
+
**kwargs)
|
|
38
|
+
|
|
39
|
+
if (xlims is not None):
|
|
40
|
+
ax.set_xlim(*xlims)
|
|
41
|
+
if (ylims is not None):
|
|
42
|
+
ax.set_ylim(*ylims)
|
|
43
|
+
if (zlims is not None):
|
|
44
|
+
ax.set_zlim(*zlims)
|
|
45
|
+
return (fig, ax)
|
ograph-0.1/src/ofunc.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
def rosenbrock(*args: float) -> float:
|
|
2
|
+
result: float = 0
|
|
3
|
+
for i in range(len(args) - 1):
|
|
4
|
+
result += 100 * (args[i + 1] - args[i]**2)**2 + (1 - args[i])**2
|
|
5
|
+
return result
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def himmelblau(x: float, y: float) -> float:
|
|
9
|
+
return (x ** 2 + y - 11) ** 2 + (x + y ** 2 - 7) ** 2
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ograph
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: Experimental Matplotlib API
|
|
5
|
+
Author-email: Lyodine <lyodine@github.com>
|
|
6
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
7
|
+
Classifier: Intended Audience :: Developers
|
|
8
|
+
Classifier: Intended Audience :: Science/Research
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
11
|
+
Classifier: Natural Language :: English
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Python: >=3.12.0
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE.md
|
|
17
|
+
Requires-Dist: matplotlib>=3.8.4
|
|
18
|
+
|
|
19
|
+
This is README.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE.md
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/__init__.py
|
|
5
|
+
src/ofig.py
|
|
6
|
+
src/ofunc.py
|
|
7
|
+
src/oplot.py
|
|
8
|
+
src/ograph.egg-info/PKG-INFO
|
|
9
|
+
src/ograph.egg-info/SOURCES.txt
|
|
10
|
+
src/ograph.egg-info/dependency_links.txt
|
|
11
|
+
src/ograph.egg-info/requires.txt
|
|
12
|
+
src/ograph.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
matplotlib>=3.8.4
|
ograph-0.1/src/oplot.py
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import matplotlib.pyplot as plt
|
|
2
|
+
import matplotlib.pylab as pylab
|
|
3
|
+
import matplotlib.colors as colors
|
|
4
|
+
import numpy as np
|
|
5
|
+
import ofig as of
|
|
6
|
+
import logging
|
|
7
|
+
from typing import TypeAlias, Any, Self
|
|
8
|
+
import math
|
|
9
|
+
from numpy.typing import ArrayLike
|
|
10
|
+
|
|
11
|
+
from typing import Optional, Sequence, Annotated, Callable
|
|
12
|
+
|
|
13
|
+
from scipy.spatial import ConvexHull, distance # type: ignore[import-untyped]
|
|
14
|
+
|
|
15
|
+
from matplotlib.axes import Axes
|
|
16
|
+
from matplotlib.patches import FancyArrowPatch
|
|
17
|
+
|
|
18
|
+
from mpl_toolkits.mplot3d.art3d import Poly3DCollection # type: ignore[import-untyped]
|
|
19
|
+
from mpl_toolkits.mplot3d.axes3d import Axes3D # type: ignore[import-untyped]
|
|
20
|
+
from mpl_toolkits.mplot3d.art3d import Line3DCollection
|
|
21
|
+
from mpl_toolkits.mplot3d.proj3d import proj_transform # type: ignore[import-untyped]
|
|
22
|
+
|
|
23
|
+
from numpy import ndarray
|
|
24
|
+
|
|
25
|
+
Array2D: TypeAlias = Annotated[ndarray, (2, 2)]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
FILL_COLOR: dict[str, str | float] = {"alpha": 0.5}
|
|
29
|
+
EDGE_COLOR = {"color": "black", "alpha": 1}
|
|
30
|
+
VERTEX_COLOR = {"color": "black", "alpha": 0.5}
|
|
31
|
+
CONTOUR_CMAP = "viridis"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class DimensionError(ValueError):
|
|
35
|
+
def __init__(self, expected: str, actual: str):
|
|
36
|
+
super().__init__(f"Dimension mismatch: expected {expected}, got {actual}")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def ensure_axes_dimension(axes: Axes | Axes3D, # type: ignore[no-any-unimported]
|
|
40
|
+
dim: int) -> None:
|
|
41
|
+
""" Assert if the give Axes (or Axes3D) is of the specified dimension.
|
|
42
|
+
If not, create an Axes (or Axes3D) with the correct dimension.
|
|
43
|
+
@param axes The Axes or Axes3D whose dimension is to be checked.
|
|
44
|
+
@exception DimensionMismatchException if the first item does not
|
|
45
|
+
match the specified dimension.
|
|
46
|
+
"""
|
|
47
|
+
dimension_to_name = {2: "rectilinear", 3: "3d"}
|
|
48
|
+
if dim in dimension_to_name:
|
|
49
|
+
if (axes.name != dimension_to_name[dim]):
|
|
50
|
+
raise DimensionError(dimension_to_name[dim], axes.name)
|
|
51
|
+
else:
|
|
52
|
+
of.fig3() if dim == 3 else of.fig2()
|
|
53
|
+
logging.warning(f"The current projection is not `{dim}d`."
|
|
54
|
+
f"A new {"Axes" if dim == 2 else "Axes3D"} is created instead.")
|
|
55
|
+
|
|
56
|
+
#raise ValueError(f"The specified dimension cannot be checked.")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def ensure_matrix_dimension(mat: Sequence[Sequence[float]] | np.ndarray,
|
|
60
|
+
dim: int) -> None:
|
|
61
|
+
""" Examine if the first item in a sequence matches the given dimension.
|
|
62
|
+
If not, raise an exception.
|
|
63
|
+
@param mat A <del>matrix</del> sequence of sequences of numbers.
|
|
64
|
+
@exception DimensionMismatchException if the first item does not match the specified
|
|
65
|
+
dimension.
|
|
66
|
+
"""
|
|
67
|
+
dimension_to_name = {2: 2, 3: 3}
|
|
68
|
+
mat_dim = len(mat[0])
|
|
69
|
+
if dim in dimension_to_name:
|
|
70
|
+
if (mat_dim != dimension_to_name[dim]):
|
|
71
|
+
raise DimensionError(str(dimension_to_name[dim]), str(mat_dim))
|
|
72
|
+
else:
|
|
73
|
+
raise ValueError("The specified dimension cannot be checked.")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# @var A 2-dimensional unit square. Immutable.
|
|
77
|
+
unit_square = np.array(
|
|
78
|
+
[[0, 0],
|
|
79
|
+
[0, 1],
|
|
80
|
+
[1, 0],
|
|
81
|
+
[1, 1]]
|
|
82
|
+
)
|
|
83
|
+
unit_square.flags.writeable = False
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# @var A 3-dimensional unit cube. Immutable.
|
|
87
|
+
unit_cube = np.array(
|
|
88
|
+
[[0, 0, 0],
|
|
89
|
+
[1, 0, 0],
|
|
90
|
+
[0, 1, 0],
|
|
91
|
+
[0, 0, 1],
|
|
92
|
+
[1, 1, 0],
|
|
93
|
+
[0, 1, 1],
|
|
94
|
+
[1, 0, 1],
|
|
95
|
+
[1, 1, 1],]
|
|
96
|
+
)
|
|
97
|
+
unit_cube.flags.writeable = False
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def view_rotate(h_rotate: float, v_rotate: float) -> None:
|
|
101
|
+
""" Rotate the current Axes3D.
|
|
102
|
+
@param h_rotate the degree to rotate vertically
|
|
103
|
+
@param v_rotate the degree to rotate horizontally
|
|
104
|
+
"""
|
|
105
|
+
ax: Axes = plt.gca()
|
|
106
|
+
ensure_axes_dimension(ax, 3)
|
|
107
|
+
if (isinstance(ax, Axes3D)): # Make mypy happy
|
|
108
|
+
ax.view_init(h_rotate, v_rotate)
|
|
109
|
+
else:
|
|
110
|
+
raise Exception("This should not happen. The exception has been checked.")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def view_axis_pos(pos: Optional[str]) -> None:
|
|
114
|
+
""" Position labels and ticks of the current Axes3D.
|
|
115
|
+
@param pos the position, one of 'lower', 'upper', 'default', 'both', 'none', and None
|
|
116
|
+
"""
|
|
117
|
+
accepted_values: list[str] = ['lower', 'upper', 'default', 'both', 'none']
|
|
118
|
+
ax: Axes3D = plt.gca() # type: ignore[no-any-unimported]
|
|
119
|
+
ensure_axes_dimension(ax, 3)
|
|
120
|
+
match pos:
|
|
121
|
+
case None:
|
|
122
|
+
ax.axis('off')
|
|
123
|
+
case other:
|
|
124
|
+
if other in accepted_values:
|
|
125
|
+
for axis in ax.xaxis, ax.yaxis, ax.zaxis:
|
|
126
|
+
axis.set_label_position(other)
|
|
127
|
+
axis.set_ticks_position(other)
|
|
128
|
+
else:
|
|
129
|
+
raise ValueError(f"The input {other} is not one of"
|
|
130
|
+
f"{str(accepted_values)}.")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def high_res() -> None:
|
|
134
|
+
"""Update the current rcParams to generate figures with 200 DPI.
|
|
135
|
+
@effect Update rcParams.
|
|
136
|
+
"""
|
|
137
|
+
pylab.rcParams.update({'figure.dpi': 200})
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def annotate(title: str,
|
|
141
|
+
xlabel: str,
|
|
142
|
+
ylabel: str,
|
|
143
|
+
zlabel: Optional[str] = None) -> None:
|
|
144
|
+
"""Add annotations (title, xlabel, ylabel) to the current figure.
|
|
145
|
+
Also set runtime configurations.
|
|
146
|
+
@param title Title of the figure
|
|
147
|
+
@param xlabel Labels for the x axis
|
|
148
|
+
@param ylabel Labels for the y axis
|
|
149
|
+
@param zlabel Labels for the z axis
|
|
150
|
+
@effect Plot to the current active figure; update rcParams.
|
|
151
|
+
"""
|
|
152
|
+
# import matplotlib.pylab as pylab
|
|
153
|
+
# To reset the parameters, use: matplotlib.rcParams.update(matplotlib.rcParamsDefault)
|
|
154
|
+
axes_label_size: str = "x-large"
|
|
155
|
+
plot_title_size: str = "x-large"
|
|
156
|
+
|
|
157
|
+
font = {'legend.fontsize': 'x-large',
|
|
158
|
+
'axes.titlesize': plot_title_size,
|
|
159
|
+
'axes.labelsize': axes_label_size,
|
|
160
|
+
'xtick.labelsize': axes_label_size,
|
|
161
|
+
'ytick.labelsize': axes_label_size,
|
|
162
|
+
'text.usetex': False,
|
|
163
|
+
'font.family': 'Open Sans',
|
|
164
|
+
'axes.titlepad': 15, }
|
|
165
|
+
|
|
166
|
+
ax: Axes | Axes3D = plt.gca() # type: ignore[no-any-unimported]
|
|
167
|
+
|
|
168
|
+
pylab.rcParams.update(font)
|
|
169
|
+
ax.set_xlabel(xlabel, fontname='PT Serif')
|
|
170
|
+
ax.set_ylabel(ylabel, fontname='PT Serif')
|
|
171
|
+
if (zlabel is not None):
|
|
172
|
+
ensure_axes_dimension(ax, 3)
|
|
173
|
+
if (isinstance(ax, Axes3D)):
|
|
174
|
+
ax.set_zlabel(zlabel, fontname='PT Serif')
|
|
175
|
+
else:
|
|
176
|
+
raise Exception("This should not happen.")
|
|
177
|
+
|
|
178
|
+
my_fig = ax.get_figure()
|
|
179
|
+
|
|
180
|
+
if (my_fig is not None):
|
|
181
|
+
my_fig.suptitle(title, fontname='PT Serif')
|
|
182
|
+
else:
|
|
183
|
+
raise Exception("Somehow the Axes is not attached to a Figure. How?")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def heatmap(data: ndarray,
|
|
187
|
+
xlabels: Optional[Sequence[str]] = None,
|
|
188
|
+
ylabels: Optional[Sequence[str]] = None) -> None:
|
|
189
|
+
"""Plot a heatmap from a square matrix.
|
|
190
|
+
@param data A matrix.
|
|
191
|
+
@param xlabels Labels for cells along the x axis.
|
|
192
|
+
@param ylabels Labels for cells along the y axis.
|
|
193
|
+
@effect Plot to the current active Axes.
|
|
194
|
+
"""
|
|
195
|
+
if not isinstance(data, ndarray):
|
|
196
|
+
# If the input is not an numpy array, attempt to cast it into one.
|
|
197
|
+
data = np.array(data)
|
|
198
|
+
|
|
199
|
+
# Configure the size of the plot to accommodate the size of each cell
|
|
200
|
+
plt.rcParams["figure.figsize"] = [len(data) * math.sqrt(len(data)),
|
|
201
|
+
len(data[0]) * math.sqrt(len(data[0]))]
|
|
202
|
+
# if len(data) != len(data[0]):
|
|
203
|
+
# raise Exception("Dimension check error, dimension mismatch")
|
|
204
|
+
|
|
205
|
+
xlabels = xlabels if (xlabels is not None)\
|
|
206
|
+
else ["X" + str(i) for i, _ in enumerate(data[0])]
|
|
207
|
+
ylabels = ylabels if (ylabels is not None)\
|
|
208
|
+
else ["Y" + str(i) for i, _ in enumerate(data)]
|
|
209
|
+
|
|
210
|
+
# fig, ax = plt.subplots()
|
|
211
|
+
|
|
212
|
+
ax = plt.gca()
|
|
213
|
+
|
|
214
|
+
ensure_axes_dimension(ax, 2)
|
|
215
|
+
ax.imshow(data, cmap="Greys")
|
|
216
|
+
|
|
217
|
+
# Show all ticks and label them with the respective list entries
|
|
218
|
+
ax.set_xticks(np.arange(len(xlabels)), labels=xlabels)
|
|
219
|
+
ax.set_yticks(np.arange(len(ylabels)), labels=ylabels)
|
|
220
|
+
|
|
221
|
+
# Rotate the tick labels and set their alignment.
|
|
222
|
+
plt.setp(ax.get_xticklabels(),
|
|
223
|
+
rotation=45,
|
|
224
|
+
ha="right",
|
|
225
|
+
rotation_mode="anchor")
|
|
226
|
+
|
|
227
|
+
# Loop over data dimensions and create text annotations.
|
|
228
|
+
print(len(ylabels))
|
|
229
|
+
print(len(xlabels))
|
|
230
|
+
|
|
231
|
+
max_cell_value = data.max()
|
|
232
|
+
min_cell_value = data.min()
|
|
233
|
+
|
|
234
|
+
for i in range(len(ylabels)):
|
|
235
|
+
for j in range(len(xlabels)):
|
|
236
|
+
cell_value_scale = max_cell_value - min_cell_value
|
|
237
|
+
ratio = (data[i, j] - min_cell_value) / cell_value_scale
|
|
238
|
+
ax.text(j, i, "{:.2f}".format(data[i, j]),
|
|
239
|
+
ha="center",
|
|
240
|
+
va="center",
|
|
241
|
+
color="w" if ratio > .6 else "k")
|
|
242
|
+
|
|
243
|
+
my_fig = ax.get_figure()
|
|
244
|
+
if (my_fig is not None):
|
|
245
|
+
my_fig.tight_layout()
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def chull(shape: Annotated[ndarray, (..., 2)] | Annotated[ndarray, (..., 3)]) -> None:
|
|
249
|
+
"""Plot a convex hull to the current active Axes.
|
|
250
|
+
@param shape A sequence of 2- or 3-dimensional points.
|
|
251
|
+
@effect Plot to the current active Axes.
|
|
252
|
+
"""
|
|
253
|
+
# Note that the checker only checks if the first element has the correct dimension.
|
|
254
|
+
match len(shape[0]):
|
|
255
|
+
case 2:
|
|
256
|
+
_chull_2d(shape)
|
|
257
|
+
case 3:
|
|
258
|
+
_chull_3d(shape)
|
|
259
|
+
case _:
|
|
260
|
+
raise ValueError("Input must be either 2 or 3")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _chull_3d(shape: ndarray) -> None:
|
|
264
|
+
ax: Axes3D = plt.gca() # type: ignore[no-any-unimported]
|
|
265
|
+
ensure_axes_dimension(ax, 3)
|
|
266
|
+
# color = 'r'
|
|
267
|
+
|
|
268
|
+
hull = ConvexHull(shape)
|
|
269
|
+
for s in hull.simplices:
|
|
270
|
+
tri = Poly3DCollection([shape[s]])
|
|
271
|
+
|
|
272
|
+
if "alpha" in FILL_COLOR:
|
|
273
|
+
tri.set_alpha(FILL_COLOR["alpha"])
|
|
274
|
+
if "color" in FILL_COLOR:
|
|
275
|
+
tri.set_color(FILL_COLOR["color"])
|
|
276
|
+
|
|
277
|
+
tri.set_edgecolor('none')
|
|
278
|
+
ax.add_collection3d(tri)
|
|
279
|
+
edges = []
|
|
280
|
+
if distance.euclidean(shape[s[0]], shape[s[1]])\
|
|
281
|
+
< distance.euclidean(shape[s[1]], shape[s[2]]):
|
|
282
|
+
edges.append((s[0], s[1]))
|
|
283
|
+
if distance.euclidean(shape[s[1]], shape[s[2]])\
|
|
284
|
+
< distance.euclidean(shape[s[2]], shape[s[0]]):
|
|
285
|
+
edges.append((s[1], s[2]))
|
|
286
|
+
else:
|
|
287
|
+
edges.append((s[2], s[0]))
|
|
288
|
+
else:
|
|
289
|
+
edges.append((s[1], s[2]))
|
|
290
|
+
if distance.euclidean(shape[s[0]], shape[s[1]]) <\
|
|
291
|
+
distance.euclidean(shape[s[2]], shape[s[0]]):
|
|
292
|
+
edges.append((s[0], s[1]))
|
|
293
|
+
else:
|
|
294
|
+
edges.append((s[2], s[0]))
|
|
295
|
+
for v0, v1 in edges:
|
|
296
|
+
ax.plot(xs=shape[[v0, v1], 0],
|
|
297
|
+
ys=shape[[v0, v1], 1],
|
|
298
|
+
zs=shape[[v0, v1], 2],
|
|
299
|
+
**EDGE_COLOR)
|
|
300
|
+
|
|
301
|
+
ax.scatter(shape[:, 0], shape[:, 1], shape[:, 2], marker='o', **VERTEX_COLOR)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _chull_2d(points: ndarray) -> None:
|
|
305
|
+
ax = plt.gca()
|
|
306
|
+
ensure_axes_dimension(ax, 2)
|
|
307
|
+
hull = ConvexHull(points)
|
|
308
|
+
ax.plot(points[:, 0], points[:, 1], 'o', **VERTEX_COLOR) # type: ignore[arg-type]
|
|
309
|
+
for simplex in hull.simplices:
|
|
310
|
+
ax.plot(points[simplex, 0],
|
|
311
|
+
points[simplex, 1],
|
|
312
|
+
**EDGE_COLOR) # type: ignore[arg-type]
|
|
313
|
+
ax.fill(points[hull.vertices, 0], points[hull.vertices, 1], lw=2, **FILL_COLOR)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
Vec2D = Annotated[Sequence[float], 2]
|
|
317
|
+
Vec3D = Annotated[Sequence[float], 3]
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class BigArrow(FancyArrowPatch):
|
|
321
|
+
"""An 2- or 3-dimensional arrow.
|
|
322
|
+
"""
|
|
323
|
+
def __init__(self,
|
|
324
|
+
start: Vec2D | Vec3D,
|
|
325
|
+
end: Vec2D | Vec3D,
|
|
326
|
+
*args: Any,
|
|
327
|
+
**kwargs: Any):
|
|
328
|
+
default_styles = {
|
|
329
|
+
"mutation_scale": 30,
|
|
330
|
+
"arrowstyle": "-|>",
|
|
331
|
+
"linestyle": "--"
|
|
332
|
+
}
|
|
333
|
+
super().__init__((start[0], start[1]),
|
|
334
|
+
(end[0], end[1]),
|
|
335
|
+
*args,
|
|
336
|
+
**(kwargs | default_styles))
|
|
337
|
+
# Note that two copies of `start` and `end` are preserved:
|
|
338
|
+
# One copy is passed to ->_posA_posB of the parent class; this copy
|
|
339
|
+
# is used in `draw`.
|
|
340
|
+
# the other copy is passed to ->start and ->end of this class; this
|
|
341
|
+
# copt is used in do_3d_projections.
|
|
342
|
+
self.start = start
|
|
343
|
+
self.end = end
|
|
344
|
+
|
|
345
|
+
def draw(self: Self, renderer: Any) -> None:
|
|
346
|
+
super().draw(renderer)
|
|
347
|
+
|
|
348
|
+
def do_3d_projection(self: Self, renderer: Any = None) -> Any:
|
|
349
|
+
# The reference
|
|
350
|
+
# https://github.com/matplotlib/matplotlib/blob/v3.8.2/lib/
|
|
351
|
+
# mpl_toolkits/mplot3d/art3d.py#L998-L1065
|
|
352
|
+
# appears to return np.min(tzs).
|
|
353
|
+
# Removing it does not seem to change anything. Still, just to be safe...
|
|
354
|
+
if self.axes is None or not isinstance(self.axes, Axes3D):
|
|
355
|
+
raise Exception("Rendered without axes")
|
|
356
|
+
else:
|
|
357
|
+
txs, tys, tzs = proj_transform(*zip(self.start, self.end), self.axes.M)
|
|
358
|
+
self.set_positions((txs[0], tys[0]), (txs[1], tys[1]))
|
|
359
|
+
return np.min(tzs)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def arrow(start: Vec2D | Vec3D,
|
|
363
|
+
end: Vec2D | Vec3D,
|
|
364
|
+
*args: Any,
|
|
365
|
+
**kwargs: Any) -> None:
|
|
366
|
+
'''Plot an arrow to the current Axes or Axes3D.
|
|
367
|
+
@param start The starting point of the arrow
|
|
368
|
+
@param end The ending point of the arrow
|
|
369
|
+
|
|
370
|
+
'''
|
|
371
|
+
ax = plt.gca()
|
|
372
|
+
# Type checking `ax` is necessary, since the arrow class can handle the difference.
|
|
373
|
+
# Plotting to 2D (projection='rectilinear') Axes calls `draw`.
|
|
374
|
+
# Plotting to 3D (projection='3d') calls do_3d_projection.
|
|
375
|
+
# Still, this function might not work for other projections.
|
|
376
|
+
arrow = BigArrow(start, end, *args, **kwargs)
|
|
377
|
+
ax.add_artist(arrow)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def plot(fun: Callable[[Array2D], Array2D],
|
|
381
|
+
x_range: Vec2D,
|
|
382
|
+
density: int = 1000,
|
|
383
|
+
*args: ArrayLike,
|
|
384
|
+
**kwargs: Any) -> None:
|
|
385
|
+
'''Plot a contour map to the current Axes or Axes3D.
|
|
386
|
+
@param fun The function to plot
|
|
387
|
+
@param x_range A tuple of the beginning and end of the x axis
|
|
388
|
+
@param density the number of points sampled over each axis
|
|
389
|
+
'''
|
|
390
|
+
ax = plt.gca()
|
|
391
|
+
x_max: float = max(x_range)
|
|
392
|
+
x_min: float = min(x_range)
|
|
393
|
+
xs = np.arange(x_min, x_max, (x_max - x_min) / density)
|
|
394
|
+
ys = fun(xs)
|
|
395
|
+
ax.plot(xs, ys, *args, **kwargs)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _make_zs(fun: Callable[[Array2D, Array2D], Array2D],
|
|
399
|
+
x_range: Vec2D,
|
|
400
|
+
y_range: Vec2D,
|
|
401
|
+
density: int = 100,) -> tuple[ndarray, ndarray, ndarray]:
|
|
402
|
+
|
|
403
|
+
x_max: float = max(x_range)
|
|
404
|
+
x_min: float = min(x_range)
|
|
405
|
+
y_max: float = max(y_range)
|
|
406
|
+
y_min: float = min(y_range)
|
|
407
|
+
|
|
408
|
+
xs = np.arange(x_min, x_max, step=(x_max - x_min) / density)
|
|
409
|
+
ys = np.arange(y_min, y_max, step=(y_max - y_min) / density)
|
|
410
|
+
|
|
411
|
+
xs, ys = np.meshgrid(xs, ys)
|
|
412
|
+
zs = fun(xs, ys)
|
|
413
|
+
|
|
414
|
+
return (xs, ys, zs)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def contour(fun: Callable[[Array2D, Array2D], Array2D],
|
|
418
|
+
x_range: Vec2D,
|
|
419
|
+
y_range: Vec2D,
|
|
420
|
+
density: int = 100,
|
|
421
|
+
levels: int = 50,
|
|
422
|
+
cmap: str = CONTOUR_CMAP,
|
|
423
|
+
colorbar: bool = True,
|
|
424
|
+
alpha: float = 0.5) -> None:
|
|
425
|
+
'''Plot a contour map to the current Axes or Axes3D.
|
|
426
|
+
@param fun The function to plot
|
|
427
|
+
@param x_range A tuple of the beginning and end of the x axis
|
|
428
|
+
@param y_range A tuple of the beginning and end of the y axis
|
|
429
|
+
@param Density the number of points sampled over each axis
|
|
430
|
+
@param levels The number of contour lines
|
|
431
|
+
@param cmap The colour map used by the contour map
|
|
432
|
+
@param colorbar If True, draw the colour bar
|
|
433
|
+
'''
|
|
434
|
+
ax = plt.gca()
|
|
435
|
+
xs, ys, zs = _make_zs(fun, x_range, y_range, density)
|
|
436
|
+
ax.set_aspect('equal') # Very important, otherwise axes use different scales.
|
|
437
|
+
cs = ax.contour(xs, ys, zs, levels=levels, cmap=cmap,
|
|
438
|
+
norm=colors.Normalize(vmin=zs.min(), vmax=zs.max()), alpha=alpha)
|
|
439
|
+
|
|
440
|
+
current_figure = ax.get_figure()
|
|
441
|
+
if colorbar and current_figure is not None:
|
|
442
|
+
current_figure.colorbar(cs)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def wireframe(fun: # type: ignore[no-any-unimported]
|
|
446
|
+
Callable[[Array2D, Array2D], Array2D],
|
|
447
|
+
x_range: Vec2D,
|
|
448
|
+
y_range: Vec2D,
|
|
449
|
+
density: int = 100,
|
|
450
|
+
cmap: str = CONTOUR_CMAP,
|
|
451
|
+
alpha: float = 0.9) -> Line3DCollection:
|
|
452
|
+
ax: Axes3D = plt.gca() # type: ignore[no-any-unimported]
|
|
453
|
+
xs, ys, zs = _make_zs(fun, x_range, y_range, density)
|
|
454
|
+
ax.set_aspect('equal') # Very important, otherwise axes use different scales.
|
|
455
|
+
return ax.plot_wireframe(xs, ys, zs,
|
|
456
|
+
cmap=cmap,
|
|
457
|
+
norm=colors.Normalize(vmin=zs.min(), vmax=zs.max()),
|
|
458
|
+
alpha=alpha)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def surface(fun: Callable[[Array2D, Array2D], Array2D], # type: ignore[no-any-unimported]
|
|
462
|
+
x_range: Vec2D,
|
|
463
|
+
y_range: Vec2D,
|
|
464
|
+
density: int = 100,
|
|
465
|
+
cmap: str = CONTOUR_CMAP,
|
|
466
|
+
colorbar: bool = True,
|
|
467
|
+
alpha: float = 0.9) -> Line3DCollection:
|
|
468
|
+
ax: Axes3D = plt.gca() # type: ignore[no-any-unimported]
|
|
469
|
+
xs, ys, zs = _make_zs(fun, x_range, y_range, density)
|
|
470
|
+
ax.set_aspect('equal') # Very important, otherwise axes use different scales.
|
|
471
|
+
cs = ax.plot_surface(xs, ys, zs,
|
|
472
|
+
cmap=cmap,
|
|
473
|
+
norm=colors.Normalize(vmin=zs.min(), vmax=zs.max()),
|
|
474
|
+
alpha=alpha,
|
|
475
|
+
rstride=1,
|
|
476
|
+
cstride=1,
|
|
477
|
+
edgecolor='none')
|
|
478
|
+
|
|
479
|
+
if colorbar:
|
|
480
|
+
ax.get_figure().colorbar(cs)
|
|
481
|
+
return cs
|