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/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