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/double.py ADDED
@@ -0,0 +1,376 @@
1
+ """
2
+ Class for managing double integrating spheres.
3
+
4
+ This module contains the DoubleSphere class, designed to simulate and analyze the
5
+ behavior of light with a sample sandwiched between a reflection and transmission
6
+ sphere.
7
+
8
+ Attributes:
9
+ r_sphere: Reflectance sphere
10
+ t_sphere: Transmittance sphere
11
+ ur1: total reflection of sample for normal incidence
12
+ ut1: total transmission of sample for normal incidence
13
+ uru: total reflection of sample for diffuse incidence
14
+ utu: total transmission of sample for diffuse incidence
15
+
16
+ Example usage:
17
+ >>> import iadpython
18
+ >>> rs = iadpython.Sphere(250,20)
19
+ >>> ts = iadpython.Sphere(250,20)
20
+ >>> d = iadpython.DoubleSphere(rs, ts)
21
+ >>> print(d)
22
+ """
23
+
24
+ import random
25
+ import time
26
+ import numpy as np
27
+ import iadpython as iad
28
+
29
+
30
+ class DoubleSphere:
31
+ """Container class for two three-port integrating sphere.
32
+
33
+ Attributes:
34
+ - r_sphere: Reflectance sphere
35
+ - t_sphere: Transmittance sphere
36
+ - current: Current sphere photon is in
37
+ - ur1: total reflection of sample for normal incidence
38
+ - ut1: total transmission of sample for normal incidence
39
+ - uru: total reflection of sample for diffuse incidence
40
+ - utu: total transmission of sample for diffuse incidence
41
+
42
+ Example::
43
+
44
+ >>> import iadpython as iad
45
+ >>> s = iad.Sphere(200, 20)
46
+ >>> print(s)
47
+ """
48
+
49
+ REFLECTION_SPHERE = 1
50
+ TRANSMISSION_SPHERE = 2
51
+
52
+ def __init__(self, r_sphere, t_sphere):
53
+ """Initialization."""
54
+ self.r_sphere = r_sphere
55
+ self.r_sphere.refl = True
56
+ self.t_sphere = t_sphere
57
+ self.t_sphere.refl = False
58
+ self.current = self.r_sphere
59
+ self.ur1 = 0
60
+ self.ut1 = 1
61
+ self._uru = self.ur1
62
+ self._utu = self.ut1
63
+ self.f_r = 0.0
64
+
65
+ def __repr__(self):
66
+ """Return basic details as a string for printing."""
67
+ s = "Double sphere experiment\n"
68
+ s += " ur1 = %s\n" % iad.stringify("%5.1f%%", self.ur1 * 100)
69
+ s += " uru = %s\n" % iad.stringify("%5.1f%%", self.uru * 100)
70
+ s += " ut1 = %s\n" % iad.stringify("%5.1f%%", self.ut1 * 100)
71
+ s += " utu = %s\n" % iad.stringify("%5.1f%%", self.utu * 100)
72
+ s += "R " + self.r_sphere.__repr__()
73
+ s += "T " + self.t_sphere.__repr__()
74
+ return s
75
+
76
+ def __str__(self):
77
+ """Return full details as a string for printing."""
78
+ s = ""
79
+ s += "Reflection Sphere\n"
80
+ s += str(self.r_sphere) + "\n"
81
+ s += "Transmission Sphere\n"
82
+ s += str(self.t_sphere) + "\n"
83
+ s += "Sample Properties\n"
84
+ s += " ur1 = %7.3f\n" % self.ur1
85
+ s += " ut1 = %7.3f\n" % self.ut1
86
+ s += " uru = %7.3f\n" % self.uru
87
+ s += " utu = %7.3f\n" % self.utu
88
+ s += "Gain Properties\n"
89
+ gr, gt = self.gain()
90
+ s += " r gain = %7.3f\n" % gr
91
+ s += " t gain = %7.3f\n" % gt
92
+ return s
93
+
94
+ @property
95
+ def uru(self):
96
+ """Getter property for sample uru."""
97
+ return self._uru
98
+
99
+ @uru.setter
100
+ def uru(self, value):
101
+ """Need to update sample uru."""
102
+ self._uru = value
103
+ self.r_sphere.sample.uru = value
104
+ self.t_sphere.sample.uru = value
105
+
106
+ @property
107
+ def utu(self):
108
+ """Need to update sample utu."""
109
+ return self._utu
110
+
111
+ @utu.setter
112
+ def utu(self, value):
113
+ """When size is changed ratios become invalid."""
114
+ self._utu = value
115
+
116
+ def do_one_photon(self, return_passes=False):
117
+ """Bounce photon in double spheres until it is detected or lost.
118
+
119
+ Args:
120
+ return_passes: When True, include sample pass count in return value.
121
+
122
+ Returns:
123
+ (r_detected, t_detected) or (r_detected, t_detected, passes)
124
+ """
125
+ weight = 1
126
+ passes = 0
127
+ r_detected = 0
128
+ t_detected = 0
129
+
130
+ # photon normally incident on sample
131
+ x = random.random()
132
+ if x < self.ur1: # reflected by sample
133
+ self.current = self.r_sphere
134
+
135
+ elif x < self.ur1 + self.ut1: # transmitted through sample
136
+ self.current = self.t_sphere
137
+ passes = 1
138
+
139
+ else: # absorbed by sample
140
+ weight = 0
141
+
142
+ while weight > 0:
143
+ detected, transmitted, _ = self.current.do_one_photon(weight=weight, double=True)
144
+
145
+ if transmitted > 0: # hit sample
146
+ if random.random() < self.utu: # passed through sample, switch spheres
147
+ passes += 1
148
+ if self.current == self.r_sphere:
149
+ self.current = self.t_sphere
150
+ else:
151
+ self.current = self.r_sphere
152
+ weight = transmitted
153
+ else: # absorbed by sample
154
+ weight = 0
155
+ else:
156
+ weight = 0
157
+ if self.current == self.r_sphere:
158
+ assert passes % 2 == 0, "reflection sphere should have even number of passes"
159
+ r_detected += detected
160
+ else:
161
+ assert passes % 2 == 1, "reflection sphere should have odd number of passes"
162
+ t_detected += detected
163
+
164
+ if return_passes:
165
+ return r_detected, t_detected, passes
166
+ return r_detected, t_detected
167
+
168
+ def do_N_photons(self, N):
169
+ """Do a Monte Carlo simulation with N photons."""
170
+ # Use current time as seed
171
+ random.seed(time.time())
172
+
173
+ num_trials = 10
174
+ total_r_detected = np.zeros(num_trials)
175
+ total_t_detected = np.zeros(num_trials)
176
+ # total_bounces = np.zeros(num_trials)
177
+
178
+ N_per_trial = N // num_trials
179
+
180
+ total = 0
181
+ for j in range(num_trials):
182
+ for _i in range(N_per_trial):
183
+ r_detected, t_detected = self.do_one_photon()
184
+ # print("%d %8.3f %8.3f" % (i, r_detected, t_detected))
185
+ total_r_detected[j] += r_detected
186
+ total_t_detected[j] += t_detected
187
+ total += 1
188
+
189
+ ave_r = np.mean(total_r_detected) / N_per_trial
190
+ std_r = np.std(total_r_detected) / N_per_trial
191
+ stderr_r = std_r / np.sqrt(num_trials)
192
+
193
+ ave_t = np.mean(total_t_detected) / N_per_trial
194
+ std_t = np.std(total_t_detected) / N_per_trial
195
+ stderr_t = std_t / np.sqrt(num_trials)
196
+
197
+ gr, gt = self.gain()
198
+ print("r_average detected = %.3f ± %.3f" % (ave_r, stderr_r))
199
+ scale = self.r_sphere.detector.a * (1 - self.r_sphere.detector.uru)
200
+ if self.r_sphere.baffle:
201
+ scale *= (
202
+ 1 - self.r_sphere.third.a
203
+ ) * self.r_sphere.r_wall + self.r_sphere.third.a * self.r_sphere.third.uru
204
+ ave_g = ave_r / scale
205
+ std_g = std_r / scale
206
+ stderr_g = std_g / np.sqrt(num_trials)
207
+ print("average gain = %.3f ± %.3f" % (ave_g, stderr_g))
208
+ print("calculated gain = %.3f" % gr)
209
+ print()
210
+
211
+ print("t_average detected = %.3f ± %.3f" % (ave_t, stderr_t))
212
+ scale = self.t_sphere.detector.a * (1 - self.t_sphere.detector.uru)
213
+ if self.t_sphere.baffle:
214
+ scale *= (
215
+ 1 - self.t_sphere.third.a
216
+ ) * self.t_sphere.r_wall + self.t_sphere.third.a * self.t_sphere.third.uru
217
+ ave_g = ave_t / scale
218
+ std_g = std_t / scale
219
+ stderr_g = std_g / np.sqrt(num_trials)
220
+ print("average gain = %.3f ± %.3f" % (ave_g, stderr_g))
221
+ print("calculated gain = %.3f" % gt)
222
+
223
+ return ave_r, stderr_r, ave_t, stderr_t
224
+
225
+ @staticmethod
226
+ def _safe_inverse(x):
227
+ """Return 1/x with infinities for zero denominators."""
228
+ x = np.asarray(x, dtype=float)
229
+ result = np.where(x == 0, np.inf, 1.0 / x)
230
+ if np.ndim(result) == 0:
231
+ return float(result)
232
+ return result
233
+
234
+ def _sphere_terms(self, sphere):
235
+ """Return CWEB gain terms for reflection or transmission sphere."""
236
+ if sphere == self.REFLECTION_SPHERE:
237
+ s = self.r_sphere
238
+ elif sphere == self.TRANSMISSION_SPHERE:
239
+ s = self.t_sphere
240
+ else:
241
+ raise ValueError(f"Unknown sphere selector: {sphere}")
242
+
243
+ return (
244
+ s.sample.a,
245
+ s.detector.a,
246
+ s.third.a,
247
+ s.a_wall,
248
+ s.detector.uru,
249
+ s.r_wall,
250
+ s.baffle,
251
+ )
252
+
253
+ def gain_single(self, sphere, uru_sample, uru_third):
254
+ """
255
+ Single-sphere gain from CWEB `Gain()`.
256
+
257
+ This is the exact algebra used in `iad/src/iad_calc.c`.
258
+ """
259
+ as_x, ad_x, at_x, aw_x, rd_x, rw_x, baffle_x = self._sphere_terms(sphere)
260
+
261
+ if baffle_x:
262
+ inv_gain = rw_x + (at_x / aw_x) * uru_third
263
+ inv_gain *= aw_x + (1 - at_x) * (ad_x * rd_x + as_x * uru_sample)
264
+ inv_gain = 1.0 - inv_gain
265
+ else:
266
+ inv_gain = 1.0 - aw_x * rw_x - ad_x * rd_x - as_x * uru_sample - at_x * uru_third
267
+
268
+ return self._safe_inverse(inv_gain)
269
+
270
+ def gain_11(self, uru, tdiffuse):
271
+ """Gain for light starting/ending in the reflection sphere (`Gain_11`)."""
272
+ g = self.gain_single(self.REFLECTION_SPHERE, uru, 0.0)
273
+ gp = self.gain_single(self.TRANSMISSION_SPHERE, uru, 0.0)
274
+
275
+ coupling = (
276
+ self.r_sphere.sample.a
277
+ * self.t_sphere.sample.a
278
+ * self.r_sphere.a_wall
279
+ * self.t_sphere.a_wall
280
+ * (1 - self.r_sphere.third.a)
281
+ * (1 - self.t_sphere.third.a)
282
+ * g
283
+ * gp
284
+ * tdiffuse
285
+ * tdiffuse
286
+ )
287
+ return g * self._safe_inverse(1.0 - coupling)
288
+
289
+ def gain_22(self, uru, tdiffuse):
290
+ """Gain for light starting/ending in the transmission sphere (`Gain_22`)."""
291
+ g = self.gain_single(self.REFLECTION_SPHERE, uru, 0.0)
292
+ gp = self.gain_single(self.TRANSMISSION_SPHERE, uru, 0.0)
293
+
294
+ coupling = (
295
+ self.r_sphere.sample.a
296
+ * self.t_sphere.sample.a
297
+ * self.r_sphere.a_wall
298
+ * self.t_sphere.a_wall
299
+ * (1 - self.r_sphere.third.a)
300
+ * (1 - self.t_sphere.third.a)
301
+ * g
302
+ * gp
303
+ * tdiffuse
304
+ * tdiffuse
305
+ )
306
+ return gp * self._safe_inverse(1.0 - coupling)
307
+
308
+ def gain(self, uru=None, utu=None):
309
+ """
310
+ Return `(Gain_11, Gain_22)` for the current sample diffuse properties.
311
+
312
+ This preserves the historical `DoubleSphere.gain()` API while using
313
+ the exact CWEB algebra.
314
+ """
315
+ if uru is None:
316
+ uru = self.uru
317
+ if utu is None:
318
+ utu = self.utu
319
+ return self.gain_11(uru, utu), self.gain_22(uru, utu)
320
+
321
+ def two_sphere_r(self, ur1, uru, ut1, utu):
322
+ """Exact CWEB `Two_Sphere_R()` power expression."""
323
+ gp = self.gain_single(self.TRANSMISSION_SPHERE, uru, 0.0)
324
+ x = self.r_sphere.detector.a * (1 - self.r_sphere.third.a) * self.r_sphere.r_wall
325
+ x *= self.gain_11(uru, utu)
326
+ x *= (
327
+ (1 - self.f_r) * ur1
328
+ + self.r_sphere.r_wall * self.f_r
329
+ + (1 - self.f_r)
330
+ * self.t_sphere.sample.a
331
+ * (1 - self.t_sphere.third.a)
332
+ * self.t_sphere.r_wall
333
+ * ut1
334
+ * utu
335
+ * gp
336
+ )
337
+ return x
338
+
339
+ def two_sphere_t(self, ur1, uru, ut1, utu):
340
+ """Exact CWEB `Two_Sphere_T()` power expression."""
341
+ g = self.gain_single(self.REFLECTION_SPHERE, uru, 0.0)
342
+ x = self.t_sphere.detector.a * (1 - self.t_sphere.third.a) * self.t_sphere.r_wall
343
+ x *= self.gain_22(uru, utu)
344
+ x *= (1 - self.f_r) * ut1 + (
345
+ 1 - self.r_sphere.third.a
346
+ ) * self.r_sphere.r_wall * self.r_sphere.sample.a * utu * (
347
+ self.f_r * self.r_sphere.r_wall + (1 - self.f_r) * ur1
348
+ ) * g
349
+ return x
350
+
351
+ def measured_rt(self, ur1, uru, ut1, utu):
352
+ """
353
+ Exact CWEB two-sphere normalization for measured `M_R` and `M_T`.
354
+
355
+ Inputs must already include any unscattered/lost-light corrections.
356
+ """
357
+ r_0 = self.two_sphere_r(0.0, 0.0, 0.0, 0.0)
358
+ t_0 = self.two_sphere_t(0.0, 0.0, 0.0, 0.0)
359
+
360
+ m_r = self.r_sphere.r_std * (self.two_sphere_r(ur1, uru, ut1, utu) - r_0)
361
+ m_r *= self._safe_inverse(self.two_sphere_r(self.r_sphere.r_std, self.r_sphere.r_std, 0.0, 0.0) - r_0)
362
+
363
+ m_t = self.two_sphere_t(ur1, uru, ut1, utu) - t_0
364
+ m_t *= self._safe_inverse(self.two_sphere_t(0.0, 0.0, 1.0, 1.0) - t_0)
365
+
366
+ return m_r, m_t
367
+
368
+ def MR(self, ur1, uru, ut1, utu):
369
+ """Convenience wrapper for the two-sphere measured reflection."""
370
+ m_r, _ = self.measured_rt(ur1, uru, ut1, utu)
371
+ return m_r
372
+
373
+ def MT(self, ur1, uru, ut1, utu):
374
+ """Convenience wrapper for the two-sphere measured transmission."""
375
+ _, m_t = self.measured_rt(ur1, uru, ut1, utu)
376
+ return m_t