sodetlib 0.6.1rc1__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.
@@ -0,0 +1,579 @@
1
+ """
2
+ This is a module for a more accurate estimation of TES parameters than what
3
+ currently exists in the IV and Bias Step Modules.
4
+
5
+ It contains the follwing procedures:
6
+ - recompute_ivpars: corrects errors in IV analysis
7
+ - run_correction: Takes IV and bias step data to compute corrected TES params
8
+ based on the RP fitting method described here:
9
+ https://simonsobs.atlassian.net/wiki/spaces/~5570586d07625a6be74c8780e4b96f6156f5e6/blog/2024/02/02/286228683/Nonlinear+TES+model+using+RP+curve
10
+
11
+ Authors: Remy Gerras, Satoru Takakura, Jack Lashner
12
+ """
13
+
14
+ import numpy as np
15
+ from scipy.interpolate import interp1d
16
+ from scipy.optimize import minimize
17
+ from tqdm.auto import trange
18
+ import multiprocessing
19
+ from concurrent.futures import ProcessPoolExecutor, as_completed, Future
20
+ from typing import Optional, List, Tuple, TypeVar
21
+ from dataclasses import dataclass
22
+ import traceback
23
+ from copy import deepcopy
24
+ from numpy.typing import NDArray
25
+
26
+ from sodetlib.operations.iv import IVAnalysis
27
+ from sodetlib.operations.bias_steps import BiasStepAnalysis
28
+
29
+ D = TypeVar("D", float, np.ndarray)
30
+
31
+
32
+ def model_logalpha(r: D, logp0=0.0, p1=7.0, p2=1.0, logp3=0.0) -> D:
33
+ """
34
+ Fits Alpha' parameter, or alpha / GT. P0 has units of [uW]^-1.
35
+ """
36
+ return logp0 + p1 * r - p2 * r ** np.exp(logp3) * np.log(1 - r)
37
+
38
+
39
+ @dataclass
40
+ class AnalysisCfg:
41
+ """
42
+ Class for configuring behavior of the parameter estimation functions
43
+ """
44
+
45
+ default_nprocs: int = 10
46
+ "Default number of processes to use when running correction in parallel."
47
+
48
+ raise_exceptions: bool = False
49
+ "If true, will raise exceptions thrown in the run_correction function."
50
+
51
+ psat_level: float = 0.9
52
+ "rfrac for which psat is defined."
53
+
54
+ rpfit_min_rfrac: float = 0.05
55
+ "Min rfrac used for RP fits and estimation."
56
+
57
+ rpfit_max_rfrac: float = 0.95
58
+ "Max rfrac used for RP fits and estiamtion."
59
+
60
+ rpfit_intg_nsamps: int = 500
61
+ "Number of samples to use in the RP fit and integral."
62
+
63
+ sc_rfrac_thresh: float = 0.02
64
+ "Bias step rfrac threshold below which TES should be considered "
65
+ "superconducting, and the correction skipped."
66
+
67
+ show_pb: bool = True
68
+ "If true, will show a pb when running on all channels together"
69
+
70
+ def __post_init__(self):
71
+ if self.rpfit_min_rfrac < 0.0001:
72
+ raise ValueError("rp_fit_min_rfrac must be larger than 0.0001")
73
+ if self.rpfit_max_rfrac > 0.9999:
74
+ raise ValueError("rp_fit_max_rfrac must be smaller than 0.9999")
75
+
76
+
77
+ @dataclass
78
+ class RpFitChanData:
79
+ """
80
+ Channel data needed to perform RP correction, pulled from bias step and IV
81
+ calibration functions.
82
+
83
+ Args
84
+ -------
85
+ bg: int
86
+ Bias Line
87
+ Rsh: float
88
+ Shunt Resistance [Ohms]
89
+ band: int
90
+ Readout band
91
+ channel :int
92
+ Readout channel
93
+ iv_idx_sc: int
94
+ Index in IV data for SC transition
95
+ iv_resp: np.ndarray
96
+ TES current response relative from IV.
97
+ iv_R: np.ndarray
98
+ TES resistance [Ohms] through IV.
99
+ iv_p_tes: np.ndarray
100
+ TES Bias power [pW] through IV.
101
+ iv_Rn: float
102
+ TES normal resistance, measured from IV.
103
+ iv_ibias: np.ndarray
104
+ TES bias current [uA] throughout IV.
105
+ iv_si: np.ndarray
106
+ TES responsivity [1/uV] throughout IV.
107
+ bs_meas_dIrat: float
108
+ TES measured dIdrat from bias steps.
109
+ bs_Rtes: float
110
+ TES resistance [Ohm] measured from bias steps.
111
+ bs_Ibias: float
112
+ TES bias current [uA] measured from bias steps.
113
+ bs_Si: float
114
+ TES responsivity [1/uV] measured from bias steps.
115
+ """
116
+
117
+ bg: int
118
+ Rsh: float
119
+
120
+ band: int
121
+ channel: int
122
+
123
+ iv_idx_sc: int
124
+ iv_resp: np.ndarray
125
+ iv_R: np.ndarray # Ohm
126
+ iv_p_tes: np.ndarray # pW
127
+ iv_Rn: float
128
+ iv_ibias: np.ndarray # uA
129
+ iv_si: np.ndarray # 1/uV
130
+
131
+ bs_meas_dIrat: float
132
+ bs_Rtes: float = np.nan # Ohm
133
+ bs_Ibias: float = np.nan # uA
134
+ bs_Si: float = np.nan # 1/uV
135
+
136
+ @classmethod
137
+ def from_data(
138
+ cls, iva: IVAnalysis, bsa: BiasStepAnalysis, band: int, channel: int
139
+ ) -> "RpFitChanData":
140
+ """Create class from IV and BSA dicts"""
141
+ iv_idx = np.where((iva.channels == channel) & (iva.bands == band))[0]
142
+ if len(iv_idx) == 0:
143
+ raise ValueError(
144
+ f"Could not find band={band} channel={channel} in IVAnalysis"
145
+ )
146
+ iv_idx = iv_idx[0]
147
+
148
+ bs_idx = np.where((bsa.channels == channel) & (bsa.bands == band))[0]
149
+ if len(bs_idx) == 0:
150
+ raise ValueError(
151
+ f"Could not find band={band} channel={channel} in Bias Steps"
152
+ )
153
+ bs_idx = bs_idx[0]
154
+
155
+ bg = iva.bgmap[iv_idx]
156
+
157
+ return cls(
158
+ bg=bg,
159
+ band=band,
160
+ channel=channel,
161
+ Rsh=iva.meta["R_sh"],
162
+ iv_idx_sc=iva.idxs[iv_idx, 0],
163
+ iv_resp=iva.resp[iv_idx],
164
+ iv_R=iva.R[iv_idx],
165
+ iv_Rn=iva.R_n[iv_idx],
166
+ iv_p_tes=iva.p_tes[iv_idx] * 1e12,
167
+ iv_si=iva.si[iv_idx] * 1e-6,
168
+ iv_ibias=iva.i_bias * 1e6,
169
+ bs_meas_dIrat=bsa.dItes[bs_idx] / bsa.dIbias[bg],
170
+ bs_Rtes=bsa.R0[bs_idx],
171
+ bs_Ibias=bsa.Ibias[bg] * 1e6,
172
+ bs_Si=bsa.Si[bs_idx] * 1e-6,
173
+ )
174
+
175
+
176
+ @dataclass
177
+ class CorrectedParams:
178
+ "Log(alpha') fit popts"
179
+
180
+ logalpha_popts: np.ndarray
181
+ "Log alpha fit parameters"
182
+ pbias_model_offset: float
183
+ "delta Popt between IV and bias steps"
184
+ delta_Popt: float
185
+ "TES Current [uA]"
186
+ corrected_I0: float
187
+ "TES resistance [Ohm]"
188
+ corrected_R0: float
189
+ "Bias power [aW]"
190
+ corrected_Pj: float
191
+ "Responsivity [1/uV]"
192
+ corrected_Si: float
193
+ "Loopgain at bias current"
194
+ loopgain: float
195
+
196
+
197
+ @dataclass
198
+ class CorrectionResults:
199
+ """
200
+ Results from the parameter correction procedure
201
+ """
202
+
203
+ chdata: RpFitChanData
204
+ "Channel data used for correction"
205
+
206
+ success: bool = False
207
+ "If correction finished successfully"
208
+
209
+ traceback: Optional[str] = None
210
+ "Traceback on failure"
211
+
212
+ corrected_params: Optional[CorrectedParams] = None
213
+ "Model popts and corrected TES parameters"
214
+
215
+
216
+ def find_logalpha_popts(
217
+ chdata: RpFitChanData, cfg: AnalysisCfg
218
+ ) -> Tuple[np.ndarray, float]:
219
+ """
220
+ Fit Log(alpha') to IV data.
221
+
222
+ Returns
223
+ ---------
224
+ popts: np.ndarray
225
+ Optimal parameters for log(alpha') fit.
226
+ pbias_model_offset: float
227
+ Offset between pbias_model from alpha' integration and data [aW]
228
+ """
229
+ smooth_dist = 5
230
+ w_len = 2 * smooth_dist + 1
231
+ w = (1.0 / float(w_len)) * np.ones(w_len) # window
232
+
233
+ rfrac = chdata.iv_R / chdata.iv_Rn
234
+ mask = np.ones_like(rfrac, dtype=bool)
235
+ mask[: chdata.iv_idx_sc + w_len + 2] = False # Try to cut out SC branch + smoothing
236
+ mask &= (rfrac > cfg.rpfit_min_rfrac) * (rfrac < cfg.rpfit_max_rfrac)
237
+ rfrac = np.convolve(rfrac, w, mode="same")
238
+ pbias = np.convolve(chdata.iv_p_tes, w, mode="same")
239
+
240
+ rfrac = rfrac[mask]
241
+ pbias = pbias[mask]
242
+ drfrac = np.diff(rfrac, prepend=np.nan)
243
+ dpbias = np.diff(pbias, prepend=np.nan)
244
+ logalpha = np.log(1 / rfrac * drfrac / dpbias)
245
+ logalpha_mask = np.isfinite(logalpha)
246
+
247
+ def logalpha_fit_func(logalpha_pars):
248
+ model = model_logalpha(rfrac, *logalpha_pars)
249
+ chi2 = np.sum((logalpha[logalpha_mask] - model[logalpha_mask]) ** 2)
250
+ return chi2
251
+
252
+ fitres = minimize(logalpha_fit_func, [4, 10, 1, 1])
253
+ logalpha_popts = fitres.x
254
+
255
+ logalpha_model = model_logalpha(rfrac, *fitres.x)
256
+
257
+ pbias_model = np.nancumsum(drfrac / (rfrac * np.exp(logalpha_model)))
258
+
259
+ def pbias_offset_fit_func(pbias_offset):
260
+ return np.sum((pbias_model + pbias_offset - pbias) ** 2)
261
+
262
+ pbias_model_offset = minimize(pbias_offset_fit_func, pbias[0]).x[0]
263
+
264
+ return logalpha_popts, pbias_model_offset
265
+
266
+
267
+ def run_correction(chdata: RpFitChanData, cfg: AnalysisCfg) -> CorrectionResults:
268
+ """
269
+ Runs TES param correction procedure.
270
+
271
+ Args
272
+ ----
273
+ chdata: RpFitChanData
274
+ IV and Bias Step calibration data
275
+ cfg: AnalysisCfg
276
+ Correction analysis configuration object.
277
+ """
278
+ res = CorrectionResults(chdata)
279
+ try:
280
+ if np.isnan(chdata.iv_Rn):
281
+ raise ValueError("IV Rn is NaN")
282
+
283
+ # Find logalpha popts
284
+ logalpha_popts, pbias_model_offset = find_logalpha_popts(chdata, cfg)
285
+
286
+ if np.abs(chdata.bs_Rtes / chdata.iv_Rn) < cfg.sc_rfrac_thresh:
287
+ # Cannot perform correction, since detectors are SC
288
+ res.corrected_params = CorrectedParams(
289
+ logalpha_popts=logalpha_popts,
290
+ pbias_model_offset=pbias_model_offset,
291
+ delta_Popt=np.nan,
292
+ corrected_I0=0.0,
293
+ corrected_R0=0.0,
294
+ corrected_Pj=0.0,
295
+ corrected_Si=np.nan,
296
+ loopgain=np.nan,
297
+ )
298
+ res.success = True
299
+ return res
300
+
301
+ # compute dPopt by minimizing (dIrat_IV - dIrat_BS) at chosen bias voltage with respect to delta_popt
302
+ dIrat_meas = chdata.bs_meas_dIrat
303
+ Rsh = chdata.Rsh
304
+ Ibias_setpoint = chdata.bs_Ibias
305
+ rfrac = np.linspace(
306
+ cfg.rpfit_min_rfrac, cfg.rpfit_max_rfrac, cfg.rpfit_intg_nsamps
307
+ )
308
+ dr = rfrac[1] - rfrac[0]
309
+ logalpha_model = model_logalpha(rfrac, *logalpha_popts)
310
+ alpha = np.exp(logalpha_model)
311
+ R = rfrac * chdata.iv_Rn
312
+ pbias = np.cumsum(dr / (rfrac * np.exp(logalpha_model))) + pbias_model_offset
313
+
314
+ def dIrat_IV(dPopt):
315
+ P_ = pbias - dPopt
316
+ L0_ = alpha * P_
317
+ i_tes_ = np.sqrt(P_ / R)
318
+ i_bias_ = i_tes_ * (R + Rsh) / Rsh
319
+ irat = (1.0 - L0_) / ((1.0 - L0_) + (1.0 + L0_) * R / Rsh)
320
+ return np.interp(Ibias_setpoint, i_bias_, irat)
321
+
322
+ def fit_func(dPopt):
323
+ diff = (dIrat_IV(dPopt) - dIrat_meas) ** 2
324
+ return diff if not np.isnan(diff) else np.inf
325
+
326
+ dPopt_fitres = minimize(fit_func, [0])
327
+ if not dPopt_fitres.success:
328
+ raise RuntimeError("dPopt fit failed")
329
+
330
+ dPopt = dPopt_fitres.x[0]
331
+
332
+ # Adjust IV parameter curves with delta_Popt, and find params at Ibias setpoint
333
+ pbias -= dPopt
334
+ ok = (pbias > 0.0) * (R > Rsh)
335
+ pbias = pbias[ok]
336
+ R = R[ok]
337
+ L0 = alpha[ok] * pbias
338
+ L = (R - Rsh) / (R + Rsh) * L0
339
+ Ites = np.sqrt(pbias / R)
340
+ Ibias = Ites * (R + Rsh) / Rsh
341
+ Si = -1 / (Ites * (R - Rsh)) * (L / (L + 1))
342
+
343
+ res.corrected_params = CorrectedParams(
344
+ logalpha_popts=logalpha_popts,
345
+ pbias_model_offset=pbias_model_offset,
346
+ delta_Popt=dPopt,
347
+ corrected_I0=np.interp(Ibias_setpoint, Ibias, Ites).item(),
348
+ corrected_R0=np.interp(Ibias_setpoint, Ibias, R).item(),
349
+ corrected_Pj=np.interp(Ibias_setpoint, Ibias, pbias).item(),
350
+ corrected_Si=np.interp(Ibias_setpoint, Ibias, Si).item(),
351
+ loopgain=np.interp(Ibias_setpoint, Ibias, L0).item(),
352
+ )
353
+ res.success = True
354
+ except Exception:
355
+ if cfg.raise_exceptions:
356
+ raise
357
+ else:
358
+ res.traceback = traceback.format_exc()
359
+ res.success = False
360
+
361
+ return res
362
+
363
+
364
+ def run_corrections_parallel(
365
+ iva: IVAnalysis, bsa: BiasStepAnalysis, cfg: AnalysisCfg, nprocs=None,
366
+ executor=None, as_completed_callable=None) -> List[CorrectionResults]:
367
+ """
368
+ Runs correction procedure in parallel for all channels in IV and BSA object
369
+ """
370
+
371
+ nchans = iva.nchans
372
+ pb = trange(nchans, disable=(not cfg.show_pb))
373
+ results = []
374
+
375
+ if nprocs is None:
376
+ nprocs = cfg.default_nprocs
377
+
378
+ # Error handler
379
+ def errback(e):
380
+ raise e
381
+
382
+ # Create executor (optionally externally provided)
383
+ if executor is None:
384
+ executor = ProcessPoolExecutor(max_workers=nprocs)
385
+ as_completed_callable = as_completed
386
+ close_executor = True
387
+ else:
388
+ close_executor = False
389
+
390
+ try:
391
+ futures = []
392
+ for idx in range(nchans):
393
+ chdata = RpFitChanData.from_data(
394
+ iva, bsa, iva.bands[idx], iva.channels[idx]
395
+ )
396
+ future = executor.submit(run_correction, chdata, cfg)
397
+ futures.append(future)
398
+
399
+ for future in as_completed_callable(futures):
400
+ try:
401
+ res = future.result()
402
+ results.append(res)
403
+ except Exception as e:
404
+ errback(e)
405
+
406
+ futures.remove(future)
407
+ pb.update(1)
408
+
409
+ pb.close()
410
+
411
+ finally:
412
+ if close_executor:
413
+ executor.shutdown(wait=True, cancel_futures=True)
414
+
415
+ return results
416
+
417
+ def compute_psats(iva: IVAnalysis, cfg: AnalysisCfg) -> Tuple[np.ndarray, np.ndarray]:
418
+ """
419
+ Re-computes Psat for an IVAnalysis object. Will save results to iva.p_sat.
420
+ This assumes i_tes, v_tes, and r_tes have already been calculated. This will
421
+ not modify the original IVAnalysis object.
422
+
423
+ Args
424
+ ----
425
+ iva2 : Dictionary
426
+ Dictionary built from original IV Analysis .npy file
427
+ psat_level : float
428
+ R_frac level for which Psat is defined. If 0.9, Psat will be the
429
+ power on the TES when R_frac = 0.9.
430
+
431
+ Returns
432
+ -------
433
+ p_sat : np.ndarray
434
+ Array of length (nchan) with the p-sat computed for each channel (W)
435
+ psat_cross_idx : np.ndarray
436
+ Array of indices at which the psat level is crossed for each channel
437
+ """
438
+ # calculates P_sat as P_TES when Rfrac = psat_level
439
+ # if the TES is at 90% R_n more than once, just take the first crossing
440
+ psats = np.full(iva.nchans, np.nan)
441
+ psat_cross_idx = np.full(iva.nchans, -1)
442
+
443
+ for i in range(iva.nchans):
444
+ R = iva.R[i]
445
+ R_n = iva.R_n[i]
446
+ p_tes = iva.p_tes[i]
447
+
448
+ if np.isnan(R_n):
449
+ continue
450
+
451
+ cross_idx = np.where(R / R_n > cfg.psat_level)[0]
452
+ if len(cross_idx) == 0:
453
+ continue
454
+
455
+ # Takes cross-index to be the first time Rfrac crosses psat_level
456
+ cross_idx = cross_idx[0]
457
+ if cross_idx == 0:
458
+ continue
459
+
460
+ psat_cross_idx[i] = cross_idx
461
+ try:
462
+ psat = interp1d(
463
+ R[cross_idx - 1 : cross_idx + 1] / R_n,
464
+ p_tes[cross_idx - 1 : cross_idx + 1],
465
+ )(cfg.psat_level)
466
+ except ValueError:
467
+ continue
468
+ psats[i] = psat
469
+
470
+ return psats, psat_cross_idx
471
+
472
+
473
+ def compute_si(iva: IVAnalysis) -> np.ndarray:
474
+ """
475
+ Recalculates responsivity using the thevenin equivalent voltage. This will
476
+ not modify the original IVAnalysis object.
477
+
478
+ Args
479
+ ----
480
+ iva : IVAnalysis
481
+ IVAnalysis object for which you want to compute Si. This should already
482
+ have items like R, R_n, R_L, i_tes, v_tes, computed.
483
+
484
+ Returns
485
+ -------
486
+ si : np.ndarray
487
+ Array of length (nchan, nbiases) with the responsivity as a fn of
488
+ thevenin equivalent voltage for each channel (V^-1).
489
+ """
490
+ si_all = np.full(iva.si.shape, np.nan)
491
+
492
+ smooth_dist = 5
493
+ w_len = 2 * smooth_dist + 1
494
+ w = (1.0 / float(w_len)) * np.ones(w_len) # window
495
+
496
+ v_thev_smooth = np.convolve(iva.v_thevenin, w, mode="same")
497
+ dv_thev = np.diff(v_thev_smooth)
498
+
499
+ for i in range(iva.nchans):
500
+ sc_idx = iva.idxs[i, 0]
501
+
502
+ if np.isnan(iva.R_n[i]) or sc_idx == -1:
503
+ continue
504
+
505
+ # Running average
506
+ i_tes_smooth = np.convolve(iva.i_tes[i], w, mode="same")
507
+ v_tes_smooth = np.convolve(iva.v_tes[i], w, mode="same")
508
+ r_tes_smooth = v_tes_smooth / i_tes_smooth
509
+
510
+ R_L = iva.R_L[i]
511
+
512
+ # Take derivatives
513
+ di_tes = np.diff(i_tes_smooth)
514
+ dv_tes = np.diff(v_tes_smooth)
515
+ R_L_smooth = np.ones(len(r_tes_smooth - 1)) * R_L
516
+ R_L_smooth[:sc_idx] = dv_tes[:sc_idx] / di_tes[:sc_idx]
517
+ r_tes_smooth_noStray = r_tes_smooth - R_L_smooth
518
+ i0 = i_tes_smooth[:-1]
519
+ r0 = r_tes_smooth_noStray[:-1]
520
+ rL = R_L_smooth[:-1]
521
+ beta = 0.0
522
+
523
+ # artificially setting rL to 0 for now,
524
+ # to avoid issues in the SC branch
525
+ # don't expect a large change, given the
526
+ # relative size of rL to the other terms
527
+ # rL = 0
528
+
529
+ # Responsivity estimate, derivation done here by MSF
530
+ # https://www.overleaf.com/project/613978cb38d9d22e8550d45c
531
+ si = -(1.0 / (i0 * r0 * (2 + beta))) * (
532
+ 1 - ((r0 * (1 + beta) + rL) / (dv_thev / di_tes))
533
+ )
534
+ si[:sc_idx] = np.nan
535
+ si_all[i, :-1] = si
536
+
537
+ return si_all
538
+
539
+
540
+ def recompute_ivpars(iva: IVAnalysis, cfg: AnalysisCfg) -> IVAnalysis:
541
+ """
542
+ Takes in an IV Analysis object and analysis cfg params, and recomputes
543
+ TES voltage, current, bias power, resistance, responsivity, and saturation
544
+ powers using the corrected TES formulas for R_L and V_thevenin.
545
+ """
546
+ iva_new = IVAnalysis.from_dict(deepcopy(iva.to_dict()))
547
+ R_sh = iva.meta["R_sh"]
548
+ R_bl = iva.meta["bias_line_resistance"]
549
+ iva_new.i_bias = iva_new.v_bias / R_bl
550
+ iva_new.v_thevenin = iva_new.i_bias * R_sh
551
+ iva_new.v_tes = np.full(iva.v_tes.shape, np.nan)
552
+ iva_new.i_tes = np.full(iva.i_tes.shape, np.nan)
553
+ iva_new.p_tes = np.full(iva.p_tes.shape, np.nan)
554
+ iva_new.R = np.full(iva.R.shape, np.nan)
555
+ iva_new.R_n = np.full(iva.R_n.shape, np.nan)
556
+ iva_new.R_L = np.full(iva.R_L.shape, np.nan)
557
+
558
+ iva_new.resp
559
+ for i in range(iva.nchans):
560
+ sc_idx = iva.idxs[i, 0]
561
+ nb_idx = iva.idxs[i, 1]
562
+
563
+ R: NDArray[np.float] = R_sh * (iva.i_bias / iva.resp[i] - 1)
564
+ R_par: float = np.nanmean(R[1:sc_idx])
565
+ R_n: float = np.nanmean(R[nb_idx:]) - R_par
566
+ R_L: float = R_sh + R_par
567
+ R_tes: NDArray[np.float] = R - R_par
568
+
569
+ iva_new.v_tes[i] = iva_new.v_thevenin * (R_tes / (R_tes + R_L))
570
+ iva_new.i_tes[i] = iva_new.v_tes[i] / R_tes
571
+ iva_new.p_tes[i] = iva_new.v_tes[i] ** 2 / R_tes
572
+ iva_new.R[i] = R_tes
573
+ iva_new.R_n[i] = R_n
574
+ iva_new.R_L[i] = R_L
575
+
576
+ iva_new.p_sat, iva_new.idxs[:, 2] = compute_psats(iva_new, cfg)
577
+ iva_new.si = compute_si(iva_new)
578
+
579
+ return iva_new