Trajectree 0.0.1__py3-none-any.whl → 0.0.2__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.
Files changed (122) hide show
  1. trajectree/__init__.py +0 -3
  2. trajectree/fock_optics/devices.py +1 -1
  3. trajectree/fock_optics/light_sources.py +2 -2
  4. trajectree/fock_optics/measurement.py +3 -3
  5. trajectree/fock_optics/utils.py +6 -6
  6. trajectree/trajectory.py +2 -2
  7. {trajectree-0.0.1.dist-info → trajectree-0.0.2.dist-info}/METADATA +2 -3
  8. trajectree-0.0.2.dist-info/RECORD +16 -0
  9. trajectree/quimb/docs/_pygments/_pygments_dark.py +0 -118
  10. trajectree/quimb/docs/_pygments/_pygments_light.py +0 -118
  11. trajectree/quimb/docs/conf.py +0 -158
  12. trajectree/quimb/docs/examples/ex_mpi_expm_evo.py +0 -62
  13. trajectree/quimb/quimb/__init__.py +0 -507
  14. trajectree/quimb/quimb/calc.py +0 -1491
  15. trajectree/quimb/quimb/core.py +0 -2279
  16. trajectree/quimb/quimb/evo.py +0 -712
  17. trajectree/quimb/quimb/experimental/__init__.py +0 -0
  18. trajectree/quimb/quimb/experimental/autojittn.py +0 -129
  19. trajectree/quimb/quimb/experimental/belief_propagation/__init__.py +0 -109
  20. trajectree/quimb/quimb/experimental/belief_propagation/bp_common.py +0 -397
  21. trajectree/quimb/quimb/experimental/belief_propagation/d1bp.py +0 -316
  22. trajectree/quimb/quimb/experimental/belief_propagation/d2bp.py +0 -653
  23. trajectree/quimb/quimb/experimental/belief_propagation/hd1bp.py +0 -571
  24. trajectree/quimb/quimb/experimental/belief_propagation/hv1bp.py +0 -775
  25. trajectree/quimb/quimb/experimental/belief_propagation/l1bp.py +0 -316
  26. trajectree/quimb/quimb/experimental/belief_propagation/l2bp.py +0 -537
  27. trajectree/quimb/quimb/experimental/belief_propagation/regions.py +0 -194
  28. trajectree/quimb/quimb/experimental/cluster_update.py +0 -286
  29. trajectree/quimb/quimb/experimental/merabuilder.py +0 -865
  30. trajectree/quimb/quimb/experimental/operatorbuilder/__init__.py +0 -15
  31. trajectree/quimb/quimb/experimental/operatorbuilder/operatorbuilder.py +0 -1631
  32. trajectree/quimb/quimb/experimental/schematic.py +0 -7
  33. trajectree/quimb/quimb/experimental/tn_marginals.py +0 -130
  34. trajectree/quimb/quimb/experimental/tnvmc.py +0 -1483
  35. trajectree/quimb/quimb/gates.py +0 -36
  36. trajectree/quimb/quimb/gen/__init__.py +0 -2
  37. trajectree/quimb/quimb/gen/operators.py +0 -1167
  38. trajectree/quimb/quimb/gen/rand.py +0 -713
  39. trajectree/quimb/quimb/gen/states.py +0 -479
  40. trajectree/quimb/quimb/linalg/__init__.py +0 -6
  41. trajectree/quimb/quimb/linalg/approx_spectral.py +0 -1109
  42. trajectree/quimb/quimb/linalg/autoblock.py +0 -258
  43. trajectree/quimb/quimb/linalg/base_linalg.py +0 -719
  44. trajectree/quimb/quimb/linalg/mpi_launcher.py +0 -397
  45. trajectree/quimb/quimb/linalg/numpy_linalg.py +0 -244
  46. trajectree/quimb/quimb/linalg/rand_linalg.py +0 -514
  47. trajectree/quimb/quimb/linalg/scipy_linalg.py +0 -293
  48. trajectree/quimb/quimb/linalg/slepc_linalg.py +0 -892
  49. trajectree/quimb/quimb/schematic.py +0 -1518
  50. trajectree/quimb/quimb/tensor/__init__.py +0 -401
  51. trajectree/quimb/quimb/tensor/array_ops.py +0 -610
  52. trajectree/quimb/quimb/tensor/circuit.py +0 -4824
  53. trajectree/quimb/quimb/tensor/circuit_gen.py +0 -411
  54. trajectree/quimb/quimb/tensor/contraction.py +0 -336
  55. trajectree/quimb/quimb/tensor/decomp.py +0 -1255
  56. trajectree/quimb/quimb/tensor/drawing.py +0 -1646
  57. trajectree/quimb/quimb/tensor/fitting.py +0 -385
  58. trajectree/quimb/quimb/tensor/geometry.py +0 -583
  59. trajectree/quimb/quimb/tensor/interface.py +0 -114
  60. trajectree/quimb/quimb/tensor/networking.py +0 -1058
  61. trajectree/quimb/quimb/tensor/optimize.py +0 -1818
  62. trajectree/quimb/quimb/tensor/tensor_1d.py +0 -4778
  63. trajectree/quimb/quimb/tensor/tensor_1d_compress.py +0 -1854
  64. trajectree/quimb/quimb/tensor/tensor_1d_tebd.py +0 -662
  65. trajectree/quimb/quimb/tensor/tensor_2d.py +0 -5954
  66. trajectree/quimb/quimb/tensor/tensor_2d_compress.py +0 -96
  67. trajectree/quimb/quimb/tensor/tensor_2d_tebd.py +0 -1230
  68. trajectree/quimb/quimb/tensor/tensor_3d.py +0 -2869
  69. trajectree/quimb/quimb/tensor/tensor_3d_tebd.py +0 -46
  70. trajectree/quimb/quimb/tensor/tensor_approx_spectral.py +0 -60
  71. trajectree/quimb/quimb/tensor/tensor_arbgeom.py +0 -3237
  72. trajectree/quimb/quimb/tensor/tensor_arbgeom_compress.py +0 -565
  73. trajectree/quimb/quimb/tensor/tensor_arbgeom_tebd.py +0 -1138
  74. trajectree/quimb/quimb/tensor/tensor_builder.py +0 -5411
  75. trajectree/quimb/quimb/tensor/tensor_core.py +0 -11179
  76. trajectree/quimb/quimb/tensor/tensor_dmrg.py +0 -1472
  77. trajectree/quimb/quimb/tensor/tensor_mera.py +0 -204
  78. trajectree/quimb/quimb/utils.py +0 -892
  79. trajectree/quimb/tests/__init__.py +0 -0
  80. trajectree/quimb/tests/test_accel.py +0 -501
  81. trajectree/quimb/tests/test_calc.py +0 -788
  82. trajectree/quimb/tests/test_core.py +0 -847
  83. trajectree/quimb/tests/test_evo.py +0 -565
  84. trajectree/quimb/tests/test_gen/__init__.py +0 -0
  85. trajectree/quimb/tests/test_gen/test_operators.py +0 -361
  86. trajectree/quimb/tests/test_gen/test_rand.py +0 -296
  87. trajectree/quimb/tests/test_gen/test_states.py +0 -261
  88. trajectree/quimb/tests/test_linalg/__init__.py +0 -0
  89. trajectree/quimb/tests/test_linalg/test_approx_spectral.py +0 -368
  90. trajectree/quimb/tests/test_linalg/test_base_linalg.py +0 -351
  91. trajectree/quimb/tests/test_linalg/test_mpi_linalg.py +0 -127
  92. trajectree/quimb/tests/test_linalg/test_numpy_linalg.py +0 -84
  93. trajectree/quimb/tests/test_linalg/test_rand_linalg.py +0 -134
  94. trajectree/quimb/tests/test_linalg/test_slepc_linalg.py +0 -283
  95. trajectree/quimb/tests/test_tensor/__init__.py +0 -0
  96. trajectree/quimb/tests/test_tensor/test_belief_propagation/__init__.py +0 -0
  97. trajectree/quimb/tests/test_tensor/test_belief_propagation/test_d1bp.py +0 -39
  98. trajectree/quimb/tests/test_tensor/test_belief_propagation/test_d2bp.py +0 -67
  99. trajectree/quimb/tests/test_tensor/test_belief_propagation/test_hd1bp.py +0 -64
  100. trajectree/quimb/tests/test_tensor/test_belief_propagation/test_hv1bp.py +0 -51
  101. trajectree/quimb/tests/test_tensor/test_belief_propagation/test_l1bp.py +0 -142
  102. trajectree/quimb/tests/test_tensor/test_belief_propagation/test_l2bp.py +0 -101
  103. trajectree/quimb/tests/test_tensor/test_circuit.py +0 -816
  104. trajectree/quimb/tests/test_tensor/test_contract.py +0 -67
  105. trajectree/quimb/tests/test_tensor/test_decomp.py +0 -40
  106. trajectree/quimb/tests/test_tensor/test_mera.py +0 -52
  107. trajectree/quimb/tests/test_tensor/test_optimizers.py +0 -488
  108. trajectree/quimb/tests/test_tensor/test_tensor_1d.py +0 -1171
  109. trajectree/quimb/tests/test_tensor/test_tensor_2d.py +0 -606
  110. trajectree/quimb/tests/test_tensor/test_tensor_2d_tebd.py +0 -144
  111. trajectree/quimb/tests/test_tensor/test_tensor_3d.py +0 -123
  112. trajectree/quimb/tests/test_tensor/test_tensor_arbgeom.py +0 -226
  113. trajectree/quimb/tests/test_tensor/test_tensor_builder.py +0 -441
  114. trajectree/quimb/tests/test_tensor/test_tensor_core.py +0 -2066
  115. trajectree/quimb/tests/test_tensor/test_tensor_dmrg.py +0 -388
  116. trajectree/quimb/tests/test_tensor/test_tensor_spectral_approx.py +0 -63
  117. trajectree/quimb/tests/test_tensor/test_tensor_tebd.py +0 -270
  118. trajectree/quimb/tests/test_utils.py +0 -85
  119. trajectree-0.0.1.dist-info/RECORD +0 -126
  120. {trajectree-0.0.1.dist-info → trajectree-0.0.2.dist-info}/WHEEL +0 -0
  121. {trajectree-0.0.1.dist-info → trajectree-0.0.2.dist-info}/licenses/LICENSE +0 -0
  122. {trajectree-0.0.1.dist-info → trajectree-0.0.2.dist-info}/top_level.txt +0 -0
@@ -1,1518 +0,0 @@
1
- """Draw psuedo-3D diagrams using matplotlib.
2
- """
3
-
4
- import functools
5
- import warnings
6
- from math import atan2, cos, pi, sin
7
-
8
- import matplotlib as mpl
9
- import matplotlib.pyplot as plt
10
-
11
-
12
- class Drawing:
13
- """Draw 2D or pseudo-3D diagrams using matplotlib. This handles the
14
- axonometric projection and the z-ordering of the elements, as well as named
15
- preset styles for repeated elements, and the automatic adjustment of the
16
- figure limits. It also has basic support for drawing smooth curves and
17
- shaded areas around certain elements automatically.
18
-
19
- Parameters
20
- ----------
21
- background : color, optional
22
- The background color of the figure, defaults to transparent.
23
- drawcolor : color, optional
24
- The default color to draw lines and text in.
25
- shapecolor : color, optional
26
- The default color to fill shapes with.
27
- a : float
28
- The axonometric angle of the x-axis in degrees.
29
- b : float
30
- The axonometric angle of the y-axis in degrees.
31
- xscale : float
32
- A factor to scale the x-axis by.
33
- yscale : float
34
- A factor to scale the y-axis by.
35
- zscale : float
36
- A factor to scale the z-axis by.
37
- presets : dict
38
- A dictionary of named style presets. When you add an element to the
39
- drawing, you can specify a preset name to use as default styling.
40
- ax : matplotlib.axes.Axes
41
- The axes to draw on. If None, a new figure is created.
42
- kwargs
43
- Passed to ``plt.figure`` if ``ax`` is None.
44
- """
45
-
46
- def __init__(
47
- self,
48
- background=(0, 0, 0, 0),
49
- drawcolor=(0.14, 0.15, 0.16, 1.0),
50
- shapecolor=(0.45, 0.50, 0.55, 1.0),
51
- a=50,
52
- b=12,
53
- xscale=1,
54
- yscale=1,
55
- zscale=1,
56
- presets=None,
57
- ax=None,
58
- **kwargs,
59
- ):
60
- if ax is None:
61
- self.fig = plt.figure(**kwargs)
62
- self.fig.set_facecolor(background)
63
- self.ax = self.fig.add_subplot(111)
64
- else:
65
- self.ax = ax
66
- self.fig = self.ax.figure
67
-
68
- self.ax.set_axis_off()
69
- self.ax.set_aspect("equal")
70
- self.ax.set_clip_on(False)
71
-
72
- self.drawcolor = drawcolor
73
- self.shapecolor = shapecolor
74
-
75
- self._xmin = None
76
- self._xmax = None
77
- self._ymin = None
78
- self._ymax = None
79
- self.presets = {} if presets is None else dict(presets)
80
- self.presets.setdefault(None, {})
81
-
82
- self._2d_project = functools.partial(
83
- simple_scale,
84
- xscale=xscale,
85
- yscale=yscale,
86
- )
87
-
88
- self._3d_project = functools.partial(
89
- axonometric_project,
90
- a=a,
91
- b=b,
92
- xscale=xscale,
93
- yscale=yscale,
94
- zscale=zscale,
95
- )
96
- self._coo_to_zorder = functools.partial(
97
- coo_to_zorder,
98
- xscale=xscale,
99
- yscale=yscale,
100
- zscale=zscale,
101
- )
102
-
103
- def _adjust_lims(self, x, y):
104
- xchange = ychange = False
105
- if self._xmin is None or x < self._xmin:
106
- xchange = True
107
- self._xmin = x
108
-
109
- if self._xmax is None or x > self._xmax:
110
- xchange = True
111
- self._xmax = x
112
-
113
- if self._ymin is None or y < self._ymin:
114
- ychange = True
115
- self._ymin = y
116
-
117
- if self._ymax is None or y > self._ymax:
118
- ychange = True
119
- self._ymax = y
120
-
121
- if xchange and self._xmin != self._xmax:
122
- dx = self._xmax - self._xmin
123
- plot_xmin = self._xmin - dx * 0.01
124
- plot_xmax = self._xmax + dx * 0.01
125
- self.ax.set_xlim(plot_xmin, plot_xmax)
126
- if ychange and self._ymin != self._ymax:
127
- dy = self._ymax - self._ymin
128
- plot_ymin = self._ymin - dy * 0.01
129
- plot_ymax = self._ymax + dy * 0.01
130
- self.ax.set_ylim(plot_ymin, plot_ymax)
131
-
132
- def text(self, coo, text, preset=None, **kwargs):
133
- """Place text at the specified coordinate.
134
-
135
- Parameters
136
- ----------
137
- coo : tuple[int, int] or tuple[int, int, int]
138
- The 2D or 3D coordinate of the text. If 3D, the coordinate will be
139
- projected onto the 2D plane, and a z-order will be assigned.
140
- text : str
141
- The text to place.
142
- preset : str, optional
143
- A preset style to use for the text.
144
- kwargs
145
- Specific style options passed to ``matplotlib.axes.Axes.text``.
146
- """
147
- style = parse_style_preset(self.presets, preset, **kwargs)
148
- style.setdefault("color", self.drawcolor)
149
- style.setdefault("horizontalalignment", "center")
150
- style.setdefault("verticalalignment", "center")
151
- style.setdefault("clip_on", False)
152
-
153
- if len(coo) == 2:
154
- x, y = self._2d_project(*coo)
155
- style.setdefault("zorder", +0.02)
156
- else:
157
- x, y = self._3d_project(*coo)
158
- style.setdefault("zorder", self._coo_to_zorder(*coo) + 0.02)
159
-
160
- self.ax.text(x, y, text, **style)
161
- self._adjust_lims(x, y)
162
-
163
- def text_between(self, cooa, coob, text, preset=None, **kwargs):
164
- """Place text between two coordinates.
165
-
166
- Parameters
167
- ----------
168
- cooa, coob : tuple[int, int] or tuple[int, int, int]
169
- The 2D or 3D coordinates of the text endpoints. If 3D, the
170
- coordinates will be projected onto the 2D plane, and a z-order
171
- will be assigned based on average z-order of the endpoints.
172
- text : str
173
- The text to place.
174
- center : float, optional
175
- The position of the text along the line, where 0.0 is the start and
176
- 1.0 is the end. Default is 0.5.
177
- preset : str, optional
178
- A preset style to use for the text.
179
- kwargs
180
- Specific style options passed to ``matplotlib.axes.Axes.text``.
181
- """
182
- style = parse_style_preset(self.presets, preset, **kwargs)
183
- style.setdefault("color", self.drawcolor)
184
- style.setdefault("horizontalalignment", "center")
185
- style.setdefault("verticalalignment", "center")
186
- style.setdefault("clip_on", False)
187
- center = style.pop("center", 0.5)
188
-
189
- if len(cooa) == 2:
190
- xa, ya = self._2d_project(*cooa)
191
- xb, yb = self._2d_project(*coob)
192
- style.setdefault("zorder", +0.02)
193
- else:
194
- style.setdefault(
195
- "zorder",
196
- mean(self._coo_to_zorder(*coo) for coo in [cooa, coob]) + 0.02,
197
- )
198
- xa, ya = self._3d_project(*cooa)
199
- xb, yb = self._3d_project(*coob)
200
-
201
- # compute midpoint
202
- x = xa * (1 - center) + xb * center
203
- y = ya * (1 - center) + yb * center
204
-
205
- # compute angle
206
- if xa <= xb:
207
- angle = atan2(yb - ya, xb - xa) * 180 / pi
208
- else:
209
- angle = atan2(ya - yb, xa - xb) * 180 / pi
210
- style.setdefault("rotation", angle)
211
-
212
- self.ax.text(x, y, text, **style)
213
- self._adjust_lims(x, y)
214
-
215
- def label_ax(self, x, y, text, preset=None, **kwargs):
216
- """Place text at the specified location, using the axis coordinates
217
- rather than 2D or 3D data coordinates.
218
-
219
- Parameters
220
- ----------
221
- x, y : float
222
- The x and y positions of the text, relative to the axis.
223
- text : str
224
- The text to place.
225
- preset : str, optional
226
- A preset style to use for the text.
227
- kwargs
228
- Specific style options passed to ``matplotlib.axes.Axes.text``.
229
- """
230
- style = parse_style_preset(self.presets, preset, **kwargs)
231
- style.setdefault("color", self.drawcolor)
232
- style.setdefault("horizontalalignment", "center")
233
- style.setdefault("verticalalignment", "center")
234
- style.setdefault("transform", self.ax.transAxes)
235
- self.ax.text(x, y, text, **style)
236
- self._adjust_lims(x, y)
237
-
238
- def label_fig(self, x, y, text, preset=None, **kwargs):
239
- """Place text at the specified location, using the figure coordinates
240
- rather than 2D or 3D data coordinates.
241
-
242
- Parameters
243
- ----------
244
- x, y : float
245
- The x and y positions of the text, relative to the figure.
246
- text : str
247
- The text to place.
248
- preset : str, optional
249
- A preset style to use for the text.
250
- kwargs
251
- Specific style options passed to ``matplotlib.axes.Axes.text``.
252
- """
253
- style = parse_style_preset(self.presets, preset, **kwargs)
254
- style.setdefault("color", self.drawcolor)
255
- style.setdefault("horizontalalignment", "center")
256
- style.setdefault("verticalalignment", "center")
257
- style.setdefault("transform", self.fig.transFigure)
258
- self.ax.text(x, y, text, **style)
259
- self._adjust_lims(x, y)
260
-
261
- def _parse_style_for_marker(self, coo, preset=None, **kwargs):
262
- style = parse_style_preset(self.presets, preset, **kwargs)
263
- if "color" in style:
264
- # assume coloring whole shape
265
- style.setdefault("facecolor", style.pop("color"))
266
- style.setdefault("facecolor", self.shapecolor)
267
- style.setdefault("edgecolor", darken_color(style["facecolor"]))
268
- style.setdefault("linewidth", 1)
269
- style.setdefault("radius", 0.25)
270
-
271
- if len(coo) == 2:
272
- x, y = self._2d_project(*coo)
273
- style.setdefault("zorder", +0.01)
274
- else:
275
- x, y = self._3d_project(*coo)
276
- style.setdefault("zorder", self._coo_to_zorder(*coo) + 0.01)
277
-
278
- return x, y, style
279
-
280
- def _adjust_lims_for_marker(self, x, y, r):
281
- for x, y in [
282
- (x - 1.1 * r, y),
283
- (x + 1.1 * r, y),
284
- (x, y - 1.1 * r),
285
- (x, y + 1.1 * r),
286
- ]:
287
- self._adjust_lims(x, y)
288
-
289
- def circle(self, coo, preset=None, **kwargs):
290
- """Draw a circle at the specified coordinate.
291
-
292
- Parameters
293
- ----------
294
- coo : tuple[int, int] or tuple[int, int, int]
295
- The 2D or 3D coordinate of the circle. If 3D, the coordinate will
296
- be projected onto the 2D plane, and a z-order will be assigned.
297
- preset : str, optional
298
- A preset style to use for the circle.
299
- kwargs
300
- Specific style options passed to ``matplotlib.patches.Circle``.
301
- """
302
- x, y, style = self._parse_style_for_marker(
303
- coo, preset=preset, **kwargs
304
- )
305
- circle = mpl.patches.Circle((x, y), **style)
306
- self.ax.add_artist(circle)
307
- self._adjust_lims_for_marker(x, y, style["radius"])
308
-
309
- def wedge(self, coo, theta1, theta2, preset=None, **kwargs):
310
- """Draw a wedge at the specified coordinate.
311
-
312
- Parameters
313
- ----------
314
- coo : tuple[int, int] or tuple[int, int, int]
315
- The 2D or 3D coordinate of the wedge. If 3D, the coordinate will
316
- be projected onto the 2D plane, and a z-order will be assigned.
317
- theta1 : float
318
- The angle in degrees of the first edge of the wedge.
319
- theta2 : float
320
- The angle in degrees of the second edge of the wedge.
321
- preset : str, optional
322
- A preset style to use for the wedge.
323
- kwargs
324
- Specific style options passed to ``matplotlib.patches.Wedge``.
325
- """
326
- x, y, style = self._parse_style_for_marker(
327
- coo, preset=preset, **kwargs
328
- )
329
-
330
- # wedge uses r, not radius
331
- style["r"] = style.pop("radius")
332
- # and is not filled by default
333
- style.setdefault("fill", True)
334
-
335
- wedge = mpl.patches.Wedge(
336
- (x, y), theta1=theta1, theta2=theta2, **style
337
- )
338
-
339
- self.ax.add_artist(wedge)
340
- self._adjust_lims_for_marker(x, y, style["r"])
341
-
342
- def dot(self, coo, preset=None, **kwargs):
343
- """Draw a small circle with no border. Alias for circle with defaults
344
- `radius=0.1` and `linewidth=0.0`.
345
-
346
- Parameters
347
- ----------
348
- coo : tuple[int, int] or tuple[int, int, int]
349
- The 2D or 3D coordinate of the dot. If 3D, the coordinate will
350
- be projected onto the 2D plane, and a z-order will be assigned.
351
- preset : str, optional
352
- A preset style to use for the dot.
353
- kwargs
354
- Specific style options passed to ``matplotlib.patches.Circle``.
355
- """
356
- style = parse_style_preset(self.presets, preset, **kwargs)
357
- style.setdefault("radius", 0.1)
358
- style.setdefault("linewidth", 0.0)
359
- self.circle(coo, **style)
360
-
361
- def regular_polygon(self, coo, preset=None, **kwargs):
362
- """Draw a regular polygon at the specified coordinate.
363
-
364
- Parameters
365
- ----------
366
- coo : tuple[int, int] or tuple[int, int, int]
367
- The 2D or 3D coordinate of the polygon. If 3D, the coordinate will
368
- be projected onto the 2D plane, and a z-order will be assigned.
369
- n : int
370
- The number of sides of the polygon.
371
- orientation : float, optional
372
- The orientation of the polygon in radians. Default is 0.0.
373
- preset : str, optional
374
- A preset style to use for the polygon.
375
- kwargs
376
- Specific style options passed to ``matplotlib.patches.Polygon``.
377
- """
378
- x, y, style = self._parse_style_for_marker(
379
- coo, preset=preset, **kwargs
380
- )
381
-
382
- n = style.pop("n", 3)
383
- orientation = style.pop("orientation", 0.0)
384
-
385
- rpoly = mpl.patches.RegularPolygon(
386
- (x, y), numVertices=n, orientation=orientation, **style
387
- )
388
- self.ax.add_artist(rpoly)
389
- self._adjust_lims_for_marker(x, y, style["radius"])
390
-
391
- def marker(self, coo, preset=None, **kwargs):
392
- """Draw a 'marker' at the specified coordinate. This is a shorthand for
393
- creating polygons with shape specified by a single character.
394
-
395
- Parameters
396
- ----------
397
- coo : tuple[int, int] or tuple[int, int, int]
398
- The 2D or 3D coordinate of the marker. If 3D, the coordinate will
399
- be projected onto the 2D plane, and a z-order will be assigned.
400
- marker : str, optional
401
- The marker shape to draw. One of ``"o.v^<>sDphH8"``.
402
- preset : str, optional
403
- A preset style to use for the marker.
404
- kwargs
405
- Specific style options.
406
- """
407
- style = parse_style_preset(self.presets, preset, **kwargs)
408
- marker = style.pop("marker", "s")
409
- if marker in ("o", "."):
410
- return self.circle(coo, preset=preset, **style)
411
-
412
- if isinstance(marker, int):
413
- n = marker
414
- orientation = 0.0
415
- else:
416
- n, orientation = {
417
- "v": (3, pi / 3),
418
- "^": (3, 0),
419
- "<": (3, pi / 2),
420
- ">": (3, -pi / 2),
421
- "s": (4, pi / 4),
422
- "D": (4, 0),
423
- "p": (5, 0),
424
- "h": (6, 0),
425
- "H": (6, pi / 2),
426
- "8": (8, 0),
427
- }[marker]
428
-
429
- self.regular_polygon(
430
- coo, preset=preset, n=n, orientation=orientation, **style
431
- )
432
-
433
- def square(self, coo, preset=None, **kwargs):
434
- return self.marker(coo, preset=preset, marker="s", **kwargs)
435
-
436
- def cube(self, coo, preset=None, **kwargs):
437
- """Draw a cube at the specified coordinate, which must be 3D.
438
-
439
- Parameters
440
- ----------
441
- coo : tuple[int, int, int]
442
- The 3D coordinate of the cube. The coordinate will
443
- be projected onto the 2D plane, and a z-order will be assigned.
444
- preset : str, optional
445
- A preset style to use for the cube.
446
- kwargs
447
- Specific style options passed to ``matplotlib.patches.Polygon``.
448
- """
449
- style = parse_style_preset(self.presets, preset, **kwargs)
450
-
451
- r = style.pop("radius", 0.25)
452
- x, y, z = coo
453
- xm, xp = x - r, x + r
454
- ym, yp = y - r, y + r
455
- zm, zp = z - r, z + r
456
-
457
- faces = [
458
- ((xm, ym, zm), (xm, ym, zp), (xm, yp, zp), (xm, yp, zm)),
459
- ((xp, ym, zm), (xp, ym, zp), (xp, yp, zp), (xp, yp, zm)),
460
- ((xp, ym, zm), (xp, ym, zp), (xm, ym, zp), (xm, ym, zm)),
461
- ((xp, yp, zm), (xp, yp, zp), (xm, yp, zp), (xm, yp, zm)),
462
- ((xp, ym, zm), (xp, yp, zm), (xm, yp, zm), (xm, ym, zm)),
463
- ((xp, ym, zp), (xp, yp, zp), (xm, yp, zp), (xm, ym, zp)),
464
- ]
465
- for face in faces:
466
- self.shape(face, preset=preset, **style)
467
-
468
- def line(self, cooa, coob, preset=None, **kwargs):
469
- """Draw a line between two coordinates.
470
-
471
- Parameters
472
- ----------
473
- cooa, coob : tuple[int, int] or tuple[int, int, int]
474
- The 2D or 3D coordinates of the line endpoints. If 3D, the
475
- coordinates will be projected onto the 2D plane, and a z-order
476
- will be assigned based on average z-order of the endpoints.
477
- stretch : float
478
- Stretch the line by this factor. 1.0 is no stretch, 0.5 is half
479
- length, 2.0 is double length. Default is 1.0.
480
- arrowhead : bool or dict, optional
481
- Draw an arrowhead at the end of the line. Default is False. If a
482
- dict, it is passed as keyword arguments to the arrowhead method.
483
- text_between : str, optional
484
- Add text along the line.
485
- preset : str, optional
486
- A preset style to use for the line.
487
- kwargs
488
- Specific style options passed to ``matplotlib.lines.Line2D``.
489
-
490
- See Also
491
- --------
492
- Drawing.arrowhead
493
- """
494
- style = parse_style_preset(self.presets, preset, **kwargs)
495
- style.setdefault("color", self.drawcolor)
496
- style.setdefault("solid_capstyle", "round")
497
- style.setdefault("stretch", 1.0)
498
- style.setdefault("arrowhead", None)
499
- style.setdefault("text", None)
500
- stretch = style.pop("stretch")
501
- arrowhead = style.pop("arrowhead")
502
- text = style.pop("text")
503
-
504
- if len(cooa) == 2:
505
- xs, ys = zip(*[self._2d_project(*coo) for coo in [cooa, coob]])
506
- style.setdefault("zorder", +0.0)
507
- else:
508
- style.setdefault(
509
- "zorder",
510
- mean(self._coo_to_zorder(*coo) for coo in [cooa, coob]),
511
- )
512
- xs, ys = zip(*[self._3d_project(*coo) for coo in [cooa, coob]])
513
-
514
- if stretch != 1.0:
515
- # shorten around center
516
- center = mean(xs), mean(ys)
517
- xs = [center[0] + stretch * (x - center[0]) for x in xs]
518
- ys = [center[1] + stretch * (y - center[1]) for y in ys]
519
-
520
- if arrowhead is not None:
521
- if arrowhead is True:
522
- arrowhead = {}
523
- else:
524
- arrowhead = dict(arrowhead)
525
- self.arrowhead(cooa, coob, preset=preset, **(style | arrowhead))
526
-
527
- line = mpl.lines.Line2D(xs, ys, **style)
528
- self.ax.add_artist(line)
529
-
530
- if text:
531
- if isinstance(text, str):
532
- text = {"text": text}
533
- else:
534
- text = dict(text)
535
-
536
- # don't want to pass full style dict to text_between
537
- text.setdefault("zorder", style["zorder"])
538
- self.text_between(cooa, coob, **text)
539
-
540
- for x, y in zip(xs, ys):
541
- self._adjust_lims(x, y)
542
-
543
- def line_offset(
544
- self,
545
- cooa,
546
- coob,
547
- offset,
548
- midlength=0.5,
549
- relative=True,
550
- preset=None,
551
- **kwargs,
552
- ):
553
- """Draw a line between two coordinates, but curving out by a given
554
- offset perpendicular to the line.
555
-
556
- Parameters
557
- ----------
558
- cooa, coob : tuple[int, int] or tuple[int, int, int]
559
- The 2D or 3D coordinates of the line endpoints. If 3D, the
560
- coordinates will be projected onto the 2D plane, and a z-order
561
- will be assigned based on average z-order of the endpoints.
562
- offset : float
563
- The offset of the curve from the line, as a fraction of the total
564
- line length. This is always processed in the 2D projected plane.
565
- midlength : float
566
- The length of the middle straight section, as a fraction of the
567
- total line length. Default is 0.5.
568
- arrowhead : bool or dict, optional
569
- Draw an arrowhead at the end of the line. Default is False. If a
570
- dict, it is passed as keyword arguments to the arrowhead method.
571
- text_between : str, optional
572
- Add text along the line.
573
- relative : bool, optional
574
- If ``True`` (the default), then ``offset`` is taken as a fraction
575
- of the line length, else in absolute units.
576
- preset : str, optional
577
- A preset style to use for the line.
578
- kwargs
579
- Specific style options passed to ``curve``.
580
- """
581
- style = parse_style_preset(self.presets, preset, **kwargs)
582
- style.setdefault("arrowhead", None)
583
- style.setdefault("text", None)
584
- arrowhead = style.pop("arrowhead")
585
- text = style.pop("text")
586
-
587
- if len(cooa) == 2:
588
- xs, ys = zip(*[self._2d_project(*coo) for coo in [cooa, coob]])
589
- style.setdefault("zorder", +0.0)
590
- else:
591
- style.setdefault(
592
- "zorder",
593
- mean(self._coo_to_zorder(*coo) for coo in [cooa, coob]),
594
- )
595
- xs, ys = zip(*[self._3d_project(*coo) for coo in [cooa, coob]])
596
-
597
- cooa = xs[0], ys[0]
598
- coob = xs[1], ys[1]
599
- forward, inverse = get_rotator_and_inverse(cooa, coob)
600
- R = forward(*coob)[0]
601
-
602
- if relative:
603
- offset *= R
604
-
605
- endlength = (1 - midlength) / 2
606
- cooml = inverse(endlength * R, offset)
607
- coomm = inverse(R / 2, offset)
608
- coomr = inverse((1 - endlength) * R, offset)
609
- curve_pts = [cooa, cooml, coomm, coomr, coob]
610
-
611
- if arrowhead is not None:
612
- if arrowhead is True:
613
- arrowhead = {}
614
- else:
615
- arrowhead = dict(arrowhead)
616
-
617
- # want to correct center for midlength
618
- center = arrowhead.pop("center", 0.5)
619
- arrowhead["center"] = min(
620
- max(0.0, 0.5 + (center - 0.5) / midlength), 1.0
621
- )
622
- self.arrowhead(cooml, coomr, preset=preset, **(style | arrowhead))
623
-
624
- self.curve(curve_pts, preset=preset, **style)
625
-
626
- if text:
627
- if isinstance(text, str):
628
- text = {"text": text}
629
- else:
630
- text = dict(text)
631
- # don't want to pass full style dict to text_between
632
- text.setdefault("zorder", style["zorder"])
633
- self.text_between(cooml, coomr, **text)
634
-
635
- for coo in curve_pts:
636
- self._adjust_lims(*coo)
637
-
638
- def arrowhead(self, cooa, coob, preset=None, **kwargs):
639
- """Draw just a arrowhead on the line between ``cooa`` and ``coob``.
640
-
641
- Parameters
642
- ----------
643
- cooa, coob : tuple[int, int] or tuple[int, int, int]
644
- The coordinates of the start and end of the line. If 3D, the
645
- coordinates will be projected onto the 2D plane, and a z-order
646
- will be assigned based on average z-order of the endpoints.
647
- reverse : bool or "both", optional
648
- Reverse the direction by switching ``cooa`` and ``coob``. If
649
- ``"both"``, draw an arrowhead in both directions. Default is
650
- False.
651
- center : float, optional
652
- The position of the arrowhead along the line, where 0 is the start
653
- and 1 is the end. Default is 0.5.
654
- width : float, optional
655
- The width of the arrowhead. Default is 0.05.
656
- length : float, optional
657
- The length of the arrowhead. Default is 0.1.
658
- preset : str, optional
659
- A preset style to use for the arrowhead, including the above
660
- options.
661
- kwargs
662
- Specific style options passed to ``matplotlib.lines.Line2D``.
663
- """
664
- style = parse_style_preset(self.presets, preset, **kwargs)
665
- style.setdefault("color", self.drawcolor)
666
- style.setdefault("center", 0.5)
667
- style.setdefault("width", 0.05)
668
- style.setdefault("length", 0.1)
669
- style.setdefault("reverse", False)
670
-
671
- reverse = style.pop("reverse")
672
- if reverse == "both":
673
- self.arrowhead(cooa, coob, preset=preset, **style)
674
- if reverse:
675
- cooa, coob = coob, cooa
676
-
677
- if len(cooa) != 2:
678
- style.setdefault(
679
- "zorder",
680
- mean(self._coo_to_zorder(*coo) for coo in [cooa, coob]),
681
- )
682
- cooa = self._3d_project(*cooa)
683
- coob = self._3d_project(*coob)
684
- else:
685
- cooa = self._2d_project(*cooa)
686
- coob = self._2d_project(*coob)
687
-
688
- forward, inverse = get_rotator_and_inverse(cooa, coob)
689
- rb = forward(*coob)
690
-
691
- center = style.pop("center")
692
- width = style.pop("width")
693
- length = style.pop("length")
694
- lab = rb[0]
695
- xc = center * lab
696
- arrow_x = xc - length * lab
697
- arrow_y = width * lab
698
-
699
- aa, ab, ac = [
700
- inverse(arrow_x, arrow_y),
701
- inverse(xc, 0),
702
- inverse(arrow_x, -arrow_y),
703
- ]
704
-
705
- line = mpl.lines.Line2D(*zip(*(aa, ab, ac)), **style)
706
- self.ax.add_artist(line)
707
- for x, y in [aa, ab, ac]:
708
- self._adjust_lims(x, y)
709
-
710
- def curve(self, coos, preset=None, **kwargs):
711
- """Draw a smooth line through the given coordinates.
712
-
713
- Parameters
714
- ----------
715
- coos : Sequence[tuple[int, int]] or Sequence[tuple[int, int, int]]
716
- The 2D or 3D coordinates of the line. If 3D, the coordinates will
717
- be projected onto the 2D plane, and a z-order will be assigned
718
- based on average z-order of the endpoints.
719
- smoothing : float, optional
720
- The amount of smoothing to apply to the curve. 0.0 is no smoothing,
721
- 1.0 is maximum smoothing. Default is 0.5.
722
- preset : str, optional
723
- A preset style to use for the curve.
724
- kwargs
725
- Specific style options passed to ``matplotlib.lines.Line2D``.
726
- """
727
- from matplotlib.path import Path
728
-
729
- style = parse_style_preset(self.presets, preset, **kwargs)
730
- if "color" in style:
731
- # presume that edge color is being specified
732
- style.setdefault("edgecolor", style.pop("color"))
733
- style.setdefault("edgecolor", self.drawcolor)
734
- style.setdefault("fill", False)
735
- style.setdefault("capstyle", "round")
736
- style.setdefault("smoothing", 1 / 2)
737
- smoothing = style.pop("smoothing")
738
-
739
- if len(coos[0]) != 2:
740
- style.setdefault(
741
- "zorder", mean(self._coo_to_zorder(*coo) for coo in coos)
742
- )
743
- coos = [self._3d_project(*coo) for coo in coos]
744
- else:
745
- coos = [self._2d_project(*coo) for coo in coos]
746
-
747
- N = len(coos)
748
-
749
- if N <= 2 or smoothing == 0.0:
750
- path = coos
751
- moves = [Path.MOVETO] + [Path.LINETO] * (N - 1)
752
- control_pts = {}
753
- else:
754
- control_pts = {}
755
- for i in range(1, N - 1):
756
- control_pts[i, "l"], control_pts[i, "r"] = get_control_points(
757
- coos[i - 1],
758
- coos[i],
759
- coos[i + 1],
760
- spacing=smoothing / 2,
761
- )
762
-
763
- path = [coos[0], control_pts[1, "l"], coos[1]]
764
- moves = [Path.MOVETO, Path.CURVE3, Path.CURVE3]
765
-
766
- for i in range(1, N - 2):
767
- path.extend(
768
- (control_pts[i, "r"], control_pts[i + 1, "l"], coos[i + 1])
769
- )
770
- moves.extend((Path.CURVE4, Path.CURVE4, Path.CURVE4))
771
-
772
- path.extend((control_pts[N - 2, "r"], coos[N - 1]))
773
- moves.extend((Path.CURVE3, Path.CURVE3))
774
-
775
- curve = mpl.patches.PathPatch(Path(path, moves), **style)
776
- self.ax.add_patch(curve)
777
-
778
- for coo in coos:
779
- self._adjust_lims(*coo)
780
- for coo in control_pts.values():
781
- self._adjust_lims(*coo)
782
-
783
- def shape(self, coos, preset=None, **kwargs):
784
- """Draw a closed shape with (sharp) corners at the given coordinates.
785
-
786
- Parameters
787
- ----------
788
- coos : sequence of coordinates
789
- The coordinates of the corners' of the shape.
790
- preset : str, optional
791
- A preset style to use for the shape.
792
- kwargs
793
- Specific style options passed to ``matplotlib.patches.PathPatch``.
794
-
795
- See Also
796
- --------
797
- Drawing.patch
798
- """
799
- from matplotlib.path import Path
800
-
801
- style = parse_style_preset(self.presets, preset, **kwargs)
802
- if "color" in style:
803
- style.setdefault("facecolor", style.pop("color"))
804
- style.setdefault("facecolor", self.shapecolor)
805
- style.setdefault("edgecolor", darken_color(style["facecolor"]))
806
- style.setdefault("linewidth", 1)
807
- style.setdefault("joinstyle", "round")
808
-
809
- if len(coos[0]) != 2:
810
- style.setdefault(
811
- "zorder", mean(self._coo_to_zorder(*coo) for coo in coos)
812
- )
813
- coos = [self._3d_project(*coo) for coo in coos]
814
- else:
815
- coos = [self._2d_project(*coo) for coo in coos]
816
-
817
- path = [coos[0]]
818
- moves = [Path.MOVETO]
819
- for coo in coos[1:]:
820
- path.append(coo)
821
- moves.append(Path.LINETO)
822
- path.append(coos[0])
823
- moves.append(Path.CLOSEPOLY)
824
-
825
- curve = mpl.patches.PathPatch(Path(path, moves), **style)
826
- self.ax.add_patch(curve)
827
-
828
- for coo in coos:
829
- self._adjust_lims(*coo)
830
-
831
- def rectangle(self, cooa, coob, preset=None, **kwargs):
832
- style = parse_style_preset(self.presets, preset, **kwargs)
833
- radius = style.pop("radius", 0.25)
834
-
835
- forward, inverse = get_rotator_and_inverse(cooa, coob)
836
-
837
- # rotate both onto y=0
838
- xa, _ = forward(*cooa)
839
- xb, _ = forward(*coob)
840
- points = [
841
- (xa - radius, -radius),
842
- (xa - radius, +radius),
843
- (xb + radius, +radius),
844
- (xb + radius, -radius),
845
- ]
846
- points = [inverse(*coo) for coo in points]
847
- self.shape(points, **style)
848
-
849
- def patch(self, coos, preset=None, **kwargs):
850
- """Draw a closed smooth patch through given coordinates.
851
-
852
- Parameters
853
- ----------
854
- coos : sequence of coordinates
855
- The coordinates of the 'corners' of the patch, the outline is
856
- guaranteed to pass through these points.
857
- smoothing : float
858
- The smoothing factor, the higher the smoother. The default is
859
- 0.5.
860
- preset : str, optional
861
- A preset style to use for the patch.
862
- kwargs
863
- Specific style options passed to ``matplotlib.patches.PathPatch``.
864
-
865
- See Also
866
- --------
867
- Drawing.shape, Drawing.curve
868
- """
869
- from matplotlib.path import Path
870
-
871
- style = parse_style_preset(self.presets, preset, **kwargs)
872
- style.setdefault("linestyle", ":")
873
- style.setdefault("edgecolor", (0.5, 0.5, 0.5, 0.75))
874
- style.setdefault("facecolor", (0.5, 0.5, 0.5, 0.25))
875
- style.setdefault("smoothing", 1 / 2)
876
- smoothing = style.pop("smoothing")
877
-
878
- if len(coos[0]) != 2:
879
- # use min so the patch appears *just* behind the elements
880
- # its meant to highlight
881
- style.setdefault(
882
- "zorder", min(self._coo_to_zorder(*coo) for coo in coos) - 0.01
883
- )
884
- coos = [self._3d_project(*coo) for coo in coos]
885
- else:
886
- style.setdefault("zorder", -0.01)
887
- coos = [self._2d_project(*coo) for coo in coos]
888
-
889
- N = len(coos)
890
-
891
- control_pts = {}
892
- for i in range(N):
893
- control_pts[i, "l"], control_pts[i, "r"] = get_control_points(
894
- coos[(i - 1) % N],
895
- coos[i],
896
- coos[(i + 1) % N],
897
- spacing=smoothing / 2,
898
- )
899
-
900
- path = [coos[0]]
901
- moves = [Path.MOVETO]
902
- for ia in range(N):
903
- ib = (ia + 1) % N
904
- path.append(control_pts[ia, "r"])
905
- path.append(control_pts[ib, "l"])
906
- path.append(coos[ib])
907
- moves.append(Path.CURVE4)
908
- moves.append(Path.CURVE4)
909
- moves.append(Path.CURVE4)
910
-
911
- curve = mpl.patches.PathPatch(Path(path, moves), **style)
912
- self.ax.add_patch(curve)
913
-
914
- for coo in coos:
915
- self._adjust_lims(*coo)
916
- for coo in control_pts.values():
917
- self._adjust_lims(*coo)
918
-
919
- def patch_around(self, coos, *, preset=None, **kwargs):
920
- """Draw a patch around the given coordinates, by contructing a convex
921
- hull around the points, optionally including an extra uniform or per
922
- coordinate radius.
923
-
924
- Parameters
925
- ----------
926
- coos : sequence[tuple[int, int]] or sequence[tuple[int, int, int]]
927
- The coordinates of the points to draw the patch around. If 3D, the
928
- coordinates will be projected onto the 2D plane, and a z-order will
929
- be assigned based on *min* z-order of the endpoints.
930
- radius : float or sequence[float], optional
931
- The radius of the patch around each point. If a sequence, must be
932
- the same length as ``coos``. Default is 0.0.
933
- resolution : int, optional
934
- The number of points to use pad around each point. Default is 12.
935
- preset : str, optional
936
- A preset style to use for the patch.
937
- kwargs
938
- Specific style options passed to ``matplotlib.patches.PathPatch``.
939
- """
940
- import numpy as np
941
- from scipy.spatial import ConvexHull
942
-
943
- style = parse_style_preset(self.presets, preset, **kwargs)
944
- radius = style.pop("radius", 0.0)
945
- resolution = style.pop("resolution", 12)
946
-
947
- if isinstance(radius, (int, float)):
948
- radius = [radius] * len(coos)
949
-
950
- if len(coos[0]) != 2:
951
- # use min so the patch appears *just* behind the elements
952
- # its meant to highlight
953
- style.setdefault(
954
- "zorder", min(self._coo_to_zorder(*coo) for coo in coos) - 0.01
955
- )
956
- coos = [self._3d_project(*coo) for coo in coos]
957
- else:
958
- style.setdefault("zorder", -0.01)
959
-
960
- expanded_pts = []
961
- for coo, r in zip(coos, radius):
962
- if r == 0:
963
- expanded_pts.append(coo)
964
- else:
965
- expanded_pts.extend(gen_points_around(coo, r, resolution))
966
-
967
- if len(expanded_pts) <= 3:
968
- # need at least 3 points to make convex hull
969
- boundary_pts = expanded_pts
970
- else:
971
- expanded_pts = np.array(expanded_pts)
972
- hull = ConvexHull(expanded_pts)
973
- boundary_pts = expanded_pts[hull.vertices]
974
-
975
- self.patch(boundary_pts, **style)
976
-
977
- def patch_around_circles(
978
- self,
979
- cooa,
980
- ra,
981
- coob,
982
- rb,
983
- padding=0.2,
984
- pinch=True,
985
- preset=None,
986
- **kwargs,
987
- ):
988
- """Draw a smooth patch around two circles.
989
-
990
- Parameters
991
- ----------
992
- cooa : tuple[int, int] or tuple[int, int, int]
993
- The coordinates of the center of the first circle. If 3D, the
994
- coordinates will be projected onto the 2D plane, and a z-order
995
- will be assigned based on average z-order of the endpoints.
996
- ra : int
997
- The radius of the first circle.
998
- coob : tuple[int, int] or tuple[int, int, int]
999
- The coordinates of the center of the second circle. If 3D, the
1000
- coordinates will be projected onto the 2D plane, and a z-order
1001
- will be assigned based on average z-order of the endpoints.
1002
- rb : int
1003
- The radius of the second circle.
1004
- padding : float, optional
1005
- The amount of padding to add around the circles. Default is 0.2.
1006
- pinch : bool or float, optional
1007
- If or how much to pinch the patch in between the circles.
1008
- Default is to match the padding.
1009
- preset : str, optional
1010
- A preset style to use for the patch.
1011
- kwargs
1012
- Specific style options passed to ``matplotlib.patches.PathPatch``.
1013
-
1014
- See Also
1015
- --------
1016
- Drawing.patch
1017
- """
1018
- style = parse_style_preset(self.presets, preset, **kwargs)
1019
- style.setdefault("smoothing", 1.0)
1020
-
1021
- if pinch is True:
1022
- pinch = 1 - padding
1023
-
1024
- if len(cooa) != 2:
1025
- style.setdefault(
1026
- "zorder",
1027
- min(self._coo_to_zorder(*coo) for coo in [cooa, coob]) - 0.01,
1028
- )
1029
- cooa = self._3d_project(*cooa)
1030
- coob = self._3d_project(*coob)
1031
- else:
1032
- style.setdefault("zorder", -0.01)
1033
- cooa = self._2d_project(*cooa)
1034
- coob = self._2d_project(*coob)
1035
-
1036
- forward, inverse = get_rotator_and_inverse(cooa, coob)
1037
- xb = forward(*coob)[0]
1038
- rl = (1 + padding) * ra
1039
- rr = (1 + padding) * rb
1040
- rcoos = [
1041
- # left loop
1042
- (0, -rl),
1043
- (-rl * 2**-0.5, -rl * 2**-0.5),
1044
- (-rl, 0),
1045
- (-rl * 2**-0.5, +rl * 2**-0.5),
1046
- (0, +rl),
1047
- # above pinch point
1048
- (xb / 2, (1 - float(pinch)) * (ra + rb)),
1049
- # right loop
1050
- (xb, +rr),
1051
- (xb + rr * 2**-0.5, +rr * 2**-0.5),
1052
- (xb + rr, 0),
1053
- (xb + rr * 2**-0.5, -rr * 2**-0.5),
1054
- (xb, -rr),
1055
- # below pinch point
1056
- (xb / 2, (float(pinch) - 1) * (ra + rb)),
1057
- ]
1058
-
1059
- pcoos = [inverse(*rcoo) for rcoo in rcoos]
1060
- self.patch(pcoos, preset=preset, **style)
1061
-
1062
- def savefig(self, fname, dpi=300, bbox_inches="tight"):
1063
- self.fig.savefig(fname, dpi=dpi, bbox_inches=bbox_inches)
1064
-
1065
-
1066
- def parse_style_preset(presets, preset, **kwargs):
1067
- """Parse a one or more style presets plus manual kwargs.
1068
-
1069
- Parameters
1070
- ----------
1071
- presets : dict
1072
- The dictionary of presets.
1073
- preset : str or sequence of str
1074
- The name of the preset(s) to use. If multiple, later presets take
1075
- precedence.
1076
- kwargs
1077
- Any additional manual keyword arguments are added to the style and
1078
- override the presets.
1079
-
1080
- Returns
1081
- -------
1082
- style : dict
1083
- """
1084
- if (preset is None) or isinstance(preset, str):
1085
- preset = (preset,)
1086
-
1087
- style = {}
1088
- for p in preset:
1089
- if p not in presets:
1090
- warnings.warn(f"Drawing has no preset '{p}' yet.")
1091
- else:
1092
- style.update(presets[p])
1093
- style.update(kwargs)
1094
- return style
1095
-
1096
-
1097
- def simple_scale(i, j, xscale=1, yscale=1):
1098
- return i * xscale, j * yscale
1099
-
1100
-
1101
- def axonometric_project(
1102
- i,
1103
- j,
1104
- k,
1105
- a=50,
1106
- b=12,
1107
- xscale=1,
1108
- yscale=1,
1109
- zscale=1,
1110
- ):
1111
- """Project the 3D location ``(i, j, k)`` onto the 2D plane, using
1112
- the axonometric projection with the given angles ``a`` and ``b``.
1113
-
1114
- The ``xscale``, ``yscale`` and ``zscale`` parameters can be used to
1115
- scale the axes, including flipping them.
1116
-
1117
- Parameters
1118
- ----------
1119
- i, j, k : float
1120
- The 3D coordinates of the point to project.
1121
- a, b : float
1122
- The left and right angles to displace x and y axes, from horizontal,
1123
- in degrees.
1124
- xscale, yscale, zscale : float
1125
- The scaling factor for the x, y and z axes. If negative, the axis
1126
- is flipped.
1127
-
1128
- Returns
1129
- -------
1130
- x, y : float
1131
- The 2D coordinates of the projected point.
1132
- """
1133
- i *= xscale * 0.8
1134
- j *= yscale
1135
- k *= zscale
1136
- return (
1137
- +i * cos(pi * a / 180) + j * cos(pi * b / 180),
1138
- -i * sin(pi * a / 180) + j * sin(pi * b / 180) + k,
1139
- )
1140
-
1141
-
1142
- def coo_to_zorder(i, j, k, xscale=1, yscale=1, zscale=1):
1143
- """Given the coordinates of a point in 3D space, return a z-order value
1144
- that can be used to draw it on top of other elements in the diagram.
1145
- Take into account the scaling of the axes, so that the z-ordering
1146
- is correct even if the axes flipped.
1147
- """
1148
- return (
1149
- +i * xscale / abs(xscale)
1150
- - j * yscale / abs(yscale)
1151
- + k * zscale / abs(zscale)
1152
- )
1153
-
1154
-
1155
- # colorblind palettes by Okabe & Ito (https://jfly.uni-koeln.de/color/)
1156
-
1157
- _COLORS_DEFAULT = {
1158
- "blue": "#56B4E9", # light blue
1159
- "orange": "#E69F00", # orange
1160
- "green": "#009E73", # green
1161
- "red": "#D55E00", # red
1162
- "yellow": "#F0E442", # yellow
1163
- "pink": "#CC79A7", # pink
1164
- "bluedark": "#0072B2", # dark blue
1165
- }
1166
-
1167
-
1168
- def get_wong_color(
1169
- which,
1170
- alpha=None,
1171
- hue_factor=0.0,
1172
- sat_factor=1.0,
1173
- val_factor=1.0,
1174
- ):
1175
- """Get a color by name, optionally modifying its alpha, hue, saturation
1176
- or value.
1177
-
1178
- These colorblind friendly colors were ppularized in an article by Wong
1179
- (https://www.nature.com/articles/nmeth.1618) but originally come from
1180
- Okabe & Ito (https://jfly.uni-koeln.de/color/).
1181
-
1182
- Parameters
1183
- ----------
1184
- which : {'blue', 'orange', 'green', 'red', 'yellow', 'pink', 'bluedark'}
1185
- The name of the color to get.
1186
- alpha : float, optional
1187
- The alpha channel value to set for the color. Default is 1.0.
1188
- hue_factor : float, optional
1189
- The amount to shift the hue of the color. Default is 0.0.
1190
- sat_factor : float, optional
1191
- The amount to scale the saturation of the color. Default is 1.0.
1192
- val_factor : float, optional
1193
- The amount to scale the value of the color. Default is 1.0.
1194
-
1195
- Returns
1196
- -------
1197
- color : tuple[float, float, float, float]
1198
- The RGBA color as a tuple of floats.
1199
- """
1200
- import matplotlib as mpl
1201
-
1202
- h = _COLORS_DEFAULT[which]
1203
- rgb = mpl.colors.to_rgb(h)
1204
- h, s, v = mpl.colors.rgb_to_hsv(rgb)
1205
- h = (h + hue_factor) % 1.0
1206
- s = min(max(0.0, s * sat_factor), 1.0)
1207
- v = min(max(0.0, v * val_factor), 1.0)
1208
- r, g, b = mpl.colors.hsv_to_rgb((h, s, v))
1209
- if alpha is not None:
1210
- return (r, g, b, alpha)
1211
- return r, g, b
1212
-
1213
-
1214
- _COLORS_SORTED = [
1215
- _COLORS_DEFAULT["bluedark"],
1216
- _COLORS_DEFAULT["blue"],
1217
- _COLORS_DEFAULT["green"],
1218
- _COLORS_DEFAULT["yellow"],
1219
- _COLORS_DEFAULT["orange"],
1220
- _COLORS_DEFAULT["red"],
1221
- _COLORS_DEFAULT["pink"],
1222
- ]
1223
-
1224
-
1225
- def mod_sat(c, mod=None, alpha=None):
1226
- """Modify the luminosity of color ``c``, optionally set the ``alpha``
1227
- channel, and return the final color as a RGBA tuple."""
1228
- import matplotlib as mpl
1229
-
1230
- r, g, b, a = mpl.colors.to_rgba(c)
1231
- if alpha is None:
1232
- alpha = a
1233
-
1234
- if mod is None:
1235
- return r, g, b, alpha
1236
-
1237
- h, s, v = mpl.colors.rgb_to_hsv((r, g, b))
1238
- return (*mpl.colors.hsv_to_rgb((h, mod * s, v)), alpha)
1239
-
1240
-
1241
- def auto_colors(nc, alpha=None, default_sequence=False):
1242
- """Generate a nice sequence of ``nc`` colors. By default this uses an
1243
- interpolation between the colorblind friendly colors of Okabe & Ito in hue
1244
- sorted order, with luminosity moderated by a sine function to increase
1245
- local distinguishability.
1246
-
1247
- Parameters
1248
- ----------
1249
- nc : int
1250
- The number of colors to generate.
1251
- alpha : float, optional
1252
- The alpha channel value to set for all colors. Default is 1.0.
1253
- default_sequence : bool, optional
1254
- If ``True``, take from the default sequence of 7 colors, un-sorted and
1255
- un-modulated.
1256
-
1257
- Returns
1258
- -------
1259
- colors : list[tuple[float, float, float, float]]
1260
- """
1261
- import math
1262
-
1263
- import numpy as np
1264
- from matplotlib.colors import LinearSegmentedColormap
1265
-
1266
- if default_sequence:
1267
- if nc > 7:
1268
- raise ValueError(
1269
- "Can only generate 7 colors with default sequence"
1270
- )
1271
- return [
1272
- mod_sat(c, alpha=alpha)
1273
- for c in tuple(_COLORS_DEFAULT.values())[:nc]
1274
- ]
1275
-
1276
- cmap = LinearSegmentedColormap.from_list("colorblind", _COLORS_SORTED)
1277
-
1278
- xs = list(map(cmap, np.linspace(0, 1.0, nc)))
1279
-
1280
- # modulate color saturation with sine to generate local distinguishability
1281
- # ... but only turn on gradually for increasing number of nodes
1282
- sat_mod_period = min(4, nc / 7)
1283
- sat_mod_factor = max(0.0, 2 / 3 * math.tanh((nc - 7) / 4))
1284
-
1285
- if alpha is None:
1286
- alpha = 1.0
1287
-
1288
- return [
1289
- mod_sat(
1290
- c,
1291
- 1 - sat_mod_factor * math.sin(math.pi * i / sat_mod_period) ** 2,
1292
- alpha,
1293
- )
1294
- for i, c in enumerate(xs)
1295
- ]
1296
-
1297
-
1298
- def darken_color(color, factor=2 / 3):
1299
- """Take ``color`` and darken it by ``factor``."""
1300
- rgba = mpl.colors.to_rgba(color)
1301
- return tuple(factor * c for c in rgba[:3]) + rgba[3:]
1302
-
1303
-
1304
- def average_color(colors):
1305
- """Take a sequence of colors and return the RMS average in RGB space."""
1306
- from matplotlib.colors import to_rgba
1307
-
1308
- # first map to rgba
1309
- colors = [to_rgba(c) for c in colors]
1310
-
1311
- r, g, b, a = zip(*colors)
1312
-
1313
- # then RMS average each channel
1314
- rm = (sum(ri**2 for ri in r) / len(r)) ** 0.5
1315
- gm = (sum(gi**2 for gi in g) / len(g)) ** 0.5
1316
- bm = (sum(bi**2 for bi in b) / len(b)) ** 0.5
1317
- am = sum(a) / len(a)
1318
-
1319
- return (rm, gm, bm, am)
1320
-
1321
-
1322
- def jitter_color(color, factor=0.05):
1323
- """Take ``color`` and add a random offset to each of its components."""
1324
- import random
1325
-
1326
- rgba = mpl.colors.to_rgba(color)
1327
- hsv = mpl.colors.rgb_to_hsv(rgba[:3])
1328
- hsv = (
1329
- hsv[0],
1330
- min(max(0, hsv[1] + random.uniform(-factor / 2, factor / 2)), 1),
1331
- min(max(0, hsv[2] + random.uniform(-factor / 2, factor / 2)), 1),
1332
- )
1333
- rgb = mpl.colors.hsv_to_rgb(hsv)
1334
- return tuple(rgb) + rgba[3:]
1335
-
1336
-
1337
-
1338
- COLORING_SEED = 8 # 8, 10
1339
-
1340
-
1341
- def set_coloring_seed(seed):
1342
- """Set the seed for the random color generator.
1343
-
1344
- Parameters
1345
- ----------
1346
- seed : int
1347
- The seed to use.
1348
- """
1349
- global COLORING_SEED
1350
- COLORING_SEED = seed
1351
-
1352
-
1353
- def hash_to_nvalues(s, nval, seed=None):
1354
- """Hash the string ``s`` to ``nval`` different floats in the range [0, 1]."""
1355
- import hashlib
1356
-
1357
- if seed is None:
1358
- seed = COLORING_SEED
1359
-
1360
- m = hashlib.sha256()
1361
- m.update(f"{seed}".encode())
1362
- m.update(s.encode())
1363
- hsh = m.hexdigest()
1364
-
1365
- b = len(hsh) // nval
1366
- if b == 0:
1367
- raise ValueError(
1368
- f"Can't extract {nval} values from hash of length {len(hsh)}"
1369
- )
1370
- return tuple(
1371
- int(hsh[i * b : (i + 1) * b], 16) / 16**b for i in range(nval)
1372
- )
1373
-
1374
-
1375
- def hash_to_color(
1376
- s,
1377
- hmin=0.0,
1378
- hmax=1.0,
1379
- smin=0.3,
1380
- smax=0.8,
1381
- vmin=0.8,
1382
- vmax=0.9,
1383
- ):
1384
- """Generate a random color for a string ``s``.
1385
-
1386
- Parameters
1387
- ----------
1388
- s : str
1389
- The string to generate a color for.
1390
- hmin : float, optional
1391
- The minimum hue value.
1392
- hmax : float, optional
1393
- The maximum hue value.
1394
- smin : float, optional
1395
- The minimum saturation value.
1396
- smax : float, optional
1397
- The maximum saturation value.
1398
- vmin : float, optional
1399
- The minimum value value.
1400
- vmax : float, optional
1401
- The maximum value value.
1402
-
1403
- Returns
1404
- -------
1405
- color : tuple
1406
- A tuple of floats in the range [0, 1] representing the RGB color.
1407
- """
1408
- from matplotlib.colors import to_hex, hsv_to_rgb
1409
-
1410
- h, s, v = hash_to_nvalues(s, 3)
1411
- h = hmin + h * (hmax - hmin)
1412
- s = smin + s * (smax - smin)
1413
- v = vmin + v * (vmax - vmin)
1414
-
1415
- rgb = hsv_to_rgb((h, s, v))
1416
- return to_hex(rgb)
1417
-
1418
-
1419
- def mean(xs):
1420
- """Get the mean of a list of numbers."""
1421
- s = 0
1422
- i = 0
1423
- for x in xs:
1424
- s += x
1425
- i += 1
1426
- return s / i
1427
-
1428
-
1429
- def distance(pa, pb):
1430
- """Get the distance between two points, in arbtirary dimensions."""
1431
- d = 0.0
1432
- for a, b in zip(pa, pb):
1433
- d += (a - b) ** 2
1434
- return d**0.5
1435
-
1436
-
1437
- def get_angle(pa, pb):
1438
- """Get the angle between the line from p1 to p2 and the x-axis."""
1439
- (xa, ya), (xb, yb) = pa, pb
1440
- return atan2(yb - ya, xb - xa)
1441
-
1442
-
1443
- def get_rotator_and_inverse(pa, pb):
1444
- """Get a rotation matrix that rotates points by theta radians about
1445
- the origin and then translates them by offset.
1446
- """
1447
- theta = get_angle(pa, pb)
1448
- dx, dy = pa
1449
-
1450
- def forward(x, y):
1451
- """Rotate and translate a point."""
1452
- x, y = x - dx, y - dy
1453
- x, y = (
1454
- x * cos(-theta) - y * sin(-theta),
1455
- x * sin(-theta) + y * cos(-theta),
1456
- )
1457
- return x, y
1458
-
1459
- def inverse(x, y):
1460
- """Rotate and translate a point."""
1461
- x, y = x * cos(theta) - y * sin(theta), x * sin(theta) + y * cos(theta)
1462
- return x + dx, y + dy
1463
-
1464
- return forward, inverse
1465
-
1466
-
1467
- def get_control_points(pa, pb, pc, spacing=1 / 3):
1468
- """Get two points that can be used to construct a bezier curve that
1469
- passes smoothly through the angle `pa`, `pb`, `pc`.
1470
- """
1471
- # rotate onto x-axis (ra always (0, 0))
1472
- forward, inverse = get_rotator_and_inverse(pa, pb)
1473
- _, rb, rc = [forward(*p) for p in [pa, pb, pc]]
1474
-
1475
- # flip so third point is always above axis
1476
- flip_y = rc[1] < 0
1477
- if flip_y:
1478
- rc = rc[0], -rc[1]
1479
-
1480
- phi = get_angle(rb, rc) / 2
1481
-
1482
- # lengths of the two lines
1483
- lab = rb[0]
1484
- lbc = distance(rb, rc)
1485
-
1486
- # lengths of perpendicular offsets
1487
- oab = lab * cos(phi)
1488
- obc = lbc * cos(phi)
1489
-
1490
- dx_ab = spacing * oab * cos(phi)
1491
- dy_ab = spacing * oab * sin(phi)
1492
-
1493
- dx_bc = spacing * obc * cos(phi)
1494
- dy_bc = spacing * obc * sin(phi)
1495
-
1496
- # get control points in this reference frame
1497
- rc_ab = rb[0] - dx_ab, rb[1] - dy_ab
1498
- rc_bc = rb[0] + dx_bc, rb[1] + dy_bc
1499
-
1500
- # unflip and un rotate
1501
- if flip_y:
1502
- rc_ab = rc_ab[0], -rc_ab[1]
1503
- rc_bc = rc_bc[0], -rc_bc[1]
1504
-
1505
- c_ab, c_bc = inverse(*rc_ab), inverse(*rc_bc)
1506
-
1507
- return c_ab, c_bc
1508
-
1509
-
1510
- def gen_points_around(coo, radius=1, resolution=12):
1511
- """Generate points around a circle."""
1512
- x, y = coo
1513
- dphi = 2 * pi / resolution
1514
- phis = (i * dphi for i in range(resolution))
1515
- for phi in phis:
1516
- xa = x + radius * cos(phi)
1517
- ya = y + radius * sin(phi)
1518
- yield xa, ya