bluemesh2d 0.1.1.dev0__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 (74) hide show
  1. bluemesh2d/_version.py +24 -0
  2. bluemesh2d/aabb_tree/findball.py +121 -0
  3. bluemesh2d/aabb_tree/findtria.py +299 -0
  4. bluemesh2d/aabb_tree/maketree.py +147 -0
  5. bluemesh2d/aabb_tree/mapvert.py +72 -0
  6. bluemesh2d/aabb_tree/queryset.py +83 -0
  7. bluemesh2d/aabb_tree/scantree.py +124 -0
  8. bluemesh2d/dependencies.py +83 -0
  9. bluemesh2d/feedback.py +142 -0
  10. bluemesh2d/geom_util/boundary_util.py +216 -0
  11. bluemesh2d/geom_util/getiso.py +194 -0
  12. bluemesh2d/geom_util/poly_util.py +795 -0
  13. bluemesh2d/geom_util/proj_util.py +250 -0
  14. bluemesh2d/geomesh_util/border_util.py +283 -0
  15. bluemesh2d/geomesh_util/depth_field.py +333 -0
  16. bluemesh2d/geomesh_util/grd_util.py +1000 -0
  17. bluemesh2d/geomesh_util/interpolation_mesh.py +318 -0
  18. bluemesh2d/geomesh_util/merge_circumcenters.py +268 -0
  19. bluemesh2d/geomesh_util/water_polygon.py +406 -0
  20. bluemesh2d/hfun_util/build_hfun.py +589 -0
  21. bluemesh2d/hfun_util/hfun_dispersion.py +96 -0
  22. bluemesh2d/hfun_util/lfshfn.py +110 -0
  23. bluemesh2d/hfun_util/limhfn.py +65 -0
  24. bluemesh2d/hfun_util/make_constant_hfun.py +32 -0
  25. bluemesh2d/hfun_util/make_depth_hfun.py +51 -0
  26. bluemesh2d/hfun_util/smooth_and_precomput.py +188 -0
  27. bluemesh2d/hfun_util/trihfn.py +84 -0
  28. bluemesh2d/hjac_util/limgrad.py +105 -0
  29. bluemesh2d/mesh_ball/cdtbal1.py +38 -0
  30. bluemesh2d/mesh_ball/cdtbal2.py +104 -0
  31. bluemesh2d/mesh_ball/inv_2x2.py +61 -0
  32. bluemesh2d/mesh_ball/inv_3x3.py +72 -0
  33. bluemesh2d/mesh_ball/pwrbal2.py +134 -0
  34. bluemesh2d/mesh_ball/tribal2.py +27 -0
  35. bluemesh2d/mesh_cost/relhfn.py +62 -0
  36. bluemesh2d/mesh_cost/triang.py +59 -0
  37. bluemesh2d/mesh_cost/triarea.py +51 -0
  38. bluemesh2d/mesh_cost/trideg.py +44 -0
  39. bluemesh2d/mesh_cost/triscr.py +43 -0
  40. bluemesh2d/mesh_file/bnd_util.py +664 -0
  41. bluemesh2d/mesh_file/loadmsh.py +165 -0
  42. bluemesh2d/mesh_file/ugrid.py +308 -0
  43. bluemesh2d/mesh_util/cfmtri.py +118 -0
  44. bluemesh2d/mesh_util/deltri.py +131 -0
  45. bluemesh2d/mesh_util/idxtri.py +53 -0
  46. bluemesh2d/mesh_util/isfeat.py +90 -0
  47. bluemesh2d/mesh_util/minlen.py +46 -0
  48. bluemesh2d/mesh_util/setset.py +49 -0
  49. bluemesh2d/mesh_util/tricon.py +90 -0
  50. bluemesh2d/mesh_util/tridiv.py +187 -0
  51. bluemesh2d/meshgen.py +202 -0
  52. bluemesh2d/ortho_merge/constants.py +27 -0
  53. bluemesh2d/ortho_merge/geometry.py +64 -0
  54. bluemesh2d/ortho_merge/ortho_merge_iter.py +812 -0
  55. bluemesh2d/ortho_merge/orthogonalize.py +2989 -0
  56. bluemesh2d/pipeline.py +277 -0
  57. bluemesh2d/poly_data/airfoil.msh +958 -0
  58. bluemesh2d/poly_data/channel.msh +212 -0
  59. bluemesh2d/poly_data/islands.msh +13819 -0
  60. bluemesh2d/poly_data/lake.msh +612 -0
  61. bluemesh2d/poly_data/river.msh +690 -0
  62. bluemesh2d/poly_test/inpoly.py +105 -0
  63. bluemesh2d/poly_test/inpoly_mat.py +104 -0
  64. bluemesh2d/refine.py +972 -0
  65. bluemesh2d/smood.py +623 -0
  66. bluemesh2d/smooth.py +522 -0
  67. bluemesh2d/tricost.py +426 -0
  68. bluemesh2d/tridemo.py +723 -0
  69. bluemesh2d/triread.py +53 -0
  70. bluemesh2d-0.1.1.dev0.dist-info/METADATA +139 -0
  71. bluemesh2d-0.1.1.dev0.dist-info/RECORD +74 -0
  72. bluemesh2d-0.1.1.dev0.dist-info/WHEEL +5 -0
  73. bluemesh2d-0.1.1.dev0.dist-info/licenses/LICENSE +674 -0
  74. bluemesh2d-0.1.1.dev0.dist-info/top_level.txt +1 -0
bluemesh2d/tricost.py ADDED
@@ -0,0 +1,426 @@
1
+ import matplotlib.pyplot as plt
2
+ import numpy as np
3
+
4
+ from .mesh_cost.relhfn import relhfn
5
+ from .mesh_cost.triang import triang
6
+ from .mesh_cost.trideg import trideg
7
+ from .mesh_cost.triscr import triscr
8
+
9
+
10
+ def tricost(vert=None, conn=None, tria=None, tnum=None, hvrt=None):
11
+ """Plot quality-metric histograms for a 2D triangle mesh.
12
+
13
+ Parameters
14
+ ----------
15
+ vert : ndarray of shape (V, 2)
16
+ Vertex coordinates.
17
+ conn : ndarray of shape (E, 2)
18
+ Constrained edges.
19
+ tria : ndarray of shape (T, 3)
20
+ Triangle connectivity.
21
+ tnum : ndarray of shape (T, 1), optional
22
+ Part index per triangle.
23
+ hvrt : ndarray of shape (V,), optional
24
+ Vertex mesh-size values; when provided, also plots relative edge lengths.
25
+
26
+ Notes
27
+ -----
28
+ Displays histograms of element quality scores, angles, relative sizes
29
+ (when ``hvrt`` is given), and node degree.
30
+
31
+ References
32
+ ----------
33
+ Translation of the MESH2D function ``TRICOST``.
34
+ Original MATLAB source: https://github.com/dengwirda/mesh2d
35
+ """
36
+
37
+ if not all(
38
+ isinstance(x, (np.ndarray, type(None))) for x in [vert, conn, tria, tnum, hvrt]
39
+ ):
40
+ raise TypeError("tricost:incorrectInputClass - Incorrect input class.")
41
+
42
+ if vert is None or conn is None or tria is None:
43
+ raise ValueError("tricost: missing required inputs (vert, conn, tria).")
44
+
45
+ if vert.ndim != 2 or conn.ndim != 2 or tria.ndim != 2:
46
+ raise ValueError("tricost:incorrectDimensions - Inputs must be 2D arrays.")
47
+
48
+ if vert.shape[1] != 2 or conn.shape[1] < 2 or tria.shape[1] < 3:
49
+ raise ValueError("tricost:incorrectDimensions - Invalid input sizes.")
50
+
51
+ nvrt = vert.shape[0]
52
+ _ntri = tria.shape[0]
53
+
54
+ if np.min(conn[:, :2]) < 0 or np.max(conn[:, :2]) > nvrt:
55
+ raise ValueError("tricost:invalidInputs - Invalid EDGE input array.")
56
+
57
+ if np.min(tria[:, :3]) < 0 or np.max(tria[:, :3]) > nvrt:
58
+ raise ValueError("tricost:invalidInputs - Invalid TRIA input array.")
59
+
60
+ axpos31 = [0.125, 0.750, 0.800, 0.150]
61
+ axpos32 = [0.125, 0.450, 0.800, 0.150]
62
+ axpos33 = [0.125, 0.150, 0.800, 0.150]
63
+
64
+ axpos41 = [0.125, 0.835, 0.800, 0.135]
65
+ axpos42 = [0.125, 0.590, 0.800, 0.135]
66
+ axpos43 = [0.125, 0.345, 0.800, 0.135]
67
+ axpos44 = [0.125, 0.100, 0.800, 0.135]
68
+
69
+ fig = plt.figure(figsize=(6, 6))
70
+ fig.patch.set_facecolor("w")
71
+
72
+ if hvrt is not None and hvrt.size > 0:
73
+ ax1 = fig.add_axes(axpos41)
74
+ ax1.set_title("Quality score")
75
+ scrhist(triscr(vert, tria), "tria3", ax=ax1)
76
+
77
+ ax2 = fig.add_axes(axpos42)
78
+ ax2.set_title("Angles")
79
+ anghist(triang(vert, tria), "tria3", ax=ax2)
80
+
81
+ ax3 = fig.add_axes(axpos43)
82
+ ax3.set_title("Relative size")
83
+ hfnhist(relhfn(vert, tria, hvrt), "tria3", ax=ax3)
84
+
85
+ ax4 = fig.add_axes(axpos44)
86
+ ax4.set_title("Node degree")
87
+ deghist(trideg(vert, tria), "tria3", ax=ax4)
88
+
89
+ else:
90
+ ax1 = fig.add_axes(axpos31)
91
+ ax1.set_title("Quality score")
92
+ scrhist(triscr(vert, tria), "tria3", ax=ax1)
93
+
94
+ ax2 = fig.add_axes(axpos32)
95
+ ax2.set_title("Angles")
96
+ anghist(triang(vert, tria), "tria3", ax=ax2)
97
+
98
+ ax3 = fig.add_axes(axpos33)
99
+ ax3.set_title("Node degree")
100
+ deghist(trideg(vert, tria), "tria3", ax=ax3)
101
+
102
+
103
+ def mad(ff):
104
+ """Compute the mean absolute deviation from the mean.
105
+
106
+ Parameters
107
+ ----------
108
+ ff : array_like
109
+ Input values.
110
+
111
+ Returns
112
+ -------
113
+ mf : float
114
+ Mean absolute deviation.
115
+
116
+ References
117
+ ----------
118
+ Translation of the MESH2D function ``MAD``.
119
+ Original MATLAB source: https://github.com/dengwirda/mesh2d
120
+ """
121
+
122
+ ff = np.asarray(ff, dtype=float)
123
+ mf = np.mean(np.abs(ff - np.mean(ff)))
124
+ return mf
125
+
126
+
127
+ def deghist(dd, ty, ax=None):
128
+ """Plot a histogram of node degree.
129
+
130
+ Parameters
131
+ ----------
132
+ dd : array_like
133
+ Node degrees.
134
+ ty : str
135
+ Mesh type label (``'tria3'`` or ``'tria4'``).
136
+ ax : matplotlib.axes.Axes, optional
137
+ Axes to draw on; defaults to the current axes.
138
+
139
+ References
140
+ ----------
141
+ Translation of the MESH2D function ``DEGHIST``.
142
+ Original MATLAB source: https://github.com/dengwirda/mesh2d
143
+ """
144
+ if ax is None:
145
+ ax = plt.gca()
146
+ dd = np.asarray(dd).ravel()
147
+ be = np.arange(1, np.max(dd) + 1)
148
+ hc, _ = np.histogram(dd, bins=np.append(be, np.max(be) + 1))
149
+
150
+ k = (0.60, 0.60, 0.60)
151
+
152
+ # Histogram
153
+ ax.bar(be, hc, width=1.05, color=k, edgecolor=k)
154
+
155
+ ax.axis("tight")
156
+ ax.set_yticks([])
157
+ ax.set_xticks(np.arange(2, 13, 2))
158
+ ax.tick_params(axis="both", which="both", length=5, width=2)
159
+ ax.set_xlim([0, 12])
160
+ ax.set_xlabel("", fontsize=22)
161
+
162
+ # Axis label depends on the triangulation type
163
+ if ty == "tria4":
164
+ ax.text(-0.225, 0, r"$|d|_{\tau}$", ha="right", fontsize=22)
165
+ elif ty == "tria3":
166
+ ax.text(-0.225, 0, r"$|d|_{f}$", ha="right", fontsize=22)
167
+
168
+
169
+ def anghist(ad, ty, ax=None):
170
+ """Plot a histogram of triangle angles.
171
+
172
+ Parameters
173
+ ----------
174
+ ad : array_like
175
+ Internal angles in degrees.
176
+ ty : str
177
+ Mesh type label (``'tria3'`` or ``'tria4'``).
178
+ ax : matplotlib.axes.Axes, optional
179
+ Axes to draw on; defaults to the current axes.
180
+
181
+ References
182
+ ----------
183
+ Translation of the MESH2D function ``ANGHIST``.
184
+ Original MATLAB source: https://github.com/dengwirda/mesh2d
185
+ """
186
+ if ax is None:
187
+ ax = plt.gca()
188
+ ad = np.asarray(ad).ravel()
189
+ be = np.linspace(0.0, 180.0, 91)
190
+ bm = (be[:-1] + be[1:]) / 2.0
191
+ hc, _ = np.histogram(ad, bins=be)
192
+
193
+ if ty == "tria4":
194
+ poor = (bm < 10.0) | (bm >= 160.0)
195
+ okay = ((bm >= 10.0) & (bm < 20.0)) | ((bm >= 140.0) & (bm < 160.0))
196
+ good = ((bm >= 20.0) & (bm < 30.0)) | ((bm >= 120.0) & (bm < 140.0))
197
+ best = (bm >= 30.0) & (bm < 120.0)
198
+ elif ty == "tria3":
199
+ poor = (bm < 15.0) | (bm >= 150.0)
200
+ okay = ((bm >= 15.0) & (bm < 30.0)) | ((bm >= 120.0) & (bm < 150.0))
201
+ good = ((bm >= 30.0) & (bm < 45.0)) | ((bm >= 90.0) & (bm < 120.0))
202
+ best = (bm >= 45.0) & (bm < 90.0)
203
+ else:
204
+ raise ValueError("Type must be 'tria3' or 'tria4'.")
205
+
206
+ r = (0.85, 0.00, 0.00)
207
+ y = (1.00, 0.95, 0.00)
208
+ g = (0.00, 0.90, 0.00)
209
+ k = (0.60, 0.60, 0.60)
210
+
211
+ ax.bar(bm[poor], hc[poor], width=1.05, color=r, edgecolor=r)
212
+ ax.bar(bm[okay], hc[okay], width=1.05, color=y, edgecolor=y)
213
+ ax.bar(bm[good], hc[good], width=1.05, color=g, edgecolor=g)
214
+ ax.bar(bm[best], hc[best], width=1.05, color=k, edgecolor=k)
215
+
216
+ ax.axis("tight")
217
+ ax.set_yticks([])
218
+ ax.set_xticks(np.arange(0, 181, 30))
219
+ ax.set_xlim([0.0, 180.0])
220
+ ax.tick_params(axis="both", which="both", length=5, width=2, labelsize=14)
221
+
222
+ mina = max(1.000, np.min(ad))
223
+ maxa = min(179.0, np.max(ad))
224
+
225
+ mada_val = mad(ad)
226
+
227
+ ax.axvline(x=mina, color="r", linewidth=1.5)
228
+ ax.axvline(x=maxa, color="r", linewidth=1.5)
229
+
230
+ if mina > 25.0:
231
+ ax.text(
232
+ mina - 1.8, 0.90 * np.max(hc), f"{np.min(ad):.1f}", ha="right", fontsize=15
233
+ )
234
+ else:
235
+ ax.text(
236
+ mina + 1.8, 0.90 * np.max(hc), f"{np.min(ad):.1f}", ha="left", fontsize=15
237
+ )
238
+
239
+ if maxa < 140.0:
240
+ ax.text(
241
+ maxa + 1.8, 0.90 * np.max(hc), f"{np.max(ad):.1f}", ha="left", fontsize=15
242
+ )
243
+ else:
244
+ ax.text(
245
+ maxa - 1.8, 0.90 * np.max(hc), f"{np.max(ad):.1f}", ha="right", fontsize=15
246
+ )
247
+
248
+ if maxa < 100.0:
249
+ ax.text(
250
+ maxa - 16.0,
251
+ 0.45 * np.max(hc),
252
+ r"$\bar{\sigma}_{\theta}\!=$",
253
+ ha="left",
254
+ fontsize=16,
255
+ )
256
+ ax.text(
257
+ maxa + 1.8, 0.45 * np.max(hc), f"{mada_val:.2f}", ha="left", fontsize=15
258
+ )
259
+ else:
260
+ ax.text(
261
+ maxa - 16.0,
262
+ 0.45 * np.max(hc),
263
+ r"$\bar{\sigma}_{\theta}\!=$",
264
+ ha="left",
265
+ fontsize=16,
266
+ )
267
+ ax.text(
268
+ maxa + 1.8, 0.45 * np.max(hc), f"{mada_val:.3f}", ha="left", fontsize=15
269
+ )
270
+
271
+ if ty == "tria4":
272
+ ax.text(-9.0, 0.0, r"$\theta_{\tau}$", ha="right", fontsize=22)
273
+ elif ty == "tria3":
274
+ ax.text(-9.0, 0.0, r"$\theta_{f}$", ha="right", fontsize=22)
275
+
276
+
277
+ def scrhist(sc, ty, ax=None):
278
+ """Plot a histogram of element quality scores.
279
+
280
+ Parameters
281
+ ----------
282
+ sc : array_like
283
+ Quality scores in ``[0, 1]``.
284
+ ty : str
285
+ Mesh type label (``'tria3'``, ``'tria4'``, ``'dual3'``, or ``'dual4'``).
286
+ ax : matplotlib.axes.Axes, optional
287
+ Axes to draw on; defaults to the current axes.
288
+
289
+ References
290
+ ----------
291
+ Translation of the MESH2D function ``SCRHIST``.
292
+ Original MATLAB source: https://github.com/dengwirda/mesh2d
293
+ """
294
+
295
+ if ax is None:
296
+ ax = plt.gca()
297
+ sc = np.asarray(sc).ravel()
298
+ be = np.linspace(0.0, 1.0, 101)
299
+ bm = (be[:-1] + be[1:]) / 2.0
300
+ hc, _ = np.histogram(sc, bins=be)
301
+
302
+ if ty in ("tria4", "dual4"):
303
+ poor = bm < 0.25
304
+ okay = (bm >= 0.25) & (bm < 0.50)
305
+ good = (bm >= 0.50) & (bm < 0.75)
306
+ best = bm >= 0.75
307
+ elif ty in ("tria3", "dual3"):
308
+ poor = bm < 0.30
309
+ okay = (bm >= 0.30) & (bm < 0.60)
310
+ good = (bm >= 0.60) & (bm < 0.90)
311
+ best = bm >= 0.90
312
+ else:
313
+ raise ValueError("Type must be 'tria3','tria4','dual3' or 'dual4'.")
314
+
315
+ r = (0.85, 0.00, 0.00)
316
+ y = (1.00, 0.95, 0.00)
317
+ g = (0.00, 0.90, 0.00)
318
+ k = (0.60, 0.60, 0.60)
319
+
320
+ ax.bar(bm[poor], hc[poor], width=1.05 / 100, color=r, edgecolor=r)
321
+ ax.bar(bm[okay], hc[okay], width=1.05 / 100, color=y, edgecolor=y)
322
+ ax.bar(bm[good], hc[good], width=1.05 / 100, color=g, edgecolor=g)
323
+ ax.bar(bm[best], hc[best], width=1.05 / 100, color=k, edgecolor=k)
324
+
325
+ ax.axis("tight")
326
+ ax.set_yticks([])
327
+ ax.set_xticks(np.arange(0.0, 1.01, 0.2))
328
+ ax.set_xlim([0.0, 1.0])
329
+ ax.tick_params(axis="both", which="both", length=5, width=2, labelsize=14)
330
+
331
+ mins = max(0.010, np.min(sc))
332
+ _maxs = min(0.990, np.max(sc))
333
+
334
+ ax.axvline(x=mins, color="r", linewidth=1.5)
335
+ ax.axvline(x=np.mean(sc), color="r", linewidth=1.5)
336
+
337
+ if mins > 0.4:
338
+ ax.text(
339
+ mins - 0.01, 0.9 * np.max(hc), f"{np.min(sc):.3f}", ha="right", fontsize=15
340
+ )
341
+ else:
342
+ ax.text(
343
+ mins + 0.01, 0.9 * np.max(hc), f"{np.min(sc):.3f}", ha="left", fontsize=15
344
+ )
345
+
346
+ if np.mean(sc) > mins + 0.150:
347
+ ax.text(
348
+ np.mean(sc) - 0.01,
349
+ 0.9 * np.max(hc),
350
+ f"{np.mean(sc):.3f}",
351
+ ha="right",
352
+ fontsize=15,
353
+ )
354
+
355
+ if ty == "tria4":
356
+ ax.text(
357
+ -0.04, 0.0, r"$\mathcal{Q}^{\mathcal{T}}_{\tau}$", ha="right", fontsize=22
358
+ )
359
+ elif ty == "tria3":
360
+ ax.text(-0.04, 0.0, r"$\mathcal{Q}^{\mathcal{T}}_{f}$", ha="right", fontsize=22)
361
+ elif ty == "dual4":
362
+ ax.text(
363
+ -0.04, 0.0, r"$\mathcal{Q}^{\mathcal{D}}_{\tau}$", ha="right", fontsize=22
364
+ )
365
+ elif ty == "dual3":
366
+ ax.text(-0.04, 0.0, r"$\mathcal{Q}^{\mathcal{D}}_{f}$", ha="right", fontsize=22)
367
+
368
+
369
+ def hfnhist(hf, ty, ax=None):
370
+ """Plot a histogram of relative mesh-size ratios.
371
+
372
+ Parameters
373
+ ----------
374
+ hf : array_like
375
+ Ratio of actual to target edge length.
376
+ ty : str
377
+ Mesh type label (kept for API compatibility).
378
+ ax : matplotlib.axes.Axes, optional
379
+ Axes to draw on; defaults to the current axes.
380
+
381
+ References
382
+ ----------
383
+ Translation of the MESH2D function ``HFNHIST``.
384
+ Original MATLAB source: https://github.com/dengwirda/mesh2d
385
+ """
386
+
387
+ if ax is None:
388
+ ax = plt.gca()
389
+ hf = np.asarray(hf).ravel()
390
+
391
+ be = np.linspace(0.0, 2.0, 101)
392
+ bm = (be[:-1] + be[1:]) / 2.0
393
+ hc, _ = np.histogram(hf, bins=be)
394
+
395
+ poor = (bm < 0.40) | (bm >= 1.6)
396
+ okay = ((bm >= 0.40) & (bm < 0.60)) | ((bm >= 1.4) & (bm < 1.6))
397
+ good = ((bm >= 0.60) & (bm < 0.80)) | ((bm >= 1.2) & (bm < 1.4))
398
+ best = (bm >= 0.80) & (bm < 1.2)
399
+
400
+ r = (0.85, 0.00, 0.00)
401
+ y = (1.00, 0.95, 0.00)
402
+ g = (0.00, 0.90, 0.00)
403
+ k = (0.60, 0.60, 0.60)
404
+
405
+ ax.bar(bm[poor], hc[poor], width=1.05 / 100, color=r, edgecolor=r)
406
+ ax.bar(bm[okay], hc[okay], width=1.05 / 100, color=y, edgecolor=y)
407
+ ax.bar(bm[good], hc[good], width=1.05 / 100, color=g, edgecolor=g)
408
+ ax.bar(bm[best], hc[best], width=1.05 / 100, color=k, edgecolor=k)
409
+
410
+ ax.axis("tight")
411
+ ax.set_yticks([])
412
+ ax.set_xticks(np.arange(0.0, 2.1, 0.5))
413
+ ax.set_xlim([0.0, 2.0])
414
+ ax.tick_params(axis="both", which="both", length=5, width=2, labelsize=14)
415
+
416
+ maxhf = np.max(hf)
417
+ ax.axvline(x=maxhf, color="r", linewidth=1.5)
418
+
419
+ ax.text(maxhf + 0.02, 0.90 * np.max(hc), f"{maxhf:.2f}", ha="left", fontsize=15)
420
+
421
+ ax.text(
422
+ maxhf - 0.18, 0.45 * np.max(hc), r"$\bar{\sigma}_{h} =$", ha="left", fontsize=16
423
+ )
424
+ ax.text(maxhf + 0.02, 0.45 * np.max(hc), f"{mad(hf):.2f}", ha="left", fontsize=15)
425
+
426
+ ax.text(-0.10, 0.0, r"$h_{r}$", ha="right", fontsize=22)