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/start.py ADDED
@@ -0,0 +1,377 @@
1
+ """Module for generating the starting layer for adding-doubling.
2
+
3
+ Two types of starting methods are possible IGI and Diamond.
4
+
5
+ Example::
6
+
7
+ >>> import iadpython as iad
8
+ >>> s = iad.Sample(a=0.9, b=10, g=0.9, n=1.5)
9
+ >>> r, t = iad.init_layer(s)
10
+ >>> print(r)
11
+ >>> print(t)
12
+ """
13
+
14
+ import numpy as np
15
+ import scipy.linalg
16
+ import iadpython as iad
17
+
18
+ __all__ = (
19
+ "zero_layer",
20
+ "starting_thickness",
21
+ "igi",
22
+ "diamond",
23
+ "thinnest_layer",
24
+ "boundary_layer",
25
+ "boundary_matrices",
26
+ "unscattered_rt",
27
+ )
28
+
29
+
30
+ def zero_layer(sample):
31
+ """Create R and T matrices for thinnest_layer with zero thickness.
32
+
33
+ Need to include quadrature normalization so R+T=1.
34
+ """
35
+ if sample.twonuw is None:
36
+ sample.update_quadrature()
37
+ t = np.diagflat(1 / sample.twonuw)
38
+ r = np.zeros_like(t)
39
+ return r, t
40
+
41
+
42
+ def starting_thickness(sample):
43
+ """Find the best starting thickness to start the doubling process.
44
+
45
+ The criterion is based on an assessment of the (1) round-off error,
46
+ (2) the angular initialization error, and (3) the thickness
47
+ initialization error. Wiscombe concluded that an optimal
48
+ starting thickness depends on the smallest quadrature slab.nu, and
49
+ recommends that when either the infinitesimal generator or diamond
50
+ initialization methods are used then the initial thickness is optimal
51
+ when type 2 and 3 errors are comparable, or when d ≈ µ.
52
+
53
+ Note that round-off is important when the starting thickness is less than
54
+ 1e-4 for diamond initialization and less than 1e-8 for infinitesimal
55
+ generator initialization assuming about 14 significant digits of accuracy.
56
+
57
+ Since the final thickness is determined by repeated doubling, the
58
+ starting thickness is found by dividing by 2 until the starting thickness is
59
+ less than 'mu'. Also we make checks for a thinnest_layer with zero thickness
60
+ and one that infinitely thick.
61
+ """
62
+ if sample.nu is None:
63
+ sample.update_quadrature()
64
+
65
+ if sample.b <= 0:
66
+ return 0.0
67
+
68
+ if sample.b == np.inf:
69
+ return sample.nu[0] / 2.0
70
+
71
+ dd = sample.b_delta_M()
72
+ while dd > sample.nu[0]:
73
+ dd /= 2
74
+
75
+ return dd
76
+
77
+
78
+ def igi(sample):
79
+ r"""Infinitesmal Generator Initialization.
80
+
81
+ igi_start() generates the starting matrix with the inifinitesimal generator method.
82
+ The accuracy is O(d) and assumes that the average irradiance upwards is
83
+ equal to that travelling downwards at the top and the average radiance upwards
84
+ equals that moving upwards from the bottom.
85
+
86
+ Ultimately the formulas for the reflection matrix is
87
+
88
+ .. math:: R_{ij} = a d/(4\nu_i\nu_j) hpm_{ij}
89
+
90
+ and
91
+
92
+ .. math:: T_{ij} = a d/(4\nu_i\nu_j) hpp_{ij} + \delta_{ij}(1-d/\nu_i)/(2\nu_i w_i)
93
+ """
94
+ if sample.nu is None:
95
+ sample.update_quadrature()
96
+
97
+ if sample.b_thinnest is None:
98
+ sample.b_thinnest = starting_thickness(sample)
99
+
100
+ n = sample.quad_pts
101
+ d = sample.b_thinnest
102
+
103
+ if sample.hp is None:
104
+ sample.hp, sample.hm = iad.phase_legendre(sample)
105
+
106
+ temp = sample.a_delta_M() * d / 4 / sample.nu
107
+ R = temp * (sample.hm / sample.nu).T
108
+ T = temp * (sample.hp / sample.nu).T
109
+ T += (1 - d / sample.nu) / sample.twonuw * np.identity(n)
110
+
111
+ return R, T
112
+
113
+
114
+ def diamond(sample):
115
+ """Diamond initialization.
116
+
117
+ The diamond method uses the same `r_hat` and `t_hat` as was used for infinitesimal
118
+ generator method. Division by `2*nu_j*w_j` is not needed until the
119
+ final values for `R` and `T` are formed.
120
+ """
121
+ if sample.nu is None:
122
+ sample.update_quadrature()
123
+
124
+ if sample.b_thinnest is None:
125
+ sample.b_thinnest = starting_thickness(sample)
126
+
127
+ n = sample.quad_pts
128
+ d = sample.b_thinnest
129
+ II = np.identity(n)
130
+
131
+ if sample.hp is None:
132
+ sample.hp, sample.hm = iad.phase_legendre(sample)
133
+
134
+ w = sample.twonuw / sample.nu / 2
135
+ temp = sample.a_delta_M() * d * w / 4
136
+ r_hat = temp * (sample.hm / sample.nu).T
137
+ t_hat = d / (2 * sample.nu) * II - temp * (sample.hp / sample.nu).T
138
+
139
+ C = np.linalg.solve((II + t_hat).T, r_hat.T)
140
+ G = 0.5 * (II + t_hat - C.T @ r_hat).T
141
+ lu, piv = scipy.linalg.lu_factor(G)
142
+
143
+ D = 1 / sample.twonuw * (C * sample.twonuw).T
144
+ R = scipy.linalg.lu_solve((lu, piv), D).T / sample.twonuw
145
+ T = scipy.linalg.lu_solve((lu, piv), II).T / sample.twonuw
146
+ T -= II.T / sample.twonuw
147
+ return R, T
148
+
149
+
150
+ def thinnest_layer(sample):
151
+ """Reflection and transmission matrices for a thin thinnest_layer.
152
+
153
+ The optimal method for calculating a thin thinnest_layer depends on the
154
+ thinness of the thinnest_layer and the quadrature angles. This chooses
155
+ the best method using Wiscombe's criteria.
156
+ """
157
+ if sample.b <= 0:
158
+ return zero_layer(sample)
159
+
160
+ sample.b_thinnest = starting_thickness(sample)
161
+
162
+ if sample.b_thinnest < 1e-4 or sample.b_thinnest < 0.09 * sample.nu[0]:
163
+ return igi(sample)
164
+
165
+ return diamond(sample)
166
+
167
+
168
+ def _boundary(sample, n_i, n_g, n_t, b, nu_i):
169
+ """Find matrix for R and T for air/glass/slab interface.
170
+
171
+ The resulting matrix is a diagonal matrix that is represented as an
172
+ array.
173
+
174
+ The reflection matrix is the same entering or exiting the slab. The
175
+ transmission matrices should differ by a factor of
176
+ (n_slab/n_outside)**4, due to n**2 law of radiance, but there is some
177
+ inconsistency in the program and if I use this principle then regular
178
+ calculations for R and T don't work and the fluence calculations still
179
+ don't work. So punted and took all that code out.
180
+
181
+ """
182
+ r, t = iad.absorbing_glass_RT(n_i, n_g, n_t, nu_i, b)
183
+ r *= sample.twonuw
184
+ return r, t
185
+
186
+
187
+ def boundary_layer(s, top=True):
188
+ """Create reflection and transmission arrays for a boundary_layer.
189
+
190
+ If 'boundary_layer=="top"' then the arrays returned are for the top
191
+ surface and the labels are as expected i.e. 'T01' is the reflection for
192
+ light from air passing to the slab. Otherwise the calculations are made
193
+ for the bottom surface and the labels are backwards i.e. 'T01 == T32'
194
+ and 'T10 == T23', where 0 is the first air slide surface, 1 is the
195
+ slide/slab surface, 2 is the second slide/slab surface, and 3 is the
196
+ bottom slide/air surface.
197
+
198
+ Args:
199
+ s: slab
200
+ top: True if this is the top slide
201
+ Returns:
202
+ R01: reflection array from air to slab
203
+ R10: reflection array from slab to air
204
+ T01: transmission array from air to slab
205
+ T10: transmission array from slab to air
206
+ """
207
+ if s.nu is None:
208
+ s.update_quadrature()
209
+
210
+ n_slab_transport = s.n
211
+ n_slab_boundary = getattr(s, "n_sample_boundary", s.n)
212
+
213
+ if top:
214
+ nu_01 = iad.cos_snell(n_slab_transport, s.nu, iad.fresnel._transport_index(s.n_outer_above))
215
+ R01, T01 = _boundary(s, s.n_outer_above, s.n_above, n_slab_boundary, s.b_above, nu_01)
216
+ R10, T10 = _boundary(s, n_slab_boundary, s.n_above, s.n_outer_above, s.b_above, s.nu)
217
+ else:
218
+ nu_23 = iad.cos_snell(n_slab_transport, s.nu, iad.fresnel._transport_index(s.n_outer_below))
219
+ R10, T10 = _boundary(s, s.n_outer_below, s.n_below, n_slab_boundary, s.b_below, nu_23)
220
+ R01, T01 = _boundary(s, n_slab_boundary, s.n_below, s.n_outer_below, s.b_below, s.nu)
221
+ return R01, R10, T01, T10
222
+
223
+
224
+ def boundary_matrices(s, top=True):
225
+ """Create reflection and transmission matrices for a boundary_layer.
226
+
227
+ These can be treated like any other layer in the adding-doubling
228
+ formalism.
229
+
230
+ Args:
231
+ s: slab
232
+ top: True if this is the top boundary
233
+ Returns:
234
+ R01: reflection array from air to slab
235
+ R10: reflection array from slab to air
236
+ T01: transmission array from air to slab
237
+ T10: transmission array from slab to air
238
+ """
239
+ R01, R10, T01, T10 = boundary_layer(s, top=top)
240
+ rr01 = np.diagflat(R01 / s.twonuw**2)
241
+ rr10 = np.diagflat(R10 / s.twonuw**2)
242
+ tt01 = np.diagflat(T01 / s.twonuw)
243
+ tt10 = np.diagflat(T10 / s.twonuw)
244
+
245
+ return rr01, rr10, tt01, tt10
246
+
247
+
248
+ def unscattered_rt(s):
249
+ """Unscattered reflection and transmission for a slide-slab-slide sandwich.
250
+
251
+ The sample is characterized by the record 'slab'. The total unscattered_layer
252
+ reflection and transmission for oblique irradiance ('urx' and 'utx')
253
+ together with their companions 'uru' and 'utu' for diffuse irradiance.
254
+ The cosine of the incident angle is specified by 'slab.cos_angle'.
255
+
256
+ The way that this routine calculates the diffuse unscattered_layer quantities
257
+ based on the global quadrature angles previously set-up. Consequently,
258
+ these estimates are not exact. In fact if 'n=4' then only two
259
+ quadrature points will actually be used to figure out the diffuse
260
+ reflection and transmission (assuming mismatched boundaries).
261
+
262
+ This algorithm is pretty simple. Since the quadrature angles are all
263
+ chosen assuming points inside the medium, I must calculate the
264
+ corresponding angle for light entering from the outside. If the the
265
+ cosine of this angle is greater than zero then the angle does not
266
+ correspond to a direction in which light is totally internally
267
+ reflected. For this ray, I find the unscattered_layer that would be reflected
268
+ or transmitted from the slab. I multiply this by the quadrature angle
269
+ and weight 'twoaw[i]' to get the total diffuse reflectance and
270
+ transmittance.
271
+
272
+ Oh, yes. The mysterious multiplication by a factor of 'n_slab*n_slab'
273
+ is required to account for the n**2-law of radiance.
274
+ """
275
+ r01, t01 = iad.zero_layer(s)
276
+ r10, t10 = iad.zero_layer(s)
277
+
278
+ nu_out = iad.cos_snell(s.n, s.nu, iad.fresnel._transport_index(s.n_outer_above))
279
+ r01, t01 = iad.specular_rt(
280
+ s.n_above,
281
+ getattr(s, "n_sample_boundary", s.n),
282
+ s.n_below,
283
+ s.b,
284
+ nu_out,
285
+ s.b_above,
286
+ s.b_below,
287
+ n_outer_top=s.n_outer_above,
288
+ n_outer_bot=s.n_outer_below,
289
+ n_slab_transport=s.n,
290
+ )
291
+ r10, t10 = iad.specular_rt(
292
+ s.n_below,
293
+ getattr(s, "n_sample_boundary", s.n),
294
+ s.n_above,
295
+ s.b,
296
+ iad.cos_snell(s.n, s.nu, iad.fresnel._transport_index(s.n_outer_below)),
297
+ s.b_below,
298
+ s.b_above,
299
+ n_outer_top=s.n_outer_below,
300
+ n_outer_bot=s.n_outer_above,
301
+ n_slab_transport=s.n,
302
+ )
303
+
304
+ rr01 = np.diagflat(r01) / s.twonuw
305
+ rr10 = np.diagflat(r10) / s.twonuw
306
+ tt01 = np.diagflat(t01) / s.twonuw
307
+ tt10 = np.diagflat(t10) / s.twonuw
308
+
309
+ return rr01, rr10, tt01, tt10
310
+
311
+
312
+ def unscattered(s):
313
+ """Unscattered reflection and transmission for a slide-slab-slide sandwich.
314
+
315
+ The sample is characterized by the record 'slab'. The total unscattered_layer
316
+ reflection and transmission for oblique irradiance ('urx' and 'utx')
317
+ together with their companions 'uru' and 'utu' for diffuse irradiance.
318
+ The cosine of the incident angle is specified by 'slab.cos_angle'.
319
+
320
+ The way that this routine calculates the diffuse unscattered_layer quantities
321
+ based on the global quadrature angles previously set-up. Consequently,
322
+ these estimates are not exact. In fact if 'n=4' then only two
323
+ quadrature points will actually be used to figure out the diffuse
324
+ reflection and transmission (assuming mismatched boundaries).
325
+
326
+ This algorithm is pretty simple. Since the quadrature angles are all
327
+ chosen assuming points inside the medium, I must calculate the
328
+ corresponding angle for light entering from the outside. If the the
329
+ cosine of this angle is greater than zero then the angle does not
330
+ correspond to a direction in which light is totally internally
331
+ reflected. For this ray, I find the unscattered_layer that would be reflected
332
+ or transmitted from the slab. I multiply this by the quadrature angle
333
+ and weight 'twoaw[i]' to get the total diffuse reflectance and
334
+ transmittance.
335
+
336
+ Oh, yes. The mysterious multiplication by a factor of 'n_slab*n_slab'
337
+ is required to account for the n**2-law of radiance.
338
+ """
339
+ n = s.quad_pts
340
+
341
+ uru = 0
342
+ utu = 0
343
+
344
+ for i in range(n):
345
+ nu_outside = iad.cos_snell(s.n, s.nu[i], iad.fresnel._transport_index(s.n_outer_above))
346
+ if nu_outside > 0:
347
+ r, t = iad.specular_rt(
348
+ s.n_above,
349
+ getattr(s, "n_sample_boundary", s.n),
350
+ s.n_below,
351
+ s.b,
352
+ nu_outside,
353
+ s.b_above,
354
+ s.b_below,
355
+ n_outer_top=s.n_outer_above,
356
+ n_outer_bot=s.n_outer_below,
357
+ n_slab_transport=s.n,
358
+ )
359
+ uru += s.twonuw[i] * r[i, i]
360
+ utu += s.twonuw[i] * t[i, i]
361
+
362
+ ur1, ut1 = iad.specular_rt(
363
+ s.n_above,
364
+ getattr(s, "n_sample_boundary", s.n),
365
+ s.n_below,
366
+ s.b,
367
+ s.nu_0,
368
+ s.b_above,
369
+ s.b_below,
370
+ n_outer_top=s.n_outer_above,
371
+ n_outer_bot=s.n_outer_below,
372
+ n_slab_transport=s.n,
373
+ )
374
+
375
+ uru *= s.n**2
376
+ utu *= s.n**2
377
+ return ur1, ut1, uru, utu
iadpython/txt.py ADDED
@@ -0,0 +1,147 @@
1
+ """Module for reading txt files.
2
+
3
+ This reads an output txt file and saves the parameters into an
4
+ Experiment object.
5
+
6
+ Example::
7
+
8
+ >>> import numpy
9
+ >>> import matplotlib.pyplot as plt
10
+ >>> import iadpython
11
+ >>>
12
+ >>> filename = 'ink.txt'
13
+ >>> exp, data = iadpython.read_txt(filename)
14
+ >>> plt.plot(data.lam, data.mr)
15
+ >>> plt.plot(data.lam, data.rr)
16
+ >>> plt.xlabel("Wavelength")
17
+ >>> plt.ylabel("measured reflectance")
18
+ >>> plt.title(filename)
19
+ >>> plt.show()
20
+ """
21
+
22
+ import os
23
+ import re
24
+ import numpy as np
25
+ import iadpython
26
+
27
+ __all__ = ("read_txt", "IADResult")
28
+
29
+
30
+ class IADResult:
31
+ """Container class results in an iad output file."""
32
+
33
+ def __init__(self):
34
+ """Initialization."""
35
+ self.lam = np.array([0], dtype=float)
36
+ self.mr = np.array([0], dtype=float)
37
+ self.cr = np.array([0], dtype=float)
38
+ self.mt = np.array([0], dtype=float)
39
+ self.ct = np.array([0], dtype=float)
40
+ self.mua = np.array([0], dtype=float)
41
+ self.musp = np.array([0], dtype=float)
42
+ self.g = np.array([0], dtype=float)
43
+ self.success = np.array([0], dtype=bool)
44
+ self.mus = np.array([0], dtype=float)
45
+
46
+
47
+ def verify_magic(fp, magic):
48
+ """Verify that the file's initial bytes match the string 'magic'."""
49
+ fp.seek(0)
50
+ chunk = fp.read(len(magic))
51
+ fp.seek(0)
52
+ return chunk == magic
53
+
54
+
55
+ def get_number_from_line(fp):
56
+ """Read the number on the next line after an = sign."""
57
+ s = fp.readline()
58
+ # print("starting=%s" % s)
59
+ if s[0] != "#":
60
+ print("line in file should start with `#`")
61
+ return 0
62
+
63
+ s = re.sub(r".*= *", "", s)
64
+ s = re.sub(r" .*", "", s)
65
+
66
+ # print("finish x=%10.5f" % float(s))
67
+ return float(s)
68
+
69
+
70
+ def read_sphere(fp):
71
+ """Read the information for a sphere."""
72
+ line = fp.readline()
73
+ d_sphere = get_number_from_line(fp)
74
+ d_sample = get_number_from_line(fp)
75
+ sphere = iadpython.Sphere(d_sphere, d_sample)
76
+ sphere.baffle = "a baffle" in line
77
+ sphere.third.d = get_number_from_line(fp)
78
+ sphere.detector.d = get_number_from_line(fp)
79
+ sphere.detector.uru = get_number_from_line(fp) / 100
80
+ sphere.r_wall = get_number_from_line(fp) / 100
81
+ sphere.r_std = get_number_from_line(fp) / 100
82
+ return sphere
83
+
84
+
85
+ def read_misc(fp, _exp):
86
+ """Read info after sphere data but before data."""
87
+ for _ in range(14):
88
+ fp.readline()
89
+
90
+
91
+ def read_txt(filename):
92
+ """Read an IAD output file in .rxt format."""
93
+ if not os.path.exists(filename):
94
+ raise ValueError('input file "%s" must end in ".txt"' % filename)
95
+
96
+ # verify that file is an output file
97
+ with open(filename, encoding="utf-8") as fp:
98
+ if not verify_magic(fp, "# Inverse Adding-Doubling"):
99
+ raise ValueError('"%s" does not start with "# Inverse Adding-Doubling"' % filename)
100
+
101
+ # create experiment object
102
+ exp = iadpython.Experiment()
103
+
104
+ # now read the header
105
+ fp.readline()
106
+ fp.readline()
107
+ exp.d_beam = get_number_from_line(fp)
108
+ exp.sample.d = get_number_from_line(fp)
109
+ exp.sample.d_above = get_number_from_line(fp)
110
+ exp.sample.d_below = get_number_from_line(fp)
111
+ exp.sample.n = get_number_from_line(fp)
112
+ exp.sample.n_above = get_number_from_line(fp)
113
+ exp.sample.n_below = get_number_from_line(fp)
114
+ fp.readline()
115
+ exp.fraction_of_rc_in_mr = get_number_from_line(fp) / 100
116
+ exp.fraction_of_tc_in_mt = get_number_from_line(fp) / 100
117
+ fp.readline()
118
+ exp.r_sphere = read_sphere(fp)
119
+ fp.readline()
120
+ exp.t_sphere = read_sphere(fp)
121
+ read_misc(fp, exp)
122
+
123
+ data = IADResult()
124
+ position = fp.tell()
125
+
126
+ result = np.loadtxt(fp, usecols=range(8), delimiter="\t")
127
+ lam, mr, cr, mt, ct, mua, musp, g = result.T
128
+ data.lam = np.atleast_1d(lam)
129
+ data.mr = np.atleast_1d(mr)
130
+ data.cr = np.atleast_1d(cr)
131
+ data.mt = np.atleast_1d(mt)
132
+ data.ct = np.atleast_1d(ct)
133
+ data.mua = np.atleast_1d(mua)
134
+ data.musp = np.atleast_1d(musp)
135
+ data.g = np.atleast_1d(g)
136
+ data.mus = data.musp / (1 - g)
137
+ exp.m_r = data.mr
138
+ exp.m_t = data.mt
139
+ exp.lambda0 = data.lam
140
+
141
+ fp.seek(position)
142
+ converters = {8: lambda s: s.lstrip("#").strip()}
143
+ status = np.loadtxt(fp, usecols=[8], dtype=str, converters=converters)
144
+ status = np.atleast_1d(status)
145
+ data.success = status == "*"
146
+
147
+ return exp, data