freealg 0.7.5__py3-none-any.whl → 0.7.6__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.
freealg/__version__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.7.5"
1
+ __version__ = "0.7.6"
@@ -0,0 +1,98 @@
1
+
2
+ # SPDX-FileCopyrightText: Copyright 2025, Siavash Ameli <sameli@berkeley.edu>
3
+ # SPDX-License-Identifier: BSD-3-Clause
4
+ # SPDX-FileType: SOURCE
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify it under
7
+ # the terms of the license found in the LICENSE.txt file in the root directory
8
+ # of this source tree.
9
+
10
+
11
+ # =======
12
+ # Imports
13
+ # =======
14
+
15
+ import numpy
16
+
17
+ __all__ = ['build_moment_constraint_matrix']
18
+
19
+
20
+ # ==========
21
+ # series mul
22
+ # ==========
23
+
24
+ def _series_mul(a, b, q_max):
25
+
26
+ na = min(len(a), q_max + 1)
27
+ nb = min(len(b), q_max + 1)
28
+ out = numpy.zeros(q_max + 1, dtype=float)
29
+ for i in range(na):
30
+ if a[i] == 0.0:
31
+ continue
32
+ j_max = min(nb - 1, q_max - i)
33
+ if j_max >= 0:
34
+ out[i:i + j_max + 1] += a[i] * b[:j_max + 1]
35
+ return out
36
+
37
+
38
+ # ==========
39
+ # series pow
40
+ # ==========
41
+
42
+ def _series_pow(mser, j, q_max):
43
+ if j == 0:
44
+ out = numpy.zeros(q_max + 1, dtype=float)
45
+ out[0] = 1.0
46
+ return out
47
+ out = mser.copy()
48
+ for _ in range(1, j):
49
+ out = _series_mul(out, mser, q_max)
50
+ return out
51
+
52
+
53
+ # ===============================
54
+ # build moment constraints matrix
55
+ # ===============================
56
+
57
+ def build_moment_constraint_matrix(pairs, deg_z, s, mu):
58
+
59
+ mu = numpy.asarray(mu, dtype=float).ravel()
60
+ if mu.size == 0:
61
+ return numpy.zeros((0, len(pairs)), dtype=float)
62
+
63
+ # m(z) = -sum_{p>=0} mu_p / z^{p+1}; t = 1/z so m(t) = -sum mu_p t^{p+1}
64
+ r = mu.size - 1
65
+ q_max = r
66
+
67
+ mser = numpy.zeros(q_max + 1, dtype=float)
68
+ for p in range(mu.size):
69
+ q = p + 1
70
+ if q <= q_max:
71
+ mser[q] = -float(mu[p])
72
+
73
+ # Precompute (m(t))^j coefficients up to t^{q_max}
74
+ mpow = []
75
+ for j in range(s + 1):
76
+ mpow.append(_series_pow(mser, j, q_max))
77
+
78
+ # Constraints: coeff of t^q in Q(t) := t^{deg_z} P(1/t, m(t)) must be 0
79
+ # Q(t) = sum_{i,j} c_{i,j} * t^{deg_z - i} * (m(t))^j
80
+ n_coef = len(pairs)
81
+ B = numpy.zeros((q_max + 1, n_coef), dtype=float)
82
+
83
+ for k, (i, j) in enumerate(pairs):
84
+ shift = deg_z - i
85
+ if shift < 0:
86
+ continue
87
+ mj = mpow[j]
88
+ for q in range(q_max + 1):
89
+ qq = q - shift
90
+ if 0 <= qq <= q_max:
91
+ B[q, k] = mj[qq]
92
+
93
+ # Drop all-zero rows (can happen if index-set can't support higher moments)
94
+ row_norm = numpy.linalg.norm(B, axis=1)
95
+ keep = row_norm > 0.0
96
+ B = B[keep, :]
97
+
98
+ return B
@@ -13,6 +13,7 @@
13
13
 
14
14
  import numpy
15
15
  from .._geometric_form._continuation_genus0 import joukowski_z
16
+ from ._constraints import build_moment_constraint_matrix
16
17
 
17
18
  __all__ = ['sample_z_joukowski', 'filter_z_away_from_cuts', 'powers',
18
19
  'fit_polynomial_relation', 'sanity_check_stieltjes_branch',
@@ -131,7 +132,11 @@ def powers(x, deg):
131
132
  # =======================
132
133
 
133
134
  def fit_polynomial_relation(z, m, s, deg_z, ridge_lambda=0.0, weights=None,
134
- triangular=None, normalize=False):
135
+ triangular=None, normalize=False,
136
+ mu=None, mu_reg=None):
137
+ """
138
+ Fits polynomial P(z, m) = 0 with samples from the physical branch.
139
+ """
135
140
 
136
141
  z = numpy.asarray(z, dtype=complex).ravel()
137
142
  m = numpy.asarray(m, dtype=complex).ravel()
@@ -198,13 +203,81 @@ def fit_polynomial_relation(z, m, s, deg_z, ridge_lambda=0.0, weights=None,
198
203
  s_col[s_col == 0.0] = 1.0
199
204
  As = Ar / s_col[None, :]
200
205
 
201
- if ridge_lambda > 0.0:
202
- L = numpy.sqrt(ridge_lambda) * numpy.eye(n_coef, dtype=float)
203
- As = numpy.vstack([As, L])
206
+ # Optional moment constraints B c = 0 (hard via nullspace, soft via
207
+ # weighted rows)
208
+ if mu is not None:
209
+ B = build_moment_constraint_matrix(pairs, deg_z, s, mu)
210
+ if B.shape[0] > 0:
211
+ Bs = B / s_col[None, :]
212
+
213
+ if mu_reg is None:
214
+ # Hard constraints: solve in nullspace of Bs
215
+ uB, sB, vhB = numpy.linalg.svd(Bs, full_matrices=True)
216
+ tolB = 1e-12 * (sB[0] if sB.size else 1.0)
217
+ rankB = int(numpy.sum(sB > tolB))
218
+ if rankB >= n_coef:
219
+ raise RuntimeError(
220
+ 'Moment constraints leave no feasible coefficients.')
221
+
222
+ N = vhB[rankB:, :].T # (n_coef, n_free)
223
+ AN = As @ N
224
+
225
+ if ridge_lambda > 0.0:
226
+ L = numpy.sqrt(ridge_lambda) * numpy.eye(N.shape[1],
227
+ dtype=float)
228
+ AN = numpy.vstack([AN, L])
229
+
230
+ _, _, vhN = numpy.linalg.svd(AN, full_matrices=False)
231
+ y = vhN[-1, :]
232
+ coef_scaled = N @ y
233
+
234
+ coef = coef_scaled / s_col
235
+
236
+ else:
237
+ mu_reg = float(mu_reg)
238
+ if mu_reg > 0.0:
239
+ As_aug = As
240
+ Bs_w = numpy.sqrt(mu_reg) * Bs
241
+ As_aug = numpy.vstack([As_aug, Bs_w])
242
+
243
+ if ridge_lambda > 0.0:
244
+ L = numpy.sqrt(ridge_lambda) * numpy.eye(n_coef,
245
+ dtype=float)
246
+ As_aug = numpy.vstack([As_aug, L])
247
+
248
+ _, _, vh = numpy.linalg.svd(As_aug, full_matrices=False)
249
+ coef_scaled = vh[-1, :]
250
+ coef = coef_scaled / s_col
251
+ else:
252
+ # mu_reg == 0 => ignore constraints
253
+ if ridge_lambda > 0.0:
254
+ L = numpy.sqrt(ridge_lambda) * numpy.eye(n_coef,
255
+ dtype=float)
256
+ As = numpy.vstack([As, L])
257
+
258
+ _, _, vh = numpy.linalg.svd(As, full_matrices=False)
259
+ coef_scaled = vh[-1, :]
260
+ coef = coef_scaled / s_col
204
261
 
205
- _, _, vh = numpy.linalg.svd(As, full_matrices=False)
206
- coef_scaled = vh[-1, :]
207
- coef = coef_scaled / s_col
262
+ else:
263
+ # B has no effective rows -> proceed unconstrained
264
+ if ridge_lambda > 0.0:
265
+ L = numpy.sqrt(ridge_lambda) * numpy.eye(n_coef, dtype=float)
266
+ As = numpy.vstack([As, L])
267
+
268
+ _, _, vh = numpy.linalg.svd(As, full_matrices=False)
269
+ coef_scaled = vh[-1, :]
270
+ coef = coef_scaled / s_col
271
+
272
+ else:
273
+ # No moment constraints
274
+ if ridge_lambda > 0.0:
275
+ L = numpy.sqrt(ridge_lambda) * numpy.eye(n_coef, dtype=float)
276
+ As = numpy.vstack([As, L])
277
+
278
+ _, _, vh = numpy.linalg.svd(As, full_matrices=False)
279
+ coef_scaled = vh[-1, :]
280
+ coef = coef_scaled / s_col
208
281
 
209
282
  full = numpy.zeros((deg_z + 1, s + 1), dtype=complex)
210
283
  for k, (i, j) in enumerate(pairs):
@@ -194,6 +194,7 @@ class AlgebraicForm(object):
194
194
 
195
195
  # Initialize
196
196
  self.a_coeffs = None # Polynomial coefficients
197
+ self.status = None # Fitting status
197
198
  self.cache = {} # Cache inner-computations
198
199
 
199
200
  # ===
@@ -207,10 +208,38 @@ class AlgebraicForm(object):
207
208
  y_eps=2e-2,
208
209
  x_pad=0.0,
209
210
  triangular=None,
211
+ mu=None,
212
+ mu_reg=None,
210
213
  normalize=False,
211
214
  verbose=False):
212
215
  """
213
- Fits polynomial.
216
+ Fit polynomial.
217
+
218
+ Parameters
219
+ ----------
220
+
221
+ deg_m : int
222
+ Degree :math:`\\deg_m(P)`
223
+
224
+ deg_z : int
225
+ Degree :math:`\\deg_z(P)`
226
+
227
+ mu : array_like, default=None
228
+ If an array :math:`[\\mu_0, \\mu_`, \\dots, \\mu_r]` is given,
229
+ it enforces the first :math:`r+1` moments. Note that :math:`\\mu_0`
230
+ should be :math:`1` to ensure unit mass. See also ``mu_reg`.
231
+
232
+ mu_reg: float, default=None
233
+ If `None`, the constraints ``mu`` are applied as hard constraint.
234
+ If a positive number, the constraints are applied as a soft
235
+ constraints with regularisation ``mu_reg``.
236
+
237
+ Notes
238
+ -----
239
+
240
+ When the input data are from an exact model, hard moment constraint is
241
+ preferred over soft constraint as the latter can hurt an already a good
242
+ fit.
214
243
  """
215
244
 
216
245
  # Very important: reset cache whenever this function is called. This
@@ -231,11 +260,13 @@ class AlgebraicForm(object):
231
260
  z_fit = filter_z_away_from_cuts(z_fit, self.support, y_eps=y_eps,
232
261
  x_pad=x_pad)
233
262
 
263
+ # Fitting (w_inf = None means adaptive weight selection)
234
264
  m1_fit = self.stieltjes(z_fit)
235
265
  a_coeffs = fit_polynomial_relation(z_fit, m1_fit, s=deg_m, deg_z=deg_z,
236
266
  ridge_lambda=reg,
237
267
  triangular=triangular,
238
- normalize=normalize)
268
+ normalize=normalize, mu=mu,
269
+ mu_reg=mu_reg)
239
270
 
240
271
  self.a_coeffs = a_coeffs
241
272
 
@@ -251,8 +282,9 @@ class AlgebraicForm(object):
251
282
  eta=max(y_eps, 1e-2), n_x=128,
252
283
  max_bad_frac=0.05)
253
284
 
254
- status['res_max'] = res_max
255
- status['res_99_9'] = res_99_9
285
+ status['res_max'] = float(res_max)
286
+ status['res_99_9'] = float(res_99_9)
287
+ self.status = status
256
288
 
257
289
  if verbose:
258
290
  print(f'fit residual max : {res_max:>0.4e}')
@@ -657,11 +689,16 @@ class AlgebraicForm(object):
657
689
  z_query = x + 1j * self.delta
658
690
 
659
691
  # Initial condition at t=0 (physical branch)
660
- w0_list = self.stieltjes(z_query)
692
+ # w0_list = self.stieltjes(z_query)
693
+ stieltjes = numpy.vectorize(m_fn)
694
+ w0_list = stieltjes(z_query)
661
695
 
662
696
  # Times
663
697
  t = numpy.log(alpha)
664
698
 
699
+ # Ensure it starts from t = 0
700
+ t = numpy.concatenate([numpy.zeros(1), t])
701
+
665
702
  # Evolve
666
703
  W, ok = decompress_newton(
667
704
  z_query, t, self.a_coeffs,
@@ -669,6 +706,9 @@ class AlgebraicForm(object):
669
706
 
670
707
  rho = W.imag / numpy.pi
671
708
 
709
+ # Remove time zero
710
+ rho = rho[1:, :]
711
+
672
712
  if verbose:
673
713
  print("success rate per t:", ok.mean(axis=1))
674
714
 
freealg/_util.py CHANGED
@@ -14,7 +14,7 @@
14
14
  import numpy
15
15
  import scipy
16
16
 
17
- __all__ = ['resolve_complex_dtype', 'compute_eig']
17
+ __all__ = ['resolve_complex_dtype', 'compute_eig', 'subsample_matrix']
18
18
 
19
19
 
20
20
  # =====================
@@ -70,3 +70,26 @@ def compute_eig(A, lower=False):
70
70
  eig = scipy.linalg.eigvalsh(A, lower=lower, driver='ev')
71
71
 
72
72
  return eig
73
+
74
+
75
+ # ================
76
+ # subsample matrix
77
+ # ================
78
+
79
+ def subsample_matrix(matrix, submatrix_size, seed=None):
80
+ """
81
+ Generate a random subsample of a larger matrix
82
+ """
83
+
84
+ if matrix.shape[0] != matrix.shape[1]:
85
+ raise ValueError("Matrix must be square")
86
+
87
+ n = matrix.shape[0]
88
+ if submatrix_size > n:
89
+ raise ValueError("Submatrix size cannot exceed matrix size")
90
+
91
+ rng = numpy.random.default_rng(seed)
92
+ idx = rng.choice(n, size=submatrix_size, replace=False)
93
+ idx = numpy.sort(idx) # optional, preserves original ordering
94
+
95
+ return matrix[numpy.ix_(idx, idx)]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: freealg
3
- Version: 0.7.5
3
+ Version: 0.7.6
4
4
  Summary: Free probability for large matrices
5
5
  Home-page: https://github.com/ameli/freealg
6
6
  Download-URL: https://github.com/ameli/freealg/archive/main.zip
@@ -1,14 +1,15 @@
1
1
  freealg/__init__.py,sha256=SjcYb6HWmaclnnM-m1eC1honZRyfNBWYDYBx23kSdjo,833
2
- freealg/__version__.py,sha256=6qL_qyowXO9Pc6v11Zx2s-yd28_548ZZC-OsfzO_Pjc,22
3
- freealg/_util.py,sha256=E254DRTCeST7h1QHrfLeyg9jQntRaOyui1oXD7nMaWQ,1890
2
+ freealg/__version__.py,sha256=wu65dmVM9fKR1rBHH263ls8Ca2FZzb0ejYcrP_Ld0iY,22
3
+ freealg/_util.py,sha256=RzccUCORgzrI9NdNqwMVugiHU0uDKkJFcIyjFMUOnv8,2518
4
4
  freealg/_algebraic_form/__init__.py,sha256=MIB_jVgw2qI-JW_ypqaFSeNAB6c4GvpjNySnap_a6hg,398
5
- freealg/_algebraic_form/_continuation_algebraic.py,sha256=eQqc6f_fvW4-lVox8pFHcMu32ImYDESdQk9gOm1NElc,16147
5
+ freealg/_algebraic_form/_constraints.py,sha256=37U7nvtCTocuS7l_nfUznkPi195PY7eXFzeiikrv3B0,2448
6
+ freealg/_algebraic_form/_continuation_algebraic.py,sha256=SyuWjw0jABlIst9RjHS13gG2M94KZwwrTa3eeJNfmRI,19098
6
7
  freealg/_algebraic_form/_decompress.py,sha256=gGtixLOVxlMy5S-NsXgoA7lIrB7u7nUZImQk1mIDo3s,21101
7
8
  freealg/_algebraic_form/_decompress2.py,sha256=Ng9w9xmGe9M-DApp35IeNeQlvszfzT4NZx5BQn0lQ3I,2459
8
9
  freealg/_algebraic_form/_edge.py,sha256=7l9QyLJDxaEY4WB6MCUFtfEZSf04wyHwH7YPHFJXSbM,10690
9
10
  freealg/_algebraic_form/_homotopy.py,sha256=2oMcqJ2VJGzG7WKGM6FUS3923GT8Adtq_hLPEGgzqoU,3990
10
11
  freealg/_algebraic_form/_sheets_util.py,sha256=6OLzWQKu-gN8rxM2rbpbN8TjNZFmD8UJ-8t9kcZdkCo,4174
11
- freealg/_algebraic_form/algebraic_form.py,sha256=MrbZbu8IU_zgWSTvj4t57Npp7ToaJU1fH9bRx8_TEGs,31311
12
+ freealg/_algebraic_form/algebraic_form.py,sha256=N-R8cv7580p-iPW7oPlDZ9py7BVjEmPcCjs25UVtNV4,32706
12
13
  freealg/_free_form/__init__.py,sha256=5cnSX7kHci3wKx6-BEFhmVY_NjjmQAq1JjWPTEqETTg,611
13
14
  freealg/_free_form/_chebyshev.py,sha256=zkyVA8NLf7uUKlJdLz4ijd_SurdsqUgkA5nHGWSybaE,6916
14
15
  freealg/_free_form/_damp.py,sha256=k2vtBtWOxQBf4qXaWu_En81lQBXbEO4QbxxWpvuVhdE,1802
@@ -41,9 +42,9 @@ freealg/distributions/_wigner.py,sha256=epgx6ne6R_7to5j6-QsWIAVFJQFquWMmYgnZYMN4
41
42
  freealg/visualization/__init__.py,sha256=NLq_zwueF7ytZ8sl8zLPqm-AODxxXNvfMozHGmmklcE,435
42
43
  freealg/visualization/_glue_util.py,sha256=2oKnEYjUOS4OZfivmciVLauVr53kyHMwi6c2zRKilTQ,693
43
44
  freealg/visualization/_rgb_hsv.py,sha256=rEskxXxSlKKxIrHRslVkgxHtD010L3ge9YtcVsOPl8E,3650
44
- freealg-0.7.5.dist-info/licenses/AUTHORS.txt,sha256=0b67Nz4_JgIzUupHJTAZxu5QdSUM_HRM_X_w4xCb17o,30
45
- freealg-0.7.5.dist-info/licenses/LICENSE.txt,sha256=J-EEYEtxb3VVf_Bn1TYfWnpY5lMFIM15iLDDcnaDTPA,1443
46
- freealg-0.7.5.dist-info/METADATA,sha256=s_6RoaIXAYy3JOmaDl-g-bTB81LH5vk3ptpfPQfXqd8,5516
47
- freealg-0.7.5.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
48
- freealg-0.7.5.dist-info/top_level.txt,sha256=eR2wrgYwDdnnJ9Zf5PruPqe4kQav0GMvRsqct6y00Q8,8
49
- freealg-0.7.5.dist-info/RECORD,,
45
+ freealg-0.7.6.dist-info/licenses/AUTHORS.txt,sha256=0b67Nz4_JgIzUupHJTAZxu5QdSUM_HRM_X_w4xCb17o,30
46
+ freealg-0.7.6.dist-info/licenses/LICENSE.txt,sha256=J-EEYEtxb3VVf_Bn1TYfWnpY5lMFIM15iLDDcnaDTPA,1443
47
+ freealg-0.7.6.dist-info/METADATA,sha256=cifScuAeI6gPbjzzCzBMMLqTYtxbqIIJhuICSIi-BkE,5516
48
+ freealg-0.7.6.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
49
+ freealg-0.7.6.dist-info/top_level.txt,sha256=eR2wrgYwDdnnJ9Zf5PruPqe4kQav0GMvRsqct6y00Q8,8
50
+ freealg-0.7.6.dist-info/RECORD,,