freealg 0.7.9__py3-none-any.whl → 0.7.11__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.
@@ -1,226 +0,0 @@
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 as np
15
- import numpy.polynomial.polynomial as poly
16
-
17
- __all__ = ['compute_singular_points']
18
-
19
-
20
- # =========
21
- # ploy trim
22
- # =========
23
-
24
- def _poly_trim(p, tol):
25
-
26
- p = np.asarray(p, dtype=complex).ravel()
27
- if p.size == 0:
28
- return np.zeros(1, dtype=complex)
29
- k = p.size - 1
30
- while k > 0 and abs(p[k]) <= tol:
31
- k -= 1
32
- return p[:k + 1].copy()
33
-
34
-
35
- # ============
36
- # poly is zero
37
- # ============
38
-
39
- def _poly_is_zero(p, tol):
40
-
41
- p = _poly_trim(p, tol)
42
- return (p.size == 1) and (abs(p[0]) <= tol)
43
-
44
-
45
- # ========
46
- # poly add
47
- # ========
48
-
49
- def _poly_add(a, b, tol):
50
-
51
- return _poly_trim(poly.polyadd(a, b), tol)
52
-
53
-
54
- # ========
55
- # poly sub
56
- # ========
57
-
58
- def _poly_sub(a, b, tol):
59
-
60
- return _poly_trim(poly.polysub(a, b), tol)
61
-
62
-
63
- # =======
64
- # ply mul
65
- # =======
66
-
67
- def _poly_mul(a, b, tol):
68
-
69
- return _poly_trim(poly.polymul(a, b), tol)
70
-
71
-
72
- # ==============
73
- # poly div exact
74
- # ==============
75
-
76
- def _poly_div_exact(a, b, tol):
77
-
78
- a = _poly_trim(a, tol)
79
- b = _poly_trim(b, tol)
80
- if _poly_is_zero(b, tol):
81
- raise ZeroDivisionError("poly division by zero")
82
-
83
- q, r = poly.polydiv(a, b)
84
- r = _poly_trim(r, tol)
85
-
86
- # Bareiss expects exact division; with floats it's only approximate.
87
- # If the remainder is small, drop it.
88
- scale = max(1.0, np.linalg.norm(a))
89
- if np.linalg.norm(r) > 1e3 * tol * scale:
90
- # Fallback: still drop remainder (keeps algorithm running).
91
- # This is acceptable because we only need the resultant roots
92
- # robustly, not exact symbolic coefficients.
93
- pass
94
-
95
- return _poly_trim(q, tol)
96
-
97
-
98
- # ================
99
- # det bareiss poly
100
- # ================
101
-
102
- def _det_bareiss_poly(M, tol):
103
-
104
- n = len(M)
105
- A = [[_poly_trim(M[i][j], tol) for j in range(n)] for i in range(n)]
106
-
107
- denom = np.array([1.0], dtype=complex)
108
- sign = 1.0
109
-
110
- for k in range(n - 1):
111
- if _poly_is_zero(A[k][k], tol):
112
- piv = -1
113
- for i in range(k + 1, n):
114
- if not _poly_is_zero(A[i][k], tol):
115
- piv = i
116
- break
117
- if piv == -1:
118
- return np.array([0.0], dtype=complex)
119
- A[k], A[piv] = A[piv], A[k]
120
- sign *= -1.0
121
-
122
- pivot = A[k][k]
123
- for i in range(k + 1, n):
124
- for j in range(k + 1, n):
125
- num = _poly_sub(_poly_mul(A[i][j], pivot, tol),
126
- _poly_mul(A[i][k], A[k][j], tol),
127
- tol)
128
- if k > 0:
129
- A[i][j] = _poly_div_exact(num, denom, tol)
130
- else:
131
- A[i][j] = _poly_trim(num, tol)
132
-
133
- denom = pivot
134
-
135
- return _poly_trim(sign * A[n - 1][n - 1], tol)
136
-
137
-
138
- # ===================
139
- # cluster real points
140
- # ===================
141
-
142
- def _cluster_real_points(x, eps):
143
-
144
- x = np.asarray(x, dtype=float).ravel()
145
- if x.size == 0:
146
- return x
147
- x = np.sort(x)
148
- uniq = []
149
- for v in x:
150
- if (len(uniq) == 0) or (abs(v - uniq[-1]) > eps):
151
- uniq.append(float(v))
152
- else:
153
- uniq[-1] = 0.5 * (uniq[-1] + float(v))
154
- return np.asarray(uniq, dtype=float)
155
-
156
-
157
- # =======================
158
- # compute singular points
159
- # =======================
160
-
161
- def compute_singular_points(a_coeffs, tol=1e-12, real_tol=None):
162
- """
163
- a_coeffs[i,j] is coefficient of z^i m^j, shape (deg_z+1, s+1).
164
-
165
- Returns
166
- -------
167
-
168
- z_bp : complex array, roots of Disc_m(P)(z)
169
- a_s_zero : complex array, roots of leading coefficient a_s(z)
170
- support : list of (a,b) from real-ish branch points paired consecutively
171
- """
172
-
173
- a_coeffs = np.asarray(a_coeffs)
174
- s = a_coeffs.shape[1] - 1
175
- if s < 1:
176
- return (np.array([], dtype=complex),
177
- np.array([], dtype=complex),
178
- [])
179
-
180
- if real_tol is None:
181
- real_tol = 1e3 * tol
182
-
183
- a = [_poly_trim(a_coeffs[:, j], tol) for j in range(s + 1)]
184
-
185
- a_s = a[s]
186
- a_s_zero = np.roots(a_s[::-1]) if a_s.size > 1 else \
187
- np.array([], dtype=complex)
188
-
189
- b = []
190
- for j in range(s):
191
- b.append(_poly_trim((j + 1) * a[j + 1], tol))
192
-
193
- mdeg = s
194
- ndeg = s - 1
195
- N = mdeg + ndeg # 2s-1
196
-
197
- z0 = np.array([0.0], dtype=complex)
198
- M = [[z0 for _ in range(N)] for __ in range(N)]
199
-
200
- for r in range(ndeg):
201
- for j in range(mdeg + 1):
202
- M[r][r + j] = a[j]
203
-
204
- for r in range(mdeg):
205
- rr = ndeg + r
206
- for j in range(ndeg + 1):
207
- M[rr][r + j] = b[j]
208
-
209
- res = _det_bareiss_poly(M, tol)
210
- if res.size <= 1:
211
- z_bp = np.array([], dtype=complex)
212
- else:
213
- z_bp = np.roots(res[::-1])
214
-
215
- support = []
216
- if z_bp.size > 0:
217
- zr = z_bp[np.abs(z_bp.imag) <= real_tol].real
218
- zr = _cluster_real_points(zr, eps=1e2 * real_tol)
219
- m2 = (zr.size // 2) * 2
220
- for k in range(0, m2, 2):
221
- a0 = float(zr[k])
222
- b0 = float(zr[k + 1])
223
- if b0 > a0:
224
- support.append((a0, b0))
225
-
226
- return z_bp, a_s_zero, support