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/sphere.py
ADDED
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Class for managing integrating spheres.
|
|
3
|
+
|
|
4
|
+
This module contains the Sphere class, designed to simulate and analyze the
|
|
5
|
+
behavior of light within an integrating sphere. An integrating sphere is a
|
|
6
|
+
device used in optical measurements, which allows for the uniform scattering
|
|
7
|
+
of light. It is commonly used for reflection and transmission measurements
|
|
8
|
+
of materials.
|
|
9
|
+
|
|
10
|
+
The Sphere class models the geometrical and optical properties of an
|
|
11
|
+
integrating sphere, enabling the calculation of various parameters such as
|
|
12
|
+
the areas of spherical caps given port diameters, relative port areas,
|
|
13
|
+
and the gain caused by reflections within the sphere. It supports different
|
|
14
|
+
measurements scenarios by adjusting port diameters, detector reflectivity,
|
|
15
|
+
wall reflectivity, and using a reflectance standard.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
d_sphere (float): Diameter of the integrating sphere in millimeters.
|
|
19
|
+
d_sample (float): Diameter of the port that holds the sample.
|
|
20
|
+
d_third (float): Diameter of the third port.
|
|
21
|
+
d_detector (float): Diameter of the port with the detector.
|
|
22
|
+
r_detector (float): Reflectivity of the detector.
|
|
23
|
+
r_wall (float): Reflectivity of the sphere's internal wall.
|
|
24
|
+
r_std (float): Reflectivity of the standard used for calibration.
|
|
25
|
+
|
|
26
|
+
Methods:
|
|
27
|
+
cap_area: actual area of a spherical cap for a port
|
|
28
|
+
relative_cap_area: relative area of spherical cap to the sphere's area.
|
|
29
|
+
gain: sphere gain relative to isotropic diffuse source in center of black sphere
|
|
30
|
+
|
|
31
|
+
Example usage:
|
|
32
|
+
>>> import iadpython
|
|
33
|
+
>>> s = iadpython.Sphere(250,20)
|
|
34
|
+
>>> print(s)
|
|
35
|
+
|
|
36
|
+
>>> s = iadpython.Sphere(200, 20, d_third=10, d_detector=10, r_detector=0.8, r_wall=0.99, r_std=0.99)
|
|
37
|
+
>>> print(sphere)
|
|
38
|
+
>>> area_sample = sphere.cap_area(sphere.d_sample)
|
|
39
|
+
>>> print(f"Sample port area: {area_sample:.2f} mm²")
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
import random
|
|
43
|
+
import time
|
|
44
|
+
from enum import Enum
|
|
45
|
+
import numpy as np
|
|
46
|
+
import iadpython as iad
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class PortType(Enum):
|
|
50
|
+
"""Possible sphere wall locations."""
|
|
51
|
+
|
|
52
|
+
WALL = 0
|
|
53
|
+
SAMPLE = 1
|
|
54
|
+
DETECTOR = 2
|
|
55
|
+
THIRD = 3
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Sphere:
|
|
59
|
+
"""Container class for a three-port integrating sphere.
|
|
60
|
+
|
|
61
|
+
For a reflection measurement, the third port is the diameter of through
|
|
62
|
+
which the light enters to hit the sample. For a transmission measurement
|
|
63
|
+
this is the port that might allow unscattered transmission to leave. In
|
|
64
|
+
either case, the reflectance from this port is assumed to be zero.
|
|
65
|
+
|
|
66
|
+
By default, the sample port is on top (z=-R), the third port is on the bottom (z=R)
|
|
67
|
+
and the detector is on the side (x=R)
|
|
68
|
+
|
|
69
|
+
Attributes:
|
|
70
|
+
- d: diameter of integrating sphere [mm]
|
|
71
|
+
- r_wall: reflectivity of the wall
|
|
72
|
+
- a_wall: fraction of the sphere that is walls (relative
|
|
73
|
+
- r_std: reflectivity of the standard used with the sphere
|
|
74
|
+
- sample: port object representing sample port
|
|
75
|
+
- third: port object for empty port in reflection sphere
|
|
76
|
+
or the standard port in transmission sphere
|
|
77
|
+
- detector: port object representing detector port
|
|
78
|
+
- x: x-coordinate of photon on wall
|
|
79
|
+
- y: y-coordinate of photon on wall
|
|
80
|
+
- z: z-coordinate of photon on wall
|
|
81
|
+
|
|
82
|
+
Example::
|
|
83
|
+
|
|
84
|
+
>>> import iadpython as iad
|
|
85
|
+
>>> s = iad.Sphere(200, 20)
|
|
86
|
+
>>> print(s)
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
d_sphere,
|
|
92
|
+
d_sample,
|
|
93
|
+
d_third=0,
|
|
94
|
+
r_third=0,
|
|
95
|
+
d_detector=0,
|
|
96
|
+
r_detector=0,
|
|
97
|
+
r_std=0.99,
|
|
98
|
+
r_wall=0.99,
|
|
99
|
+
refl=True,
|
|
100
|
+
):
|
|
101
|
+
"""Initialization."""
|
|
102
|
+
self._d = d_sphere
|
|
103
|
+
self._r_wall = r_wall
|
|
104
|
+
self._r_std = r_std
|
|
105
|
+
self.refl = refl
|
|
106
|
+
R = d_sphere / 2
|
|
107
|
+
self.sample = iad.Port(self, d_sample, uru=0, x=0, y=0, z=-R)
|
|
108
|
+
self.detector = iad.Port(self, d_detector, uru=r_detector, x=R, y=0, z=0)
|
|
109
|
+
self.third = iad.Port(self, d_third, uru=r_third, x=0, y=0, z=R)
|
|
110
|
+
self._a_wall = 1 - self.sample.a - self.third.a - self.detector.a
|
|
111
|
+
self.x = 0
|
|
112
|
+
self.y = 0
|
|
113
|
+
self.z = 0
|
|
114
|
+
self.baffle = False
|
|
115
|
+
self.weight = 0
|
|
116
|
+
|
|
117
|
+
def __repr__(self):
|
|
118
|
+
"""Return basic details as a string for printing."""
|
|
119
|
+
s = ""
|
|
120
|
+
s += "Sphere: d=%s, " % iad.stringify("%5.2f", self.d)
|
|
121
|
+
s += "r_wall=%s, " % iad.stringify("%5.1f%%", self.r_wall * 100)
|
|
122
|
+
s += "r_std=%s, " % iad.stringify("%5.1f%%", self.r_std * 100)
|
|
123
|
+
s += "baffle= %s\n" % self.baffle
|
|
124
|
+
s += " Sample: " + repr(self.sample)
|
|
125
|
+
s += " Third: " + repr(self.third)
|
|
126
|
+
s += " Detector: " + repr(self.detector)
|
|
127
|
+
return s
|
|
128
|
+
|
|
129
|
+
def __str__(self):
|
|
130
|
+
"""Return full details as a string for printing."""
|
|
131
|
+
if self.refl:
|
|
132
|
+
s = "Reflectance "
|
|
133
|
+
else:
|
|
134
|
+
s = "Transmittance "
|
|
135
|
+
s += "Sphere\n"
|
|
136
|
+
s += " diameter = %s mm\n" % iad.stringify("%7.2f", self.d)
|
|
137
|
+
s += " radius = %s mm\n" % iad.stringify("%7.2f", self.d / 2)
|
|
138
|
+
s += " relative area = %s\n" % iad.stringify("%7.1f%%", self.a_wall * 100)
|
|
139
|
+
s += " uru walls = %s\n" % iad.stringify("%7.1f%%", self.r_wall * 100)
|
|
140
|
+
s += " uru standard = %s\n" % iad.stringify("%7.1f%%", self.r_std * 100)
|
|
141
|
+
s += " baffle = %s\n" % self.baffle
|
|
142
|
+
s += "Sample Port\n" + str(self.sample)
|
|
143
|
+
if self.refl:
|
|
144
|
+
s += "Entrance Port\n" + str(self.third)
|
|
145
|
+
else:
|
|
146
|
+
s += "Third Port\n" + str(self.third)
|
|
147
|
+
s += "Detector Port\n" + str(self.detector)
|
|
148
|
+
s += "Gain range\n"
|
|
149
|
+
s += " (sample uru = 0%%) gain = %s\n" % iad.stringify("%7.3f", self.gain(0.0))
|
|
150
|
+
ss = iad.stringify("%7.3f", self.gain(self.sample.uru))
|
|
151
|
+
s += " (sample uru = %3.0f%%) gain = %s\n" % (self.sample.uru * 100, ss)
|
|
152
|
+
s += " (sample uru = std) gain = %s\n" % iad.stringify("%7.3f", self.gain(self.r_std))
|
|
153
|
+
s += " (sample uru = 100%%) gain = %s\n" % iad.stringify("%7.3f", self.gain(1.0))
|
|
154
|
+
return s
|
|
155
|
+
|
|
156
|
+
def gain(self, sample_uru=None, third_uru=None):
|
|
157
|
+
"""
|
|
158
|
+
Determine gain on detector due to multiple bounces in the sphere.
|
|
159
|
+
|
|
160
|
+
If UX1 is the power passing through the sample UT1 or the power reflected by the
|
|
161
|
+
sample UR1, then power falling on the detector port will be
|
|
162
|
+
|
|
163
|
+
P_detector = a_detector * gain * UX1 * P_0
|
|
164
|
+
|
|
165
|
+
where P_0 is the incident light. The power detected will be reduced by the
|
|
166
|
+
reflectivity of the detector
|
|
167
|
+
|
|
168
|
+
P_detected = (1-r_detector) * P_detector
|
|
169
|
+
"""
|
|
170
|
+
if sample_uru is None:
|
|
171
|
+
sample_uru = self.sample.uru
|
|
172
|
+
|
|
173
|
+
if third_uru is None:
|
|
174
|
+
third_uru = self.third.uru
|
|
175
|
+
|
|
176
|
+
if self.baffle:
|
|
177
|
+
tmp = self.detector.a * self.detector.uru + self.sample.a * sample_uru
|
|
178
|
+
r = self.r_wall + (self.third.a / self._a_wall) * third_uru
|
|
179
|
+
denom = 1 - r * (self._a_wall + (1 - self.third.a) * tmp)
|
|
180
|
+
else:
|
|
181
|
+
denom = 1 - self._a_wall * self.r_wall
|
|
182
|
+
denom -= self.detector.a * self.detector.uru
|
|
183
|
+
denom -= self.sample.a * sample_uru
|
|
184
|
+
denom -= self.third.a * third_uru
|
|
185
|
+
|
|
186
|
+
denom = np.asarray(denom)
|
|
187
|
+
return np.where(denom == 0, np.inf, 1 / denom)
|
|
188
|
+
|
|
189
|
+
def MR(self, sample_ur1, sample_uru=None, R_u=0, f_u=1, f_w=0):
|
|
190
|
+
"""
|
|
191
|
+
Determine the value of MR due to multiple bounces in the sphere.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
sample_ur1: The reflectance of the sample for normal illumination.
|
|
195
|
+
sample_uru: The reflectance of the sample for diffuse illumination.
|
|
196
|
+
R_u (optional): The unscattered reflectance from the sample.
|
|
197
|
+
f_u (optional): The fraction of unscattered reflected light collected.
|
|
198
|
+
f_w (optional): The fraction of light that hits the sphere wall first.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
float: The calibrated measured reflection
|
|
202
|
+
"""
|
|
203
|
+
if sample_uru is None:
|
|
204
|
+
# use collimated total reflectance as approximate value for uru
|
|
205
|
+
sample_uru = sample_ur1
|
|
206
|
+
|
|
207
|
+
r_diffuse = sample_ur1 - R_u
|
|
208
|
+
|
|
209
|
+
r_first = 1
|
|
210
|
+
if self.baffle:
|
|
211
|
+
# light cannot hit detector and must bounce once
|
|
212
|
+
# however, some light can exit the entrance port
|
|
213
|
+
r_first = self.r_wall * (1 - self.third.a)
|
|
214
|
+
|
|
215
|
+
# nothing in sample port or third (entrance) port
|
|
216
|
+
gain_0 = self.gain(0, 0)
|
|
217
|
+
|
|
218
|
+
# sample in sample port, third (entrance) port is empty
|
|
219
|
+
gain = self.gain(sample_uru, 0)
|
|
220
|
+
|
|
221
|
+
# sample port has known standard, third (entrance) port is empty
|
|
222
|
+
gain_cal = self.gain(self.r_std, 0)
|
|
223
|
+
|
|
224
|
+
P_cal = gain_cal * (self.r_std * (1 - f_w) + f_w * self.r_wall)
|
|
225
|
+
P_0 = gain_0 * (f_w * self.r_wall)
|
|
226
|
+
|
|
227
|
+
P_ss = r_first * (r_diffuse * (1 - f_w) + f_w * self.r_wall)
|
|
228
|
+
P_su = self.r_wall * (1 - f_w) * f_u * R_u
|
|
229
|
+
P = gain * (P_ss + P_su)
|
|
230
|
+
|
|
231
|
+
MR = self.r_std * (P - P_0) / (P_cal - P_0)
|
|
232
|
+
|
|
233
|
+
# print("UR1 = %6.3f r_diffuse = %6.3f" % (sample_ur1, r_diffuse))
|
|
234
|
+
# print("URU = %6.3f R_u = %6.3f" % (sample_uru, R_u))
|
|
235
|
+
# print("P_ss = %6.3f P_su = %6.3f" % (P_ss, P_su))
|
|
236
|
+
# print("G_0 = %6.3f P_0 = %6.3f" % (gain_0, P_0))
|
|
237
|
+
# print("G = %6.3f P = %6.3f" % (gain, P))
|
|
238
|
+
# print("G_cal = %6.3f P_cal = %6.3f" % (gain_cal, P_cal))
|
|
239
|
+
# print("MR = %6.3f" % (MR))
|
|
240
|
+
|
|
241
|
+
return MR
|
|
242
|
+
|
|
243
|
+
def MT(self, sample_ut1, sample_uru, T_u=0, f_u=1):
|
|
244
|
+
"""
|
|
245
|
+
Determine the measured transmission (MT) due to multiple bounces in the sphere.
|
|
246
|
+
|
|
247
|
+
The f_u variable describes the fraction of unscattered transmission
|
|
248
|
+
that is collected by the sphere. It is equivalent to the -C option for the iad
|
|
249
|
+
program and the default is that all the unscattered transmission is collected
|
|
250
|
+
(because the third port is blocked). If the third port allows unscattered light
|
|
251
|
+
to leave the sphere then it should be set to zero.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
sample_ut1: The transmission of the sample for normal illumination.
|
|
255
|
+
sample_uru: The reflectance of the sample for diffuse illumination.
|
|
256
|
+
T_u (optional): The unscattered transmission of the sample.
|
|
257
|
+
f_u (optional): The fraction of unscattered transmission collected.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
float: The calculated measured transmission (MT) value.
|
|
261
|
+
"""
|
|
262
|
+
if self.third.a == 0:
|
|
263
|
+
# sample in sample port, third port is always sphere wall
|
|
264
|
+
r_cal = self.r_wall
|
|
265
|
+
r_third = self.r_wall
|
|
266
|
+
|
|
267
|
+
elif f_u == 0:
|
|
268
|
+
# sample in sample port, third port is empty except for calibration
|
|
269
|
+
r_cal = self.r_std
|
|
270
|
+
r_third = 0
|
|
271
|
+
|
|
272
|
+
else:
|
|
273
|
+
# sample in sample port, third port always has known standard
|
|
274
|
+
r_cal = self.r_std
|
|
275
|
+
r_third = self.r_std
|
|
276
|
+
|
|
277
|
+
r_first = 1
|
|
278
|
+
if self.baffle:
|
|
279
|
+
r_first = self.r_wall * (1 - self.third.a) + r_third * self.third.a
|
|
280
|
+
|
|
281
|
+
ut1_diffuse = sample_ut1 - T_u
|
|
282
|
+
|
|
283
|
+
gain = self.gain(sample_uru, r_third)
|
|
284
|
+
gain_cal = self.gain(0, r_cal)
|
|
285
|
+
|
|
286
|
+
P_ss = r_first * ut1_diffuse
|
|
287
|
+
P_su = r_third * T_u * f_u
|
|
288
|
+
P = (P_su + P_ss) * gain
|
|
289
|
+
P_cal = r_cal * gain_cal
|
|
290
|
+
|
|
291
|
+
MT = r_cal * P / P_cal
|
|
292
|
+
|
|
293
|
+
# print("UT1 = %6.3f URU = %6.3f" % (sample_ut1, sample_uru))
|
|
294
|
+
# print("T_u = %6.3f f_u = %5.2f" % (T_u,f_u))
|
|
295
|
+
# print("P_ss = %6.3f P_su = %6.3f" % (P_ss, P_su))
|
|
296
|
+
# print("G = %6.3f P = %6.3f" % (gain, P))
|
|
297
|
+
# print("G_cal = %6.3f P_cal = %6.3f" % (gain_cal, P_cal))
|
|
298
|
+
# print("r_first = %6.3f" % (r_first))
|
|
299
|
+
# print("r_cal = %6.3f" % (r_cal))
|
|
300
|
+
# print("r_third = %6.3f" % (r_third))
|
|
301
|
+
# print("MT = %6.3f" % (MT))
|
|
302
|
+
|
|
303
|
+
return MT
|
|
304
|
+
|
|
305
|
+
def pdetector(self):
|
|
306
|
+
"""Print the detector power."""
|
|
307
|
+
P = (1 - self.third.a) * self.r_wall
|
|
308
|
+
N = 1000
|
|
309
|
+
pd = np.zeros(N)
|
|
310
|
+
pw = np.zeros(N)
|
|
311
|
+
ps = np.zeros(N)
|
|
312
|
+
|
|
313
|
+
pd[0] = self.detector.a * P
|
|
314
|
+
ps[0] = self.sample.a * P
|
|
315
|
+
pw[0] = self.a_wall * P
|
|
316
|
+
|
|
317
|
+
for j in range(N - 1):
|
|
318
|
+
pd[j + 1] = self.detector.a * self.r_wall * pw[j]
|
|
319
|
+
ps[j + 1] = self.sample.a * self.r_wall * pw[j]
|
|
320
|
+
pw[j + 1] = self.a_wall * self.r_wall * pw[j]
|
|
321
|
+
pw[j + 1] += (1 - self.third.a) * self.detector.uru * pd[j]
|
|
322
|
+
pw[j + 1] += (1 - self.third.a) * self.sample.uru * ps[j]
|
|
323
|
+
|
|
324
|
+
sumw = np.cumsum(pw)
|
|
325
|
+
sumw -= sumw[1]
|
|
326
|
+
sumd = np.cumsum(pd)
|
|
327
|
+
print(" k P_d^k P_w^k sum(P_d^k) sum(P_w^k)")
|
|
328
|
+
for j in range(10):
|
|
329
|
+
print("%3d %9.5f %9.5f %9.5f %9.5f" % (j + 1, pd[j], pw[j], sumd[j], sumw[j]))
|
|
330
|
+
print("%3d %9.5f %9.5f %9.5f %9.5f" % (N - 1, pd[N - 1], pw[N - 1], sumd[N - 1], sumw[N - 1]))
|
|
331
|
+
print()
|
|
332
|
+
|
|
333
|
+
pd[0] = self.detector.a * P
|
|
334
|
+
ps[0] = self.sample.a * P
|
|
335
|
+
pw[0] = self.a_wall * P
|
|
336
|
+
|
|
337
|
+
pw[1] = self.a_wall * self.r_wall * pw[0]
|
|
338
|
+
pw[1] += (1 - self.third.a) * self.detector.uru * pd[0]
|
|
339
|
+
pw[1] += (1 - self.third.a) * self.sample.uru * ps[0]
|
|
340
|
+
pd[1] = self.detector.a * self.r_wall * pw[0]
|
|
341
|
+
|
|
342
|
+
beta = 1 - self.third.a
|
|
343
|
+
beta *= self.detector.a * self.detector.uru + self.sample.a * self.sample.uru
|
|
344
|
+
for j in range(1, N - 1):
|
|
345
|
+
pw[j + 1] = self.r_wall * (self.a_wall * pw[j] + beta * pw[j - 1])
|
|
346
|
+
|
|
347
|
+
sumw = np.cumsum(pw)
|
|
348
|
+
sumw -= sumw[1]
|
|
349
|
+
sumd = np.cumsum(pd)
|
|
350
|
+
print(" k P_d^k P_w^k sum(P_d^k) sum(P_w^k)")
|
|
351
|
+
for j in range(10):
|
|
352
|
+
print("%3d %9.5f %9.5f %9.5f %9.5f" % (j + 1, pd[j], pw[j], sumd[j], sumw[j]))
|
|
353
|
+
print("%3d %9.5f %9.5f %9.5f %9.5f" % (N - 1, pd[N - 1], pw[N - 1], sumd[N - 1], sumw[N - 1]))
|
|
354
|
+
print()
|
|
355
|
+
|
|
356
|
+
beta = 1 - self.third.a
|
|
357
|
+
beta *= self.detector.a * self.detector.uru + self.sample.a * self.sample.uru
|
|
358
|
+
numer = self.a_wall**3 * self.r_wall + beta * (2 * self.a_wall + self.a_wall**2 * self.r_wall + beta)
|
|
359
|
+
denom = 1 - self.r_wall * (self.a_wall + beta)
|
|
360
|
+
sum3 = self.r_wall * numer / denom * P
|
|
361
|
+
|
|
362
|
+
pdx = self.detector.a * P
|
|
363
|
+
pdx += self.detector.a * self.r_wall * self.a_wall * P
|
|
364
|
+
pdx += self.detector.a * self.r_wall * (self.a_wall**2 * self.r_wall + beta) * P
|
|
365
|
+
pdx += self.detector.a * self.r_wall * sum3
|
|
366
|
+
|
|
367
|
+
print("%9.5f" % pdx)
|
|
368
|
+
|
|
369
|
+
beta = 1 - self.third.a
|
|
370
|
+
beta *= self.detector.a * self.detector.uru + self.sample.a * self.sample.uru
|
|
371
|
+
pdx = self.detector.a * P / (1 - self.r_wall * (self.a_wall + beta))
|
|
372
|
+
print("%9.5f" % pdx)
|
|
373
|
+
|
|
374
|
+
@property
|
|
375
|
+
def d(self):
|
|
376
|
+
"""Getter property for sphere diameter."""
|
|
377
|
+
return self._d
|
|
378
|
+
|
|
379
|
+
@d.setter
|
|
380
|
+
def d(self, value):
|
|
381
|
+
"""When size is changed ratios become invalid."""
|
|
382
|
+
assert self.sample.d <= value, "sphere must be bigger than sample port"
|
|
383
|
+
assert self.third.d <= value, "sphere must be bigger than third port"
|
|
384
|
+
assert self.detector.d <= value, "sphere must be bigger than detector port"
|
|
385
|
+
self._d = value
|
|
386
|
+
self._a_wall = 1 - self.sample.a - self.third.a - self.detector.a
|
|
387
|
+
|
|
388
|
+
@property
|
|
389
|
+
def a_wall(self):
|
|
390
|
+
"""Getter property for detector port diameter."""
|
|
391
|
+
return self._a_wall
|
|
392
|
+
|
|
393
|
+
@a_wall.setter
|
|
394
|
+
def a_wall(self, value):
|
|
395
|
+
"""Change the relative wall area: a bit crazy."""
|
|
396
|
+
assert 0 <= value <= 1, "relative wall are must be between 0 and 1"
|
|
397
|
+
# Find the diameter of a spherical cap assuming all non-wall
|
|
398
|
+
# port area is assigned to a single sample port
|
|
399
|
+
self.sample.d = 2 * self._d * np.sqrt(value - value**2)
|
|
400
|
+
self.third.d = 0
|
|
401
|
+
self.detector.d = 0
|
|
402
|
+
self._a_wall = value
|
|
403
|
+
|
|
404
|
+
@property
|
|
405
|
+
def r_std(self):
|
|
406
|
+
"""Getter property for reflectance standard."""
|
|
407
|
+
return self._r_std
|
|
408
|
+
|
|
409
|
+
@r_std.setter
|
|
410
|
+
def r_std(self, value):
|
|
411
|
+
"""Change the reflectance standard used."""
|
|
412
|
+
if np.isscalar(value):
|
|
413
|
+
assert 0 <= value <= 1, "Reflectivity of standard must be between 0 and 1"
|
|
414
|
+
else:
|
|
415
|
+
assert 0 <= value.all() <= 1, "Reflectivity of standard must be between 0 and 1"
|
|
416
|
+
self._r_std = value
|
|
417
|
+
self.gain_cal = self.gain(self.r_std)
|
|
418
|
+
|
|
419
|
+
@property
|
|
420
|
+
def r_wall(self):
|
|
421
|
+
"""Getter property for wall reflectivity."""
|
|
422
|
+
return self._r_wall
|
|
423
|
+
|
|
424
|
+
@r_wall.setter
|
|
425
|
+
def r_wall(self, value):
|
|
426
|
+
"""Change the wall reflectivity."""
|
|
427
|
+
if np.isscalar(value):
|
|
428
|
+
assert 0 <= value <= 1, "Reflectivity of standard must be between 0 and 1"
|
|
429
|
+
else:
|
|
430
|
+
assert 0 <= value.all() <= 1, "Reflectivity of standard must be between 0 and 1"
|
|
431
|
+
self._r_wall = value
|
|
432
|
+
|
|
433
|
+
def uniform(self):
|
|
434
|
+
"""
|
|
435
|
+
Generate a point uniformly distributed on the surface of a sphere.
|
|
436
|
+
|
|
437
|
+
Using Gaussian distribution for all three coordinates ensures a
|
|
438
|
+
uniform distribution on the surface of the sphere. See
|
|
439
|
+
|
|
440
|
+
https://math.stackexchange.com/questions/1585975
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
(x, y, z) for a random point on the sphere's surface.
|
|
444
|
+
"""
|
|
445
|
+
while True:
|
|
446
|
+
x = random.gauss(0.0, 1.0)
|
|
447
|
+
y = random.gauss(0.0, 1.0)
|
|
448
|
+
z = random.gauss(0.0, 1.0)
|
|
449
|
+
r = np.sqrt(x**2 + y**2 + z**2)
|
|
450
|
+
if r > 0:
|
|
451
|
+
return np.array([x, y, z]) * (self.d / 2) / r
|
|
452
|
+
|
|
453
|
+
def do_one_photon(self, double=False, weight=1):
|
|
454
|
+
"""
|
|
455
|
+
Bounce photon inside sphere until it leaves.
|
|
456
|
+
|
|
457
|
+
The photon can leave through the third port, the detector port,
|
|
458
|
+
the sample port (transmitted/absorbed by the sample), or be absorbed
|
|
459
|
+
by the sphere walls.
|
|
460
|
+
|
|
461
|
+
This routine keeps propagating a photon until the weight drops to zero
|
|
462
|
+
unless double is True
|
|
463
|
+
|
|
464
|
+
If double is True then
|
|
465
|
+
"""
|
|
466
|
+
bounces = 0
|
|
467
|
+
detected = 0
|
|
468
|
+
transmitted = 0
|
|
469
|
+
|
|
470
|
+
# photon is launched from sample
|
|
471
|
+
last_location = iad.PortType.SAMPLE
|
|
472
|
+
# R = self.d/2
|
|
473
|
+
while weight > 0:
|
|
474
|
+
|
|
475
|
+
# lastx = self.x
|
|
476
|
+
# lasty = self.y
|
|
477
|
+
# lastz = self.z
|
|
478
|
+
self.x, self.y, self.z = self.uniform()
|
|
479
|
+
|
|
480
|
+
if self.detector.hit():
|
|
481
|
+
# avoid hitting self
|
|
482
|
+
if last_location == iad.PortType.DETECTOR:
|
|
483
|
+
continue
|
|
484
|
+
|
|
485
|
+
# sample --> detector prohibited
|
|
486
|
+
if last_location == iad.PortType.SAMPLE and self.baffle:
|
|
487
|
+
continue
|
|
488
|
+
|
|
489
|
+
# vx=self.x-lastx
|
|
490
|
+
# vy=self.y-lasty
|
|
491
|
+
# vz=self.z-lastz
|
|
492
|
+
# RR = np.sqrt(vx*vx+vy*vy+vz*vz)
|
|
493
|
+
# print("n = [%5.2f, %5.2f, %5.2f]" % (self.x/R, self.y/R, self.z/R))
|
|
494
|
+
# print("v = [%5.2f, %5.2f, %5.2f]" % (vx/RR, vy/RR, vz/RR))
|
|
495
|
+
# costheta = abs((vx * self.x) + (vy * self.y) + (vz * self.z))/R/RR
|
|
496
|
+
# print(costheta)
|
|
497
|
+
|
|
498
|
+
# record detected light and update weight
|
|
499
|
+
d_transmitted = weight * (1 - self.detector.uru)
|
|
500
|
+
detected += d_transmitted
|
|
501
|
+
weight -= d_transmitted
|
|
502
|
+
last_location = iad.PortType.DETECTOR
|
|
503
|
+
|
|
504
|
+
elif self.sample.hit():
|
|
505
|
+
# avoid hitting self
|
|
506
|
+
if last_location == iad.PortType.SAMPLE:
|
|
507
|
+
continue
|
|
508
|
+
|
|
509
|
+
# detector --> sample prohibited
|
|
510
|
+
if last_location == iad.PortType.DETECTOR and self.baffle:
|
|
511
|
+
continue
|
|
512
|
+
|
|
513
|
+
last_location = iad.PortType.SAMPLE
|
|
514
|
+
if not double:
|
|
515
|
+
weight *= self.sample.uru # photon stays in sphere
|
|
516
|
+
else:
|
|
517
|
+
# in a double sphere setup, the photon may pass into the second sphere
|
|
518
|
+
# the photon continues with equal weight if it is reflected
|
|
519
|
+
# otherwise the photon wil be absorbed or transmitted.
|
|
520
|
+
if random.random() > self.sample.uru:
|
|
521
|
+
transmitted = weight
|
|
522
|
+
weight = 0
|
|
523
|
+
|
|
524
|
+
elif self.third.hit():
|
|
525
|
+
weight *= self.third.uru
|
|
526
|
+
last_location = iad.PortType.THIRD
|
|
527
|
+
|
|
528
|
+
else:
|
|
529
|
+
# must have hit wall
|
|
530
|
+
weight *= self.r_wall
|
|
531
|
+
last_location = iad.PortType.WALL
|
|
532
|
+
|
|
533
|
+
if 0 < weight < 1e-4:
|
|
534
|
+
if random.random() < 0.1:
|
|
535
|
+
weight *= 10
|
|
536
|
+
else:
|
|
537
|
+
weight = 0
|
|
538
|
+
|
|
539
|
+
bounces += 1
|
|
540
|
+
|
|
541
|
+
return detected, transmitted, bounces
|
|
542
|
+
|
|
543
|
+
def do_N_photons_raw_array(self, N, num_trials=10, double=False):
|
|
544
|
+
"""Do a Monte Carlo simulation with N photons."""
|
|
545
|
+
random.seed(time.time()) # Use current time as seed
|
|
546
|
+
|
|
547
|
+
total_detected = np.zeros(num_trials)
|
|
548
|
+
total_bounces = np.zeros(num_trials)
|
|
549
|
+
|
|
550
|
+
N_per_trial = N // num_trials
|
|
551
|
+
|
|
552
|
+
for j in range(num_trials):
|
|
553
|
+
for _i in range(N_per_trial):
|
|
554
|
+
detected, _, bounces = self.do_one_photon(double=double)
|
|
555
|
+
total_detected[j] += detected
|
|
556
|
+
total_bounces[j] += bounces
|
|
557
|
+
|
|
558
|
+
detected = total_detected / N_per_trial
|
|
559
|
+
bounces = total_bounces / N_per_trial
|
|
560
|
+
return detected, bounces
|
|
561
|
+
|
|
562
|
+
def do_N_photons_gain_array(self, N, num_trials=10, double=False):
|
|
563
|
+
"""Do a Monte Carlo simulation with N photons."""
|
|
564
|
+
detected, bounces = self.do_N_photons_raw_array(N, num_trials, double)
|
|
565
|
+
|
|
566
|
+
# convert from detected to gain
|
|
567
|
+
scale = self.detector.a * (1 - self.detector.uru)
|
|
568
|
+
if self.baffle:
|
|
569
|
+
scale *= (1 - self.third.a) * self.r_wall + self.third.a * self.third.uru
|
|
570
|
+
|
|
571
|
+
gains = detected / scale
|
|
572
|
+
return gains, bounces
|
|
573
|
+
|
|
574
|
+
def do_N_photons_gain(self, N, double=False):
|
|
575
|
+
"""Do a Monte Carlo simulation with N photons."""
|
|
576
|
+
num_trials = 20
|
|
577
|
+
gains, _ = self.do_N_photons_gain_array(N, num_trials, double)
|
|
578
|
+
|
|
579
|
+
gain_ave = np.mean(gains)
|
|
580
|
+
gain_stderr = np.std(gains) / np.sqrt(len(gains))
|
|
581
|
+
|
|
582
|
+
return gain_ave, gain_stderr
|