freealg 0.7.12__py3-none-any.whl → 0.7.14__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 +1 -1
- freealg/_algebraic_form/_cusp.py +357 -0
- freealg/_algebraic_form/_cusp_wrap.py +268 -0
- freealg/_algebraic_form/_decompress2.py +2 -0
- freealg/_algebraic_form/_decompress4.py +739 -0
- freealg/_algebraic_form/_decompress5.py +738 -0
- freealg/_algebraic_form/_decompress6.py +492 -0
- freealg/_algebraic_form/_decompress7.py +355 -0
- freealg/_algebraic_form/_decompress8.py +369 -0
- freealg/_algebraic_form/_decompress9.py +363 -0
- freealg/_algebraic_form/_decompress_new.py +431 -0
- freealg/_algebraic_form/_decompress_new_2.py +1631 -0
- freealg/_algebraic_form/_decompress_util.py +172 -0
- freealg/_algebraic_form/_homotopy2.py +289 -0
- freealg/_algebraic_form/_homotopy3.py +215 -0
- freealg/_algebraic_form/_homotopy4.py +320 -0
- freealg/_algebraic_form/_homotopy5.py +185 -0
- freealg/_algebraic_form/_moments.py +0 -1
- freealg/_algebraic_form/_support.py +132 -177
- freealg/_algebraic_form/algebraic_form.py +21 -2
- freealg/distributions/_compound_poisson.py +464 -0
- {freealg-0.7.12.dist-info → freealg-0.7.14.dist-info}/METADATA +1 -1
- {freealg-0.7.12.dist-info → freealg-0.7.14.dist-info}/RECORD +27 -11
- {freealg-0.7.12.dist-info → freealg-0.7.14.dist-info}/WHEEL +0 -0
- {freealg-0.7.12.dist-info → freealg-0.7.14.dist-info}/licenses/AUTHORS.txt +0 -0
- {freealg-0.7.12.dist-info → freealg-0.7.14.dist-info}/licenses/LICENSE.txt +0 -0
- {freealg-0.7.12.dist-info → freealg-0.7.14.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,431 @@
|
|
|
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
|
+
# Imports
|
|
11
|
+
# =======
|
|
12
|
+
|
|
13
|
+
import numpy
|
|
14
|
+
from ._continuation_algebraic import powers
|
|
15
|
+
|
|
16
|
+
__all__ = ['build_time_grid', 'decompress_newton']
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ===============
|
|
20
|
+
# build time grid
|
|
21
|
+
# ===============
|
|
22
|
+
|
|
23
|
+
def build_time_grid(sizes, n0, min_n_times=0):
|
|
24
|
+
"""
|
|
25
|
+
Build a monotone time grid t for sizes/n0 = exp(t).
|
|
26
|
+
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
t_all : ndarray
|
|
30
|
+
Sorted time grid (includes t=0 and all requested times).
|
|
31
|
+
idx_req : ndarray
|
|
32
|
+
Indices into t_all for the requested times (same order as sizes).
|
|
33
|
+
"""
|
|
34
|
+
sizes = numpy.asarray(sizes, dtype=float)
|
|
35
|
+
alpha = sizes / float(n0)
|
|
36
|
+
t_req = numpy.log(alpha)
|
|
37
|
+
|
|
38
|
+
T = float(numpy.max(t_req)) if t_req.size else 0.0
|
|
39
|
+
base = numpy.unique(numpy.r_[0.0, t_req, T])
|
|
40
|
+
t_all = numpy.sort(base)
|
|
41
|
+
|
|
42
|
+
N = int(min_n_times) if min_n_times is not None else 0
|
|
43
|
+
while t_all.size < N and t_all.size >= 2:
|
|
44
|
+
gaps = numpy.diff(t_all)
|
|
45
|
+
k = int(numpy.argmax(gaps))
|
|
46
|
+
mid = 0.5 * (t_all[k] + t_all[k + 1])
|
|
47
|
+
t_all = numpy.sort(numpy.unique(numpy.r_[t_all, mid]))
|
|
48
|
+
|
|
49
|
+
idx_req = numpy.searchsorted(t_all, t_req)
|
|
50
|
+
return t_all, idx_req
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ===============
|
|
54
|
+
# eval P partials
|
|
55
|
+
# ===============
|
|
56
|
+
|
|
57
|
+
def eval_P_partials(z, m, a_coeffs):
|
|
58
|
+
"""
|
|
59
|
+
Evaluate P(z,m) and partials dP/dz, dP/dm.
|
|
60
|
+
|
|
61
|
+
P is represented by a_coeffs in the monomial basis:
|
|
62
|
+
P(z,m) = sum_{j=0..s} a_j(z) m^j
|
|
63
|
+
a_j(z) = sum_{i=0..deg_z} a_coeffs[i,j] z^i
|
|
64
|
+
"""
|
|
65
|
+
z = numpy.asarray(z, dtype=complex)
|
|
66
|
+
m = numpy.asarray(m, dtype=complex)
|
|
67
|
+
|
|
68
|
+
deg_z = int(a_coeffs.shape[0] - 1)
|
|
69
|
+
s = int(a_coeffs.shape[1] - 1)
|
|
70
|
+
|
|
71
|
+
# Scalar fast path
|
|
72
|
+
if (z.ndim == 0) and (m.ndim == 0):
|
|
73
|
+
zz = complex(z)
|
|
74
|
+
mm = complex(m)
|
|
75
|
+
|
|
76
|
+
a = numpy.empty(s + 1, dtype=complex)
|
|
77
|
+
ap = numpy.empty(s + 1, dtype=complex)
|
|
78
|
+
|
|
79
|
+
for j in range(s + 1):
|
|
80
|
+
c = a_coeffs[:, j]
|
|
81
|
+
|
|
82
|
+
val = 0.0 + 0.0j
|
|
83
|
+
for i in range(deg_z, -1, -1):
|
|
84
|
+
val = val * zz + c[i]
|
|
85
|
+
a[j] = val
|
|
86
|
+
|
|
87
|
+
dval = 0.0 + 0.0j
|
|
88
|
+
for i in range(deg_z, 0, -1):
|
|
89
|
+
dval = dval * zz + (i * c[i])
|
|
90
|
+
ap[j] = dval
|
|
91
|
+
|
|
92
|
+
# Horner in m
|
|
93
|
+
p = a[s]
|
|
94
|
+
pm = 0.0 + 0.0j
|
|
95
|
+
for j in range(s - 1, -1, -1):
|
|
96
|
+
pm = pm * mm + p
|
|
97
|
+
p = p * mm + a[j]
|
|
98
|
+
|
|
99
|
+
pz = ap[s]
|
|
100
|
+
for j in range(s - 1, -1, -1):
|
|
101
|
+
pz = pz * mm + ap[j]
|
|
102
|
+
|
|
103
|
+
return p, pz, pm
|
|
104
|
+
|
|
105
|
+
# Vector path
|
|
106
|
+
shp = numpy.broadcast(z, m).shape
|
|
107
|
+
zz = numpy.broadcast_to(z, shp).ravel()
|
|
108
|
+
mm = numpy.broadcast_to(m, shp).ravel()
|
|
109
|
+
|
|
110
|
+
zp = powers(zz, deg_z)
|
|
111
|
+
mp = powers(mm, s)
|
|
112
|
+
|
|
113
|
+
dzp = numpy.zeros_like(zp)
|
|
114
|
+
for i in range(1, deg_z + 1):
|
|
115
|
+
dzp[:, i] = i * zp[:, i - 1]
|
|
116
|
+
|
|
117
|
+
P = numpy.zeros(zz.size, dtype=complex)
|
|
118
|
+
Pz = numpy.zeros(zz.size, dtype=complex)
|
|
119
|
+
Pm = numpy.zeros(zz.size, dtype=complex)
|
|
120
|
+
|
|
121
|
+
for j in range(s + 1):
|
|
122
|
+
aj = zp @ a_coeffs[:, j]
|
|
123
|
+
P += aj * mp[:, j]
|
|
124
|
+
|
|
125
|
+
ajp = dzp @ a_coeffs[:, j]
|
|
126
|
+
Pz += ajp * mp[:, j]
|
|
127
|
+
|
|
128
|
+
if j >= 1:
|
|
129
|
+
Pm += (j * aj) * mp[:, j - 1]
|
|
130
|
+
|
|
131
|
+
return P.reshape(shp), Pz.reshape(shp), Pm.reshape(shp)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# =========================
|
|
135
|
+
# Newton for one (z,t) pair
|
|
136
|
+
# =========================
|
|
137
|
+
|
|
138
|
+
def _newton_one(z, t, a_coeffs, w0, max_iter=50, tol=1e-12,
|
|
139
|
+
armijo=1e-4, min_lam=1e-6, w_min=1e-14):
|
|
140
|
+
"""
|
|
141
|
+
Solve F_t(z,w)=0 at a single (t,z) by damped Newton.
|
|
142
|
+
|
|
143
|
+
F_t(z,w) := P(z + (1 - e^{-t})/w, e^{t} w) = 0.
|
|
144
|
+
"""
|
|
145
|
+
z = complex(z)
|
|
146
|
+
w = complex(w0)
|
|
147
|
+
|
|
148
|
+
want_upper = (z.imag > 0.0)
|
|
149
|
+
|
|
150
|
+
tau = numpy.exp(float(t))
|
|
151
|
+
beta = 1.0 - 1.0 / tau # = 1 - e^{-t}
|
|
152
|
+
|
|
153
|
+
def eval_F_and_dF(w_val):
|
|
154
|
+
if (not numpy.isfinite(w_val)) or (abs(w_val) <= w_min):
|
|
155
|
+
return numpy.nan + 1j * numpy.nan, numpy.nan + 1j * numpy.nan
|
|
156
|
+
zeta = z + beta / w_val
|
|
157
|
+
y = tau * w_val
|
|
158
|
+
P, Pz, Py = eval_P_partials(zeta, y, a_coeffs)
|
|
159
|
+
dF = (-beta / (w_val * w_val)) * Pz + tau * Py
|
|
160
|
+
return P, dF
|
|
161
|
+
|
|
162
|
+
F, dF = eval_F_and_dF(w)
|
|
163
|
+
if not numpy.isfinite(F):
|
|
164
|
+
return w, False
|
|
165
|
+
|
|
166
|
+
for _ in range(int(max_iter)):
|
|
167
|
+
if abs(F) <= tol:
|
|
168
|
+
ok = numpy.isfinite(w)
|
|
169
|
+
if want_upper:
|
|
170
|
+
ok = ok and (w.imag > 0.0)
|
|
171
|
+
return w, bool(ok)
|
|
172
|
+
|
|
173
|
+
if (not numpy.isfinite(dF)) or (abs(dF) == 0.0):
|
|
174
|
+
break
|
|
175
|
+
|
|
176
|
+
step = -F / dF
|
|
177
|
+
if not numpy.isfinite(step):
|
|
178
|
+
break
|
|
179
|
+
|
|
180
|
+
lam = 1.0
|
|
181
|
+
absF = abs(F)
|
|
182
|
+
w_new = w
|
|
183
|
+
F_new = F
|
|
184
|
+
dF_new = dF
|
|
185
|
+
|
|
186
|
+
while lam >= float(min_lam):
|
|
187
|
+
cand = w + lam * step
|
|
188
|
+
|
|
189
|
+
if (not numpy.isfinite(cand)) or (abs(cand) <= w_min):
|
|
190
|
+
lam *= 0.5
|
|
191
|
+
continue
|
|
192
|
+
if want_upper and (cand.imag <= 0.0):
|
|
193
|
+
lam *= 0.5
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
Fcand, dFcand = eval_F_and_dF(cand)
|
|
197
|
+
if not numpy.isfinite(Fcand):
|
|
198
|
+
lam *= 0.5
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
if armijo is None:
|
|
202
|
+
ok_dec = (abs(Fcand) < absF)
|
|
203
|
+
else:
|
|
204
|
+
c = float(armijo)
|
|
205
|
+
ok_dec = (abs(Fcand) <= (1.0 - c * lam) * absF)
|
|
206
|
+
|
|
207
|
+
if ok_dec:
|
|
208
|
+
w_new = cand
|
|
209
|
+
F_new = Fcand
|
|
210
|
+
dF_new = dFcand
|
|
211
|
+
break
|
|
212
|
+
|
|
213
|
+
lam *= 0.5
|
|
214
|
+
|
|
215
|
+
if lam < float(min_lam):
|
|
216
|
+
break
|
|
217
|
+
|
|
218
|
+
if abs(w_new - w) <= tol * max(1.0, abs(w)):
|
|
219
|
+
w = w_new
|
|
220
|
+
F = F_new
|
|
221
|
+
ok = numpy.isfinite(w)
|
|
222
|
+
if want_upper:
|
|
223
|
+
ok = ok and (w.imag > 0.0)
|
|
224
|
+
return w, bool(ok)
|
|
225
|
+
|
|
226
|
+
w = w_new
|
|
227
|
+
F = F_new
|
|
228
|
+
dF = dF_new
|
|
229
|
+
|
|
230
|
+
ok = numpy.isfinite(w)
|
|
231
|
+
if want_upper:
|
|
232
|
+
ok = ok and (w.imag > 0.0)
|
|
233
|
+
return w, False
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# =====================
|
|
237
|
+
# Main Newton evolution
|
|
238
|
+
# =====================
|
|
239
|
+
|
|
240
|
+
def decompress_newton(
|
|
241
|
+
z_query,
|
|
242
|
+
t_all,
|
|
243
|
+
a_coeffs,
|
|
244
|
+
w0_list=None,
|
|
245
|
+
max_iter=50,
|
|
246
|
+
tol=1e-12,
|
|
247
|
+
armijo=1e-4,
|
|
248
|
+
min_lam=1e-6,
|
|
249
|
+
w_min=1e-14,
|
|
250
|
+
sweep=False,
|
|
251
|
+
R=None,
|
|
252
|
+
z_hom_steps=0,
|
|
253
|
+
homotopy_tol=None,
|
|
254
|
+
**_ignored,
|
|
255
|
+
):
|
|
256
|
+
"""
|
|
257
|
+
Evolve the physical Stieltjes branch under free decompression.
|
|
258
|
+
|
|
259
|
+
If R and z_hom_steps are provided (as in your notebook), the solver uses a
|
|
260
|
+
straight-line z-homotopy from a far anchor point z_anchor = i R to each
|
|
261
|
+
query point z, at every time slice. This is much more reliable than a sweep
|
|
262
|
+
across the real grid for multi-cut densities.
|
|
263
|
+
|
|
264
|
+
Returns
|
|
265
|
+
-------
|
|
266
|
+
W : ndarray, shape (len(t_all), len(z_query))
|
|
267
|
+
ok : ndarray, same shape, boolean
|
|
268
|
+
"""
|
|
269
|
+
zq = numpy.asarray(z_query, dtype=complex).ravel()
|
|
270
|
+
t_all = numpy.asarray(t_all, dtype=float).ravel()
|
|
271
|
+
if t_all.size == 0:
|
|
272
|
+
raise ValueError('t_all must be non-empty.')
|
|
273
|
+
|
|
274
|
+
# Sort t if needed
|
|
275
|
+
if numpy.any(numpy.diff(t_all) < 0.0):
|
|
276
|
+
order = numpy.argsort(t_all)
|
|
277
|
+
t_all = t_all[order]
|
|
278
|
+
else:
|
|
279
|
+
order = None
|
|
280
|
+
|
|
281
|
+
n_t = t_all.size
|
|
282
|
+
n_z = zq.size
|
|
283
|
+
W = numpy.empty((n_t, n_z), dtype=complex)
|
|
284
|
+
ok = numpy.zeros((n_t, n_z), dtype=bool)
|
|
285
|
+
|
|
286
|
+
if w0_list is None:
|
|
287
|
+
w_init = -1.0 / zq
|
|
288
|
+
else:
|
|
289
|
+
w_init = numpy.asarray(w0_list, dtype=complex).ravel()
|
|
290
|
+
if w_init.size != n_z:
|
|
291
|
+
raise ValueError('w0_list must have same length as z_query.')
|
|
292
|
+
|
|
293
|
+
W[0, :] = w_init
|
|
294
|
+
ok[0, :] = numpy.isfinite(w_init)
|
|
295
|
+
pos = (zq.imag > 0.0)
|
|
296
|
+
ok[0, pos] &= (W[0, pos].imag > 0.0)
|
|
297
|
+
|
|
298
|
+
use_homotopy = (R is not None) and (int(z_hom_steps) > 0)
|
|
299
|
+
if homotopy_tol is None:
|
|
300
|
+
homotopy_tol = tol
|
|
301
|
+
|
|
302
|
+
if use_homotopy:
|
|
303
|
+
Rf = float(R)
|
|
304
|
+
if not numpy.isfinite(Rf) or (Rf <= 0.0):
|
|
305
|
+
raise ValueError('R must be a positive finite number.')
|
|
306
|
+
z_anchor = 1j * Rf
|
|
307
|
+
max_dist = float(numpy.max(numpy.abs(zq - z_anchor)))
|
|
308
|
+
if max_dist == 0.0:
|
|
309
|
+
max_dist = 1.0
|
|
310
|
+
|
|
311
|
+
# Initial anchor solve at first time
|
|
312
|
+
w_anchor_prev, ok_anchor_prev = _newton_one(
|
|
313
|
+
z_anchor, float(t_all[0]), a_coeffs, -1.0 / z_anchor,
|
|
314
|
+
max_iter=max_iter, tol=homotopy_tol,
|
|
315
|
+
armijo=armijo, min_lam=min_lam, w_min=w_min)
|
|
316
|
+
if not ok_anchor_prev:
|
|
317
|
+
w_anchor_prev = -1.0 / z_anchor
|
|
318
|
+
|
|
319
|
+
def _track_from_anchor(z_target, t, w_seed):
|
|
320
|
+
z_target = complex(z_target)
|
|
321
|
+
if z_target == z_anchor:
|
|
322
|
+
return complex(w_seed), True
|
|
323
|
+
|
|
324
|
+
n_steps_max = int(z_hom_steps)
|
|
325
|
+
n_steps = int(max(
|
|
326
|
+
4,
|
|
327
|
+
numpy.ceil(n_steps_max * abs(z_target - z_anchor) / max_dist)
|
|
328
|
+
))
|
|
329
|
+
|
|
330
|
+
s = 0.0
|
|
331
|
+
ds = 1.0 / float(n_steps)
|
|
332
|
+
w_curr = complex(w_seed)
|
|
333
|
+
refine = 0
|
|
334
|
+
refine_max = 6
|
|
335
|
+
|
|
336
|
+
while s < 1.0 - 1e-15:
|
|
337
|
+
s_next = min(1.0, s + ds)
|
|
338
|
+
z_next = z_anchor + (z_target - z_anchor) * s_next
|
|
339
|
+
|
|
340
|
+
w_next, ok_next = _newton_one(
|
|
341
|
+
z_next, t, a_coeffs, w_curr,
|
|
342
|
+
max_iter=max_iter, tol=homotopy_tol,
|
|
343
|
+
armijo=armijo, min_lam=min_lam, w_min=w_min)
|
|
344
|
+
|
|
345
|
+
if ok_next:
|
|
346
|
+
w_curr = w_next
|
|
347
|
+
s = s_next
|
|
348
|
+
ds = min(ds * 1.5, 1.0 - s)
|
|
349
|
+
refine = 0
|
|
350
|
+
else:
|
|
351
|
+
ds *= 0.5
|
|
352
|
+
refine += 1
|
|
353
|
+
if refine > refine_max or ds < 1e-8:
|
|
354
|
+
return w_curr, False
|
|
355
|
+
|
|
356
|
+
return w_curr, True
|
|
357
|
+
|
|
358
|
+
start_k = 1
|
|
359
|
+
if not numpy.isclose(t_all[0], 0.0):
|
|
360
|
+
start_k = 0
|
|
361
|
+
|
|
362
|
+
for k in range(start_k, n_t):
|
|
363
|
+
t = float(t_all[k])
|
|
364
|
+
|
|
365
|
+
w_prev_time = W[k - 1, :] if k > 0 else w_init
|
|
366
|
+
Wk = numpy.empty(n_z, dtype=complex)
|
|
367
|
+
okk = numpy.zeros(n_z, dtype=bool)
|
|
368
|
+
|
|
369
|
+
if use_homotopy:
|
|
370
|
+
# Continue anchor in time
|
|
371
|
+
w_anchor, ok_anchor = _newton_one(
|
|
372
|
+
z_anchor, t, a_coeffs, w_anchor_prev,
|
|
373
|
+
max_iter=max_iter, tol=homotopy_tol,
|
|
374
|
+
armijo=armijo, min_lam=min_lam, w_min=w_min)
|
|
375
|
+
if ok_anchor:
|
|
376
|
+
w_anchor_prev = w_anchor
|
|
377
|
+
else:
|
|
378
|
+
w_anchor = w_anchor_prev
|
|
379
|
+
|
|
380
|
+
# Track each z from the anchor
|
|
381
|
+
for j in range(n_z):
|
|
382
|
+
w_h, ok_h = _track_from_anchor(zq[j], t, w_anchor)
|
|
383
|
+
if ok_h:
|
|
384
|
+
Wk[j] = w_h
|
|
385
|
+
okk[j] = True
|
|
386
|
+
continue
|
|
387
|
+
|
|
388
|
+
# Fallback: direct Newton from previous time
|
|
389
|
+
w_d, ok_d = _newton_one(
|
|
390
|
+
zq[j], t, a_coeffs, w_prev_time[j],
|
|
391
|
+
max_iter=max_iter, tol=tol,
|
|
392
|
+
armijo=armijo, min_lam=min_lam, w_min=w_min)
|
|
393
|
+
if ok_d:
|
|
394
|
+
Wk[j] = w_d
|
|
395
|
+
okk[j] = True
|
|
396
|
+
else:
|
|
397
|
+
Wk[j] = w_h
|
|
398
|
+
okk[j] = False
|
|
399
|
+
|
|
400
|
+
elif sweep:
|
|
401
|
+
for j in range(n_z):
|
|
402
|
+
if j == 0:
|
|
403
|
+
w0 = w_prev_time[j]
|
|
404
|
+
else:
|
|
405
|
+
w0 = Wk[j - 1] if okk[j - 1] else w_prev_time[j]
|
|
406
|
+
w_sol, ok_sol = _newton_one(
|
|
407
|
+
zq[j], t, a_coeffs, w0,
|
|
408
|
+
max_iter=max_iter, tol=tol,
|
|
409
|
+
armijo=armijo, min_lam=min_lam, w_min=w_min)
|
|
410
|
+
Wk[j] = w_sol
|
|
411
|
+
okk[j] = ok_sol
|
|
412
|
+
|
|
413
|
+
else:
|
|
414
|
+
for j in range(n_z):
|
|
415
|
+
w_sol, ok_sol = _newton_one(
|
|
416
|
+
zq[j], t, a_coeffs, w_prev_time[j],
|
|
417
|
+
max_iter=max_iter, tol=tol,
|
|
418
|
+
armijo=armijo, min_lam=min_lam, w_min=w_min)
|
|
419
|
+
Wk[j] = w_sol
|
|
420
|
+
okk[j] = ok_sol
|
|
421
|
+
|
|
422
|
+
W[k, :] = Wk
|
|
423
|
+
ok[k, :] = okk
|
|
424
|
+
|
|
425
|
+
if order is not None:
|
|
426
|
+
inv = numpy.empty_like(order)
|
|
427
|
+
inv[order] = numpy.arange(order.size)
|
|
428
|
+
W = W[inv, :]
|
|
429
|
+
ok = ok[inv, :]
|
|
430
|
+
|
|
431
|
+
return W, ok
|