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/__init__.py +59 -0
- iadpython/ad.py +1377 -0
- iadpython/agrid.py +306 -0
- iadpython/cache.py +133 -0
- iadpython/combine.py +620 -0
- iadpython/constants.py +3 -0
- iadpython/data/M38597.csv +759 -0
- iadpython/double.py +376 -0
- iadpython/fresnel.py +642 -0
- iadpython/grid.py +125 -0
- iadpython/iad.py +615 -0
- iadpython/iadc.py +1003 -0
- iadpython/iadcommand.py +712 -0
- iadpython/layer.py +241 -0
- iadpython/nist.py +114 -0
- iadpython/port.py +242 -0
- iadpython/quadrature.py +172 -0
- iadpython/redistribution.py +318 -0
- iadpython/rxt.py +201 -0
- iadpython/sphere.py +582 -0
- iadpython/start.py +377 -0
- iadpython/txt.py +147 -0
- radphen_iadpython-0.7.0.dist-info/METADATA +264 -0
- radphen_iadpython-0.7.0.dist-info/RECORD +28 -0
- radphen_iadpython-0.7.0.dist-info/WHEEL +5 -0
- radphen_iadpython-0.7.0.dist-info/entry_points.txt +2 -0
- radphen_iadpython-0.7.0.dist-info/licenses/LICENSE.txt +18 -0
- radphen_iadpython-0.7.0.dist-info/top_level.txt +1 -0
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
|