radphen-iadpython 0.7.0__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.
iadpython/iad.py ADDED
@@ -0,0 +1,615 @@
1
+ """Class for doing inverse adding-doubling calculations for a sample.
2
+
3
+ Example::
4
+
5
+ >>> import iadpython as iad
6
+
7
+ >>> exp = iad.Experiment(0.5,0.1,0.01)
8
+ >>> a, b, g = exp.invert()
9
+ >>> print("a = %7.3f" % a)
10
+ >>> print("b = %7.3f" % b)
11
+ >>> print("g = %7.3f" % g)
12
+
13
+ >>> s = iad.Sample(0.5,0.1,0.01,n=1.4, n_top=1.5, n_bottom=1.5)
14
+ >>> exp = iad.Experiment(s)
15
+ >>> a, b, g = exp.invert()
16
+ >>> print("a = %7.3f" % a)
17
+ >>> print("b = %7.3f" % b)
18
+ >>> print("g = %7.3f" % g)
19
+ """
20
+
21
+ import sys
22
+ import copy
23
+ import numpy as np
24
+ import scipy.optimize
25
+ import iadpython as iad
26
+
27
+ G_BOUND_EPS = 1e-6
28
+
29
+
30
+ class Experiment:
31
+ """Container class for details of an experiment."""
32
+
33
+ def __init__(
34
+ self,
35
+ r=None,
36
+ t=None,
37
+ u=None,
38
+ sample=None,
39
+ r_sphere=None,
40
+ t_sphere=None,
41
+ num_spheres=0,
42
+ default_a=None,
43
+ default_b=None,
44
+ default_g=None,
45
+ ):
46
+ """Object initialization."""
47
+ if sample is None:
48
+ self.sample = iad.Sample()
49
+ else:
50
+ self.sample = sample
51
+
52
+ self.r_sphere = r_sphere
53
+ self.t_sphere = t_sphere
54
+ self.num_spheres = num_spheres
55
+
56
+ self.m_r = r
57
+ self.m_t = t
58
+ self.m_u = u
59
+ self.lambda0 = None
60
+
61
+ # these will have to be eventually supported
62
+ self.d_beam = 1
63
+ self.lambda0 = None
64
+
65
+ self.flip_sample = False
66
+ self.fraction_of_rc_in_mr = 1
67
+ self.fraction_of_tc_in_mt = 1
68
+ self.f_r = 0.0
69
+
70
+ self.ur1_lost = 0
71
+ self.uru_lost = 0
72
+ self.ut1_lost = 0
73
+ self.utu_lost = 0
74
+
75
+ self.default_a = default_a
76
+ self.default_b = default_b
77
+ self.default_g = default_g
78
+ self.default_ba = None
79
+ self.default_bs = None
80
+ self.default_mua = None
81
+ self.default_mus = None
82
+
83
+ self.search = "unknown"
84
+ self.metric = 1
85
+ self.tolerance = 0.0001
86
+ self.MC_tolerance = 0.01
87
+ self.final_distance = 1
88
+ self.iterations = 1
89
+ self.error = 1
90
+ self.num_measurements = 0
91
+ self.grid = None
92
+ self.method = "unknown"
93
+ self.debug_level = 0
94
+ self.max_mc_iterations = 19
95
+ self.n_photons = 100000
96
+ self.use_adaptive_grid = True
97
+ self.adaptive_grid_tol = 0.03
98
+ self.adaptive_grid_max_depth = 6
99
+ self.counter = 0
100
+ self.include_measurements = True
101
+
102
+ def __str__(self):
103
+ """Return basic details as a string for printing."""
104
+ s = "---------------- Sample ---------------\n"
105
+ s += self.sample.__str__()
106
+ s += "\n--------------- Spheres ---------------\n"
107
+ if not np.isscalar(self.num_spheres):
108
+ s += "number of spheres range (%s)\n" % iad.stringify("%d", self.num_spheres)
109
+ elif self.num_spheres == 0:
110
+ s += "No spheres used.\n"
111
+ elif self.num_spheres == 1:
112
+ s += "A single integrating sphere was used.\n"
113
+ elif self.num_spheres == 2:
114
+ s += "Double integrating spheres were used.\n"
115
+ if self.r_sphere is not None:
116
+ s += "Reflectance Sphere--------\n"
117
+ s += self.r_sphere.__str__()
118
+ if self.t_sphere is not None:
119
+ s += "Transmission Sphere--------\n"
120
+ s += self.t_sphere.__str__()
121
+
122
+ if self.include_measurements:
123
+ s += "\n------------- Measurements ------------\n"
124
+ s += " Reflection = "
125
+ s += iad.stringify("%.5f", self.m_r) + "\n"
126
+ s += " Transmission = "
127
+ s += iad.stringify("%.5f", self.m_t) + "\n"
128
+ s += " Unscattered Transmission = "
129
+ s += iad.stringify("%.5f", self.m_u) + "\n"
130
+ return s
131
+
132
+ def check_measurements(self):
133
+ """Make sure measurements are sane."""
134
+ between = " Must be between 0 and 1."
135
+ if (
136
+ not (self.m_r is None or np.isscalar(self.m_r))
137
+ or not (self.m_t is None or np.isscalar(self.m_t))
138
+ or not (self.m_u is None or np.isscalar(self.m_u))
139
+ ):
140
+ raise ValueError("invert_scalar_rt() is only for scalar m_r, m_t, m_u")
141
+
142
+ if self.m_r is not None:
143
+ if self.m_r < 0 or self.m_r > 1:
144
+ raise ValueError("Invalid refl. %.4f" % self.m_r + between)
145
+
146
+ if self.m_t is not None:
147
+ if self.m_t < 0 or self.m_t > 1:
148
+ raise ValueError("Invalid trans. %.4f" % self.m_t + between)
149
+
150
+ if self.m_u is not None:
151
+ if self.m_u < 0 or self.m_u > 1:
152
+ raise ValueError("Invalid unscattered trans. %.4f." % self.m_u + between)
153
+
154
+ def useful_measurements(self):
155
+ """Count the number of useful measurements."""
156
+ self.num_measurements = 0
157
+ if self.m_r is not None:
158
+ self.num_measurements += 1
159
+ if self.m_t is not None:
160
+ self.num_measurements += 1
161
+ if self.m_u is not None:
162
+ self.num_measurements += 1
163
+
164
+ def determine_one_parameter_search(self):
165
+ """Establish proper search when only one measurement is available."""
166
+ # default case
167
+ self.search = "find_a"
168
+
169
+ # albedo is known
170
+ if self.default_a is not None:
171
+ if self.default_b is None:
172
+ self.search = "find_b"
173
+ else:
174
+ self.search = "find_g"
175
+
176
+ # optical thickness is known
177
+ elif self.default_b is not None:
178
+ self.search = "find_a"
179
+
180
+ # anisotropy is known
181
+ elif self.default_g is not None:
182
+ self.search = "find_a"
183
+
184
+ # scattering coefficient is known
185
+ elif self.default_bs is not None:
186
+ self.search = "find_ba"
187
+
188
+ # absorption coefficient is known
189
+ elif self.default_ba is not None:
190
+ self.search = "find_bs"
191
+
192
+ def determine_two_parameter_search(self):
193
+ """Establish proper search when 2 or 3 measurements are available."""
194
+ # albedo is known
195
+ if self.default_a is not None:
196
+ self.search = "find_bg"
197
+
198
+ # optical thickness is known
199
+ elif self.default_b is not None:
200
+ self.search = "find_ag"
201
+
202
+ # anisotropy is known
203
+ elif self.default_g is not None:
204
+ self.search = "find_ab"
205
+
206
+ # scattering coefficient is known
207
+ elif self.default_bs is not None:
208
+ self.search = "find_bag"
209
+
210
+ # absorption coefficient is known
211
+ elif self.default_ba is not None:
212
+ self.search = "find_bsg"
213
+
214
+ # by this point, assume that M_R, M_T, and M_U are known
215
+ else:
216
+ if self.m_u is None or self.m_u <= 0:
217
+ self.search = "find_ab"
218
+ else:
219
+ self.search = "find_ag"
220
+
221
+ def determine_search(self):
222
+ """Determine type of search to do."""
223
+ if self.num_measurements == 0:
224
+ self.search = "unknown"
225
+
226
+ if self.num_measurements == 1:
227
+ self.determine_one_parameter_search()
228
+
229
+ else:
230
+ self.determine_two_parameter_search()
231
+
232
+ def invert_scalar_rt(self, verbose = False):
233
+ """Find a,b,g for a single experimental measurement.
234
+
235
+ This routine assumes that `m_r`, `m_t`, and `m_u` are scalars.
236
+
237
+ Parameters:
238
+ verbose: if True, print progress. Default is True.
239
+
240
+ Returns:
241
+ - `a` is the single scattering albedo of the slab
242
+ - `b` is the optical thickness of the slab
243
+ - `g` is the anisotropy of single scattering
244
+ """
245
+ self.check_measurements()
246
+ self.useful_measurements()
247
+ self.determine_search()
248
+
249
+ if self.m_r is None and self.m_t is None and self.m_u is None:
250
+ return None, None, None
251
+
252
+ # assign default values
253
+ self.sample.a = self.default_a or 0
254
+ self.sample.b = self.default_b or self.what_is_b()
255
+ self.sample.g = self.default_g or 0
256
+
257
+ # print('search is', self.search)
258
+ # print(' a = ', self.sample.a)
259
+ # print(' b = ', self.sample.b)
260
+ # print(' g = ', self.sample.g)
261
+
262
+ if self.search == "find_a":
263
+ _ = scipy.optimize.minimize_scalar(afun, args=(self), bounds=(0, 1), method="bounded")
264
+
265
+ if self.search == "find_b":
266
+ _ = scipy.optimize.minimize_scalar(bfun, args=(self), method="brent")
267
+
268
+ if self.search == "find_g":
269
+ _ = scipy.optimize.minimize_scalar(
270
+ gfun,
271
+ args=(self),
272
+ bounds=(-1 + G_BOUND_EPS, 1 - G_BOUND_EPS),
273
+ method="bounded",
274
+ )
275
+
276
+ if self.search in ["find_ab", "find_ag", "find_bg"]:
277
+
278
+ if self.grid is None:
279
+ if self.use_adaptive_grid:
280
+ self.grid = iad.AGrid(tol=self.adaptive_grid_tol, max_depth=self.adaptive_grid_max_depth)
281
+ else:
282
+ self.grid = iad.Grid()
283
+
284
+ if self.use_adaptive_grid and not isinstance(self.grid, iad.AGrid):
285
+ self.grid = iad.AGrid(tol=self.adaptive_grid_tol, max_depth=self.adaptive_grid_max_depth)
286
+ if not self.use_adaptive_grid and isinstance(self.grid, iad.AGrid):
287
+ self.grid = iad.Grid()
288
+
289
+ # the grids are two-dimensional, one value is held constant
290
+ grid_constant = None
291
+ if self.search == "find_ag":
292
+ grid_constant = self.sample.b
293
+ if self.search == "find_bg":
294
+ grid_constant = self.sample.a
295
+ if self.search == "find_ab":
296
+ grid_constant = self.sample.g
297
+
298
+ if isinstance(self.grid, iad.AGrid):
299
+ if self.grid.is_stale(grid_constant, search=self.search):
300
+ self.grid.calc(self, default=grid_constant, search=self.search)
301
+ else:
302
+ if self.grid.is_stale(grid_constant):
303
+ self.grid.calc(self, grid_constant)
304
+
305
+ a, b, g = self.grid.min_abg(self.m_r, self.m_t)
306
+ if verbose:
307
+ print('grid constant %8.5f' % grid_constant)
308
+
309
+ print('grid start a=%8.5f' % a)
310
+ print('grid start b=%8.5f' % b)
311
+ print('grid start g=%8.5f' % g)
312
+
313
+ if self.search == 'find_ab':
314
+ x = scipy.optimize.Bounds(np.array([0, 0]), np.array([1, np.inf]))
315
+ result = scipy.optimize.minimize(abfun, [a, b], args=(self), bounds=x, method="Nelder-Mead")
316
+ self._refine_two_parameter_fit(result.x, ("a", "b"), x)
317
+
318
+ if self.search == "find_ag":
319
+ x = scipy.optimize.Bounds(
320
+ np.array([0, -1 + G_BOUND_EPS]),
321
+ np.array([1, 1 - G_BOUND_EPS]),
322
+ )
323
+ result = scipy.optimize.minimize(agfun, [a, g], args=(self), bounds=x, method="Nelder-Mead")
324
+ self._refine_two_parameter_fit(result.x, ("a", "g"), x)
325
+
326
+ if self.search == "find_bg":
327
+ x = scipy.optimize.Bounds(
328
+ np.array([0, -1 + G_BOUND_EPS]),
329
+ np.array([np.inf, 1 - G_BOUND_EPS]),
330
+ )
331
+ result = scipy.optimize.minimize(bgfun, [b, g], args=(self), bounds=x, method="Nelder-Mead")
332
+ self._refine_two_parameter_fit(result.x, ("b", "g"), x)
333
+
334
+ return self.sample.a, self.sample.b, self.sample.g
335
+
336
+ def _refine_two_parameter_fit(self, values, names, bounds):
337
+ """Refine a stalled L1 fit, retaining only a smaller measured residual.
338
+
339
+ At conservative-albedo boundaries, R+T is constant and the L1
340
+ objective can have a flat plateau. A bounded residual-vector fit can
341
+ leave that plateau even when Nelder-Mead reports successful convergence.
342
+ Always restore the selected parameters after optimizer trial evaluations.
343
+ """
344
+ def residual(parameters):
345
+ for name, value in zip(names, parameters):
346
+ setattr(self.sample, name, value)
347
+ measured = self.measured_rt()
348
+ return np.asarray(measured) - np.asarray((self.m_r, self.m_t))
349
+
350
+ selected = np.asarray(values, dtype=float).copy()
351
+ distance = np.sum(np.abs(residual(selected)))
352
+ if distance > self.tolerance:
353
+ candidate = scipy.optimize.least_squares(
354
+ residual, selected, bounds=(bounds.lb, bounds.ub)
355
+ )
356
+ if np.all(np.isfinite(candidate.x)):
357
+ candidate_distance = np.sum(np.abs(residual(candidate.x)))
358
+ if np.isfinite(candidate_distance) and candidate_distance < distance:
359
+ selected = candidate.x
360
+
361
+ for name, value in zip(names, selected):
362
+ setattr(self.sample, name, value)
363
+
364
+ def print_dot(self):
365
+ """Print a character for each datapoint during analysis."""
366
+ self.counter += 1
367
+ if self.counter % 50 == 0:
368
+ print(file=sys.stderr)
369
+ print(".", end="", file=sys.stderr)
370
+ sys.stderr.flush()
371
+
372
+ def invert_rt(self, verbose=False):
373
+ """Find a,b,g for experimental measurements.
374
+
375
+ This method works if `m_r`, `m_t`, and `m_u` are scalars or arrays.
376
+
377
+ Parameters:
378
+ verbose: if True, print progress. Default is True.
379
+
380
+ Returns:
381
+ - `a` is the single scattering albedo of the slab
382
+ - `b` is the optical thickness of the slab
383
+ - `g` is the anisotropy of single scattering
384
+ """
385
+ if self.m_r is None and self.m_t is None and self.m_u is None:
386
+ return self.invert_scalar_rt()
387
+
388
+ # any scalar measurement indicates a single data point
389
+ if np.isscalar(self.m_r) or np.isscalar(self.m_t) or np.isscalar(self.m_u):
390
+ return self.invert_scalar_rt()
391
+
392
+ # figure out the number of points that we need to invert
393
+ if self.m_r is not None:
394
+ N = len(self.m_r)
395
+ elif self.m_t is not None:
396
+ N = len(self.m_t)
397
+ else:
398
+ N = len(self.m_u)
399
+
400
+ a = np.zeros(N)
401
+ b = np.zeros(N)
402
+ g = np.zeros(N)
403
+
404
+ x = copy.deepcopy(self)
405
+ x.m_r = None
406
+ x.m_t = None
407
+ x.m_u = None
408
+
409
+ for i in range(N):
410
+ if self.m_r is not None:
411
+ x.m_r = self.m_r[i]
412
+ if self.m_t is not None:
413
+ x.m_t = self.m_t[i]
414
+ if self.m_u is not None:
415
+ x.m_u = self.m_u[i]
416
+ a[i], b[i], g[i] = x.invert_scalar_rt(verbose = verbose)
417
+ if verbose:
418
+ self.print_dot()
419
+
420
+ print(file=sys.stderr)
421
+ return a, b, g
422
+
423
+ def what_is_b(self):
424
+ """Find optical thickness using unscattered transmission."""
425
+ s = self.sample
426
+ t_u = self.m_u or 0
427
+ n_boundary = getattr(s, "n_sample_boundary", s.n)
428
+ n_outer_above = getattr(s, "n_outer_above", 1.0)
429
+ n_outer_below = getattr(s, "n_outer_below", 1.0)
430
+
431
+ r1, t1 = iad.absorbing_glass_RT(n_outer_above, s.n_above, n_boundary, s.nu_0, s.b_above)
432
+
433
+ mu = iad.cos_snell(iad.fresnel._transport_index(n_outer_above), s.nu_0, s.n)
434
+
435
+ r2, t2 = iad.absorbing_glass_RT(n_boundary, s.n_below, n_outer_below, mu, s.b_below)
436
+
437
+ if t_u <= 0:
438
+ return np.inf
439
+
440
+ if t_u >= t1 * t2 / (1 - r1 * r2):
441
+ return 0.001
442
+
443
+ tt = t1 * t2
444
+
445
+ if r1 == 0 or r2 == 0:
446
+ ratio = tt / t_u
447
+ else:
448
+ ratio = (tt + np.sqrt(tt**2 + 4 * t_u**2 * r1 * r2)) / (2 * t_u)
449
+
450
+ return s.nu_0 * np.log(ratio)
451
+
452
+ def measured_rt(self):
453
+ r"""Calculate measured reflection and transmission.
454
+
455
+ The direct incident power is :math:`(1-f_u)P`. The reflected power will
456
+ be :math:`(1-f_u)R_{direct} P`. Since baffles ensure that the light cannot
457
+ reach the detector, we must bounce the light off the sphere walls to
458
+ use to above gain formulas. The contribution will then be
459
+
460
+ .. math:: (1-f_u)R_{direct} (1-a_e) r_w P.
461
+
462
+ The measured power will be
463
+
464
+ .. math:: P_d = a_d (1-a_e) r_w [(1-f_u) r_{direct} + f_u r_w] P ⋅ G(r_s)
465
+
466
+ Similarly the power falling on the detector measuring transmitted light is
467
+
468
+ .. math:: P_d'= a_d' t_{direct} r_w' (1-a_e') P ⋅ G'(r_s)
469
+
470
+ when the empty port in the transmission sphere is closed,
471
+ :math:`a_e'=0`.
472
+
473
+ The normalized sphere measurements are
474
+
475
+ .. math:: M_R = r_{std}⋅\frac{R(r_{direct},r_s)-R(0,0)}{R(r_{std},r_{std})-R(0,0)}
476
+
477
+ and
478
+
479
+ .. math:: M_T = t_{std}⋅\frac{T(t_{direct},r_s)-T(0,0)}{T(t_{std},r_{std})-T(0,0)}
480
+
481
+ Args:
482
+ ur1: reflection for collimated incidence
483
+ ut1: transmission for collimated incidence
484
+ uru: reflection for diffuse incidence
485
+ utu: transmission for diffuse incidence
486
+
487
+ Returns:
488
+ [float, float]: measured reflection and transmission
489
+ """
490
+ s = self.sample
491
+ ur1, ut1, uru, utu = s.rt()
492
+
493
+ # find the unscattered reflection and transmission
494
+ r_u, t_u = iad.specular_rt(
495
+ s.n_above,
496
+ getattr(s, "n_sample_boundary", s.n),
497
+ s.n_below,
498
+ s.b,
499
+ s.nu_0,
500
+ n_outer_top=getattr(s, "n_outer_above", 1.0),
501
+ n_outer_bot=getattr(s, "n_outer_below", 1.0),
502
+ n_slab_transport=s.n,
503
+ )
504
+
505
+ # correct for lost light
506
+ ur1_actual = ur1 - self.ur1_lost
507
+ ut1_actual = ut1 - self.ut1_lost
508
+ uru_actual = uru - self.uru_lost
509
+ utu_actual = utu - self.utu_lost
510
+
511
+ # correct for fraction not collected
512
+ m_r = ur1_actual - (1.0 - self.fraction_of_rc_in_mr) * r_u
513
+ m_t = ut1_actual - (1.0 - self.fraction_of_tc_in_mt) * t_u
514
+
515
+ if self.num_spheres == 2:
516
+ if self.r_sphere is None or self.t_sphere is None:
517
+ raise ValueError("Double sphere mode requires both reflection and transmission spheres.")
518
+
519
+ # Match CWEB/C correction path in iad_calc.c before two-sphere formulas
520
+ uru_calc = max(uru_actual, 0.0)
521
+ utu_calc = max(utu_actual, 0.0)
522
+ ur1_calc = max(ur1 - (1.0 - self.fraction_of_rc_in_mr) * r_u - self.ur1_lost, 0.0)
523
+ ut1_calc = max(ut1 - (1.0 - self.fraction_of_tc_in_mt) * t_u - self.ut1_lost, 0.0)
524
+
525
+ d_spheres = iad.DoubleSphere(self.r_sphere, self.t_sphere)
526
+ d_spheres.f_r = self.f_r
527
+ m_r, m_t = d_spheres.measured_rt(ur1_calc, uru_calc, ut1_calc, utu_calc)
528
+ return m_r, m_t
529
+
530
+ if self.num_spheres == 1:
531
+ if self.method in ("comparison", 1):
532
+ return m_r, m_t
533
+
534
+ if self.r_sphere is not None:
535
+ f_u = self.fraction_of_rc_in_mr
536
+ m_r = self.r_sphere.MR(ur1, uru, R_u=r_u, f_u=f_u, f_w=self.f_r)
537
+
538
+ if self.t_sphere is not None:
539
+ m_t = self.t_sphere.MT(ut1, uru, T_u=t_u, f_u=self.fraction_of_tc_in_mt)
540
+
541
+ return m_r, m_t
542
+
543
+
544
+ def afun(x, *args):
545
+ """Vary the albedo."""
546
+ exp = args[0]
547
+ exp.sample.a = x
548
+ m_r, m_t = exp.measured_rt()
549
+
550
+ result = 0
551
+ if exp.m_r is not None:
552
+ result += np.abs(m_r - exp.m_r)
553
+ if exp.m_t is not None:
554
+ result += np.abs(m_t - exp.m_t)
555
+ return result
556
+
557
+
558
+ def bfun(x, *args):
559
+ """Vary the optical thickness."""
560
+ exp = args[0]
561
+ exp.sample.b = x
562
+ m_r, m_t = exp.measured_rt()
563
+
564
+ result = 0
565
+ if exp.m_r is not None:
566
+ result += np.abs(m_r - exp.m_r)
567
+ if exp.m_t is not None:
568
+ result += np.abs(m_t - exp.m_t)
569
+ return result
570
+
571
+
572
+ def gfun(x, *args):
573
+ """Vary the anisotropy."""
574
+ exp = args[0]
575
+ exp.sample.g = x
576
+ m_r, m_t = exp.measured_rt()
577
+
578
+ result = 0
579
+ if exp.m_r is not None:
580
+ result += np.abs(m_r - exp.m_r)
581
+ if exp.m_t is not None:
582
+ result += np.abs(m_t - exp.m_t)
583
+ return result
584
+
585
+
586
+ def abfun(x, *args):
587
+ """Vary the ab."""
588
+ exp = args[0]
589
+ exp.sample.a = x[0]
590
+ exp.sample.b = x[1]
591
+ m_r, m_t = exp.measured_rt()
592
+ delta = np.abs(m_r - exp.m_r) + np.abs(m_t - exp.m_t)
593
+ # print("%7.4f %7.4f %7.4f %7.4f %7.4f" % (exp.sample.a, exp.sample.b, m_r, m_t, delta))
594
+ return delta
595
+
596
+
597
+ def bgfun(x, *args):
598
+ """Vary the bg."""
599
+ exp = args[0]
600
+ exp.sample.b = x[0]
601
+ exp.sample.g = x[1]
602
+ m_r, m_t = exp.measured_rt()
603
+ delta = np.abs(m_r - exp.m_r) + np.abs(m_t - exp.m_t)
604
+ return delta
605
+
606
+
607
+ def agfun(x, *args):
608
+ """Vary the ag."""
609
+ exp = args[0]
610
+ exp.sample.a = x[0]
611
+ exp.sample.g = x[1]
612
+ m_r, m_t = exp.measured_rt()
613
+ delta = np.abs(m_r - exp.m_r) + np.abs(m_t - exp.m_t)
614
+ # print("%9.7f %8.5f %8.5f %8.5f %8.5f" % (delta, x[0], x[1], m_r, m_t))
615
+ return delta