freealg 0.1.11__py3-none-any.whl → 0.7.12__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 (59) hide show
  1. freealg/__init__.py +8 -2
  2. freealg/__version__.py +1 -1
  3. freealg/_algebraic_form/__init__.py +12 -0
  4. freealg/_algebraic_form/_branch_points.py +288 -0
  5. freealg/_algebraic_form/_constraints.py +139 -0
  6. freealg/_algebraic_form/_continuation_algebraic.py +706 -0
  7. freealg/_algebraic_form/_decompress.py +641 -0
  8. freealg/_algebraic_form/_decompress2.py +204 -0
  9. freealg/_algebraic_form/_edge.py +330 -0
  10. freealg/_algebraic_form/_homotopy.py +323 -0
  11. freealg/_algebraic_form/_moments.py +448 -0
  12. freealg/_algebraic_form/_sheets_util.py +145 -0
  13. freealg/_algebraic_form/_support.py +309 -0
  14. freealg/_algebraic_form/algebraic_form.py +1232 -0
  15. freealg/_free_form/__init__.py +16 -0
  16. freealg/{_chebyshev.py → _free_form/_chebyshev.py} +75 -43
  17. freealg/_free_form/_decompress.py +993 -0
  18. freealg/_free_form/_density_util.py +243 -0
  19. freealg/_free_form/_jacobi.py +359 -0
  20. freealg/_free_form/_linalg.py +508 -0
  21. freealg/{_pade.py → _free_form/_pade.py} +42 -208
  22. freealg/{_plot_util.py → _free_form/_plot_util.py} +37 -22
  23. freealg/{_sample.py → _free_form/_sample.py} +58 -22
  24. freealg/_free_form/_series.py +454 -0
  25. freealg/_free_form/_support.py +214 -0
  26. freealg/_free_form/free_form.py +1362 -0
  27. freealg/_geometric_form/__init__.py +13 -0
  28. freealg/_geometric_form/_continuation_genus0.py +175 -0
  29. freealg/_geometric_form/_continuation_genus1.py +275 -0
  30. freealg/_geometric_form/_elliptic_functions.py +174 -0
  31. freealg/_geometric_form/_sphere_maps.py +63 -0
  32. freealg/_geometric_form/_torus_maps.py +118 -0
  33. freealg/_geometric_form/geometric_form.py +1094 -0
  34. freealg/_util.py +56 -110
  35. freealg/distributions/__init__.py +7 -1
  36. freealg/distributions/_chiral_block.py +494 -0
  37. freealg/distributions/_deformed_marchenko_pastur.py +726 -0
  38. freealg/distributions/_deformed_wigner.py +386 -0
  39. freealg/distributions/_kesten_mckay.py +29 -15
  40. freealg/distributions/_marchenko_pastur.py +224 -95
  41. freealg/distributions/_meixner.py +47 -37
  42. freealg/distributions/_wachter.py +29 -17
  43. freealg/distributions/_wigner.py +27 -14
  44. freealg/visualization/__init__.py +12 -0
  45. freealg/visualization/_glue_util.py +32 -0
  46. freealg/visualization/_rgb_hsv.py +125 -0
  47. freealg-0.7.12.dist-info/METADATA +172 -0
  48. freealg-0.7.12.dist-info/RECORD +53 -0
  49. {freealg-0.1.11.dist-info → freealg-0.7.12.dist-info}/WHEEL +1 -1
  50. freealg/_decompress.py +0 -180
  51. freealg/_jacobi.py +0 -218
  52. freealg/_support.py +0 -85
  53. freealg/freeform.py +0 -967
  54. freealg-0.1.11.dist-info/METADATA +0 -140
  55. freealg-0.1.11.dist-info/RECORD +0 -24
  56. /freealg/{_damp.py → _free_form/_damp.py} +0 -0
  57. {freealg-0.1.11.dist-info → freealg-0.7.12.dist-info}/licenses/AUTHORS.txt +0 -0
  58. {freealg-0.1.11.dist-info → freealg-0.7.12.dist-info}/licenses/LICENSE.txt +0 -0
  59. {freealg-0.1.11.dist-info → freealg-0.7.12.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,309 @@
1
+ # SPDX-FileCopyrightText: Copyright 2026, Siavash Ameli <sameli@berkeley.edu>
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+ # SPDX-FileType: SOURCE
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify it under
6
+ # the terms of the license found in the LICENSE.txt file in the root directory
7
+ # of this source tree.
8
+
9
+
10
+ # =======
11
+ # Imports
12
+ # =======
13
+
14
+ import numpy
15
+ import numpy.polynomial.polynomial as poly
16
+
17
+ __all__ = ['compute_support']
18
+
19
+
20
+ # ======================
21
+ # poly coeffs in m and z
22
+ # ======================
23
+
24
+ def _poly_coeffs_in_m_at_z(a_coeffs, z):
25
+
26
+ s = a_coeffs.shape[1] - 1
27
+ a = numpy.empty(s + 1, dtype=numpy.complex128)
28
+ for j in range(s + 1):
29
+ a[j] = poly.polyval(z, a_coeffs[:, j])
30
+ return a
31
+
32
+
33
+ # ===============
34
+ # roots poly in m
35
+ # ===============
36
+
37
+ def _roots_poly_in_m(c_asc, tol=0.0):
38
+
39
+ c = numpy.asarray(c_asc, dtype=numpy.complex128).ravel()
40
+ if c.size <= 1:
41
+ return numpy.array([], dtype=numpy.complex128)
42
+
43
+ k = c.size - 1
44
+ while k > 0 and abs(c[k]) <= tol:
45
+ k -= 1
46
+ c = c[:k + 1]
47
+ if c.size <= 1:
48
+ return numpy.array([], dtype=numpy.complex128)
49
+
50
+ return numpy.roots(c[::-1])
51
+
52
+
53
+ # ================
54
+ # dPdm coeffs at z
55
+ # ================
56
+
57
+ def _dPdm_coeffs_at_z(a_coeffs, z):
58
+
59
+ a = _poly_coeffs_in_m_at_z(a_coeffs, z)
60
+ s = a.size - 1
61
+ if s <= 0:
62
+ return numpy.array([0.0 + 0.0j], dtype=numpy.complex128)
63
+ d = numpy.empty(s, dtype=numpy.complex128)
64
+ for j in range(1, s + 1):
65
+ d[j - 1] = j * a[j]
66
+ return d
67
+
68
+
69
+ # ==============
70
+ # P and partials
71
+ # ==============
72
+
73
+ def _P_and_partials(a_coeffs, z, m):
74
+
75
+ s = a_coeffs.shape[1] - 1
76
+
77
+ a = numpy.empty(s + 1, dtype=numpy.complex128)
78
+ da = numpy.empty(s + 1, dtype=numpy.complex128)
79
+ for j in range(s + 1):
80
+ a[j] = poly.polyval(z, a_coeffs[:, j])
81
+ da[j] = poly.polyval(z, poly.polyder(a_coeffs[:, j]))
82
+
83
+ mpow = 1.0 + 0.0j
84
+ P = 0.0 + 0.0j
85
+ Pz = 0.0 + 0.0j
86
+ for j in range(s + 1):
87
+ P += a[j] * mpow
88
+ Pz += da[j] * mpow
89
+ mpow *= m
90
+
91
+ Pm = 0.0 + 0.0j
92
+ Pmm = 0.0 + 0.0j
93
+ Pzm = 0.0 + 0.0j
94
+ for j in range(1, s + 1):
95
+ Pm += j * a[j] * (m ** (j - 1))
96
+ Pzm += j * da[j] * (m ** (j - 1))
97
+ for j in range(2, s + 1):
98
+ Pmm += j * (j - 1) * a[j] * (m ** (j - 2))
99
+
100
+ return P, Pz, Pm, Pzm, Pmm, a
101
+
102
+
103
+ # ===========
104
+ # newton edge
105
+ # ===========
106
+
107
+ def _newton_edge(a_coeffs, x0, m0, tol=1e-12, max_iter=50):
108
+
109
+ x = float(x0)
110
+ m = float(m0)
111
+
112
+ for _ in range(max_iter):
113
+ z = x + 0.0j
114
+ P, Pz, Pm, Pzm, Pmm, _ = _P_and_partials(a_coeffs, z, m)
115
+
116
+ f0 = float(numpy.real(P))
117
+ f1 = float(numpy.real(Pm))
118
+
119
+ j00 = float(numpy.real(Pz))
120
+ j01 = float(numpy.real(Pm))
121
+ j10 = float(numpy.real(Pzm))
122
+ j11 = float(numpy.real(Pmm))
123
+
124
+ det = j00 * j11 - j01 * j10
125
+ if det == 0.0 or (not numpy.isfinite(det)):
126
+ return x, m, False
127
+
128
+ dx = (-f0 * j11 + f1 * j01) / det
129
+ dm = (-j00 * f1 + j10 * f0) / det
130
+
131
+ x += dx
132
+ m += dm
133
+
134
+ if abs(dx) + abs(dm) < tol:
135
+ return x, m, True
136
+
137
+ return x, m, False
138
+
139
+
140
+ # =============
141
+ # cluster edges
142
+ # =============
143
+
144
+ def _cluster_edges(edges, x_tol):
145
+
146
+ if len(edges) == 0:
147
+ return numpy.array([], dtype=float)
148
+
149
+ edges = numpy.array(sorted(edges), dtype=float)
150
+ out = [edges[0]]
151
+ for e in edges[1:]:
152
+ if abs(e - out[-1]) > x_tol:
153
+ out.append(e)
154
+ return numpy.array(out, dtype=float)
155
+
156
+
157
+ # =======================
158
+ # pick physical root at z
159
+ # =======================
160
+
161
+ def _pick_physical_root_at_z(a_coeffs, z, im_sign=+1):
162
+
163
+ a = _poly_coeffs_in_m_at_z(a_coeffs, z)
164
+ r = _roots_poly_in_m(a)
165
+ if r.size == 0:
166
+ return numpy.nan + 1j * numpy.nan
167
+
168
+ w_ref = -1.0 / z
169
+ idx = int(numpy.argmin(numpy.abs(r - w_ref)))
170
+ w = r[idx]
171
+
172
+ # optional strictness: if it violates Herglotz, declare failure
173
+ if not numpy.isfinite(w.real) or not numpy.isfinite(w.imag):
174
+ return w
175
+ if (im_sign * w.imag) <= 0.0:
176
+ return w
177
+
178
+ return w
179
+
180
+
181
+ # ===============
182
+ # compute support
183
+ # ===============
184
+
185
+ def compute_support(a_coeffs,
186
+ x_min,
187
+ x_max,
188
+ n_scan=4000,
189
+ y_eps=1e-3,
190
+ im_sign=+1,
191
+ root_tol=0.0,
192
+ edge_rel_tol=1e-6,
193
+ edge_x_cluster_tol=1e-3,
194
+ newton_tol=1e-12):
195
+ """
196
+ Fast support from fitted polynomial using branch-point system P=0, Pm=0.
197
+
198
+ Returns
199
+ -------
200
+ support : list of (a,b)
201
+ info : dict (edges, rel_res_curve, etc.)
202
+ """
203
+
204
+ a_coeffs = numpy.asarray(a_coeffs)
205
+ x_grid = numpy.linspace(float(x_min), float(x_max), int(n_scan))
206
+
207
+ # For each x, find best real critical point m (Pm=0) minimizing rel
208
+ # residual.
209
+ rel = numpy.full(x_grid.size, numpy.inf, dtype=float)
210
+ m_star = numpy.full(x_grid.size, numpy.nan, dtype=float)
211
+
212
+ for i, x in enumerate(x_grid):
213
+ z = x + 0.0j
214
+ dcoef = _dPdm_coeffs_at_z(a_coeffs, z)
215
+ mr = _roots_poly_in_m(dcoef, tol=root_tol)
216
+
217
+ best = numpy.inf
218
+ best_m = numpy.nan
219
+
220
+ for w in mr:
221
+ # accept nearly-real roots; numerical roots can have small imag
222
+ # part
223
+ if abs(w.imag) > 1e-6 * (1.0 + abs(w.real)):
224
+ continue
225
+ m = float(w.real)
226
+ P, _, _, _, _, a = _P_and_partials(a_coeffs, z, m)
227
+
228
+ denom = 1.0
229
+ am = 1.0
230
+ for j in range(a.size):
231
+ denom += abs(a[j]) * abs(am)
232
+ am *= m
233
+
234
+ r = abs(numpy.real(P)) / denom
235
+ if numpy.isfinite(r) and r < best:
236
+ best = float(r)
237
+ best_m = m
238
+
239
+ rel[i] = best
240
+ m_star[i] = best_m
241
+
242
+ # Pick candidate edges as local minima of rel(x), below an automatic scale.
243
+ rel_f = rel[numpy.isfinite(rel)]
244
+ if rel_f.size == 0:
245
+ return [], {"edges": numpy.array([], dtype=float), "n_edges": 0}
246
+
247
+ med = float(numpy.median(rel_f))
248
+ min_rel = float(numpy.min(rel_f))
249
+
250
+ # accept local minima up to a factor above the best one, but never abov
251
+ # background scale
252
+ thr = min(0.1 * med, max(float(edge_rel_tol), 1e4 * min_rel))
253
+
254
+ edges0 = []
255
+ seeds = []
256
+
257
+ for i in range(1, x_grid.size - 1):
258
+ if not numpy.isfinite(rel[i]):
259
+ continue
260
+ if rel[i] <= rel[i - 1] and rel[i] <= rel[i + 1] and rel[i] < thr and \
261
+ numpy.isfinite(m_star[i]):
262
+ edges0.append(float(x_grid[i]))
263
+ seeds.append((float(x_grid[i]), float(m_star[i])))
264
+
265
+ # Refine each seed by 2D Newton (x,m)
266
+ edges = []
267
+ for x0, m0 in seeds:
268
+ xe, me, ok = _newton_edge(a_coeffs, x0, m0, tol=newton_tol)
269
+ if ok and numpy.isfinite(xe) and numpy.isfinite(me):
270
+ edges.append(float(xe))
271
+
272
+ edges = _cluster_edges(edges, edge_x_cluster_tol)
273
+ edges.sort()
274
+
275
+ # Build support by testing midpoints between consecutive real edges
276
+ support = []
277
+ m_im_tol = 1e-10
278
+
279
+ for i in range(edges.size - 1):
280
+ a = float(edges[i])
281
+ b = float(edges[i + 1])
282
+ if b <= a:
283
+ continue
284
+
285
+ xmid = 0.5 * (a + b)
286
+
287
+ # roots of P(xmid, m) with real coefficients
288
+ a_m = _poly_coeffs_in_m_at_z(a_coeffs, xmid + 0.0j)
289
+ r = _roots_poly_in_m(a_m, tol=root_tol)
290
+
291
+ # interval is support iff there exists a non-real root (complex pair)
292
+ if numpy.any(numpy.abs(numpy.imag(r)) > m_im_tol):
293
+ support.append((a, b))
294
+
295
+ info = {
296
+ "edges": edges,
297
+ "n_edges": int(edges.size),
298
+ "support": support,
299
+ "n_support": int(len(support)),
300
+ "x_grid": x_grid,
301
+ "rel": rel,
302
+ "thr": float(thr),
303
+ "x_min": float(x_min),
304
+ "x_max": float(x_max),
305
+ "n_scan": int(n_scan),
306
+ "y_eps": float(y_eps),
307
+ }
308
+
309
+ return support, info