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.
@@ -0,0 +1,172 @@
1
+ """Module for obtaining quadrature points and weights.
2
+
3
+ Three types of gaussian quadrature are supported: normal Gaussian,
4
+ Radau quadrature, and Lobatto quadrature. The first method does not
5
+ include either endpoint of integration, Radau quadrature includes one
6
+ endpoint of the integration range, and Lobatto quadrature includes both
7
+ endpoints.
8
+
9
+ Example::
10
+
11
+ >>> import iad.quadrature
12
+
13
+ >>> n=8
14
+ >>> x, w = iad.quadrature.gauss(n)
15
+ >>> print(" i x weight")
16
+ >>> for i,x in enumerate(xi):
17
+ >>> print("%2d %+.12f %+.12f" % (i, x[i], w[i]))
18
+
19
+ >>> x, w = iad.quadrature.radau(n)
20
+ >>> print(" i x weight")
21
+ >>> for i,x in enumerate(xi):
22
+ >>> print("%2d %+.12f %+.12f" % (i, x[i], w[i]))
23
+
24
+ >>> x, w = iad.quadrature.lobatto(n)
25
+ >>> print(" i x weight")
26
+ >>> for i,x in enumerate(xi):
27
+ >>> print("%2d %+.12f %+.12f" % (i, x[i], w[i]))
28
+
29
+ """
30
+
31
+ import functools
32
+ import scipy.special
33
+ import scipy.optimize
34
+ import numpy as np
35
+
36
+ __all__ = (
37
+ "gauss",
38
+ "radau",
39
+ "lobatto",
40
+ )
41
+
42
+
43
+ def _gauss_func(n, x):
44
+ """Zeroes of this function are the Gaussian quadrature points."""
45
+ return scipy.special.legendre(n)(x)
46
+
47
+
48
+ def _radau_func(n, x):
49
+ """Zeros of this function are the Radau quadrature points."""
50
+ return (scipy.special.eval_legendre(n - 1, x) + scipy.special.eval_legendre(n, x)) / (1 + x)
51
+
52
+
53
+ def _lobatto_func(n, x):
54
+ """Zeros of this function are the Lobatto quadrature points."""
55
+ return scipy.special.legendre(n - 1).deriv(1)(x)
56
+
57
+
58
+ def gauss(n, a=-1, b=1):
59
+ """Return abscissas and weights for Gaussian quadrature.
60
+
61
+ The definite integral ranges from a to b. The default
62
+ interval is -1 to 1. The quadrature approximation is
63
+ just the sum of :math:`w_i f(x_i)`. Neither a nor b is included
64
+ in the list of quadrature abscissas.
65
+
66
+ The result should be exact when integrating any polynomial
67
+ of degree 2n-1 or less.
68
+
69
+ If -a=b, then abscissas will be symmetric about the origin
70
+
71
+ Args:
72
+ n: number of quadrature points
73
+ a: lower limit of integral
74
+ b: upper limit of integral
75
+ Returns:
76
+ x: array of abscissas of length n
77
+ w: array of weights of length n
78
+ """
79
+ x, w, _ = scipy.special.roots_legendre(n, mu=True)
80
+
81
+ # scale for desired interval
82
+ x *= 0.5 * (a - b)
83
+ x += 0.5 * (a + b)
84
+ w *= 0.5 * (b - a)
85
+
86
+ return np.flip(x), np.flip(w)
87
+
88
+
89
+ def radau(n, a=-1, b=1):
90
+ """Return abscissas and weights for Radau quadrature.
91
+
92
+ The definite integral ranges from a to b. The default
93
+ interval is -1 to 1. The quadrature approximation is
94
+ just the sum of :math:`w_i f(x_i)`. The upper endpoint b is include
95
+ in the list of quadrature abscissas.
96
+
97
+ The result should be exact when integrating any polynomial
98
+ of degree 2n-2 or less.
99
+
100
+ Args:
101
+ n: number of quadrature points
102
+ a: lower limit of integral
103
+ b: upper limit of integral
104
+ Returns:
105
+ x: array of abscissas of length n
106
+ w: array of weights of length n
107
+ """
108
+ x = np.zeros(n)
109
+ w = np.zeros(n)
110
+ x[0] = -1
111
+
112
+ # the roots of P_{n} bracket the roots of P_{n}':
113
+ brackets = scipy.special.roots_legendre(n)[0]
114
+
115
+ f = functools.partial(_radau_func, n)
116
+ for i in range(n - 1):
117
+ x[i + 1] = scipy.optimize.brentq(f, brackets[i], brackets[i + 1])
118
+
119
+ pp = scipy.special.legendre(n - 1).deriv(1)
120
+ w[0] = 2 / n**2
121
+ w[1:] = 1 / pp(x[1:]) ** 2 / (1 - x[1:])
122
+
123
+ # scale for desired interval
124
+ x *= 0.5 * (a - b)
125
+ x += 0.5 * (b + a)
126
+ w *= 0.5 * (b - a)
127
+
128
+ return np.flip(x), np.flip(w)
129
+
130
+
131
+ def lobatto(n, a=-1, b=1):
132
+ """Return abscissas and weights for Lobatto quadrature.
133
+
134
+ The definite integral ranges from a to b. The default
135
+ interval is -1 to 1. The quadrature approximation is
136
+ just the sum of w_i f(x_i). Both endpoints a and b are include
137
+ in the list of quadrature abscissas.
138
+
139
+ The result should be exact when integrating any polynomial
140
+ of degree 2n-3 or less.
141
+
142
+ If -a=b, then abscissas will be symmetric about the origin
143
+
144
+ Args:
145
+ n: number of quadrature points
146
+ a: lower limit of integral
147
+ b: upper limit of integral
148
+ Returns:
149
+ x: array of abscissas of length n
150
+ w: array of weights of length n
151
+ """
152
+ x = np.zeros(n)
153
+ w = np.full(n, 2 / n / (n - 1))
154
+ x[0] = -1
155
+ x[-1] = 1
156
+
157
+ # The roots of P_{n-1} bracket the roots of P_{n-1}':
158
+ brackets = scipy.special.roots_legendre(n - 1)[0]
159
+
160
+ f = functools.partial(_lobatto_func, n)
161
+ for i in range(n - 2):
162
+ x[i + 1] = scipy.optimize.brentq(f, brackets[i], brackets[i + 1])
163
+
164
+ pp = scipy.special.legendre(n - 1)(x)
165
+ w[1:-1] = w[1:-1] / pp[1:-1] ** 2
166
+
167
+ # scale for desired interval
168
+ x *= 0.5 * (a - b)
169
+ x += 0.5 * (a + b)
170
+ w *= 0.5 * (b - a)
171
+
172
+ return np.flip(x), np.flip(w)
@@ -0,0 +1,318 @@
1
+ r"""Module for calculating the redistribution function.
2
+
3
+ The single scattering phase function ..math:`p(\nu)` for a tissue determines the
4
+ amount of light scattered at an angle nu=cos(theta) from the direction of
5
+ incidence. The subtended angle nu is the dot product incident and exiting
6
+ of the unit vectors.
7
+
8
+ The redistribution function ..math:`h[i,j]` determines the fraction of light
9
+ scattered from an incidence cone with angle `\nu_i` into a cone with angle
10
+ ..math:`\nu_j`. The redistribution function is calculated by averaging the phase
11
+ function over all possible azimuthal angles for fixed angles ..math:`\nu_i` and
12
+ ..math:`nu_j`,
13
+
14
+ Note that the angles ..math:`\nu_i` and ..math:`\nu_j` may also be negative (light
15
+ travelling in the opposite direction).
16
+
17
+ When the cosine of the angle of incidence or exitance is unity (..math:`\nu_i=1` or
18
+ ..math:`\nu_j=1`), then the redistribution function is equivalent to the phase
19
+ function ..math:`p(\nu_j)`.
20
+ """
21
+
22
+ import scipy.special
23
+ import scipy.integrate
24
+ import warnings
25
+ import numpy as np
26
+ from numpy.polynomial.legendre import legvander, leggauss
27
+ from scipy.special import eval_legendre
28
+ import pandas as pd
29
+ from scipy.interpolate import interp1d, CubicSpline
30
+
31
+ __all__ = ('hg_elliptic',
32
+ 'phase_legendre',
33
+ 'legendre_coeffs_from_df')
34
+
35
+
36
+ def _prepare_moments(pf_data, n_needed):
37
+ """Validate and normalize a ``pf_type='MOMENTS'`` array.
38
+
39
+ Shared by ``phase_legendre`` and ``Sample._deltaM_forward_fraction`` so
40
+ both consumers of a supplied moments array agree on the same validation
41
+ and normalization convention -- rather than each re-deriving it and
42
+ risking drift.
43
+
44
+ Parameters
45
+ ----------
46
+ pf_data : array-like, Legendre moments a_l (index 0 = a_0)
47
+ n_needed : minimum number of moments required
48
+
49
+ Returns
50
+ -------
51
+ a_raw : ndarray, shape (n_needed,), normalized so a_0 = 1 (matching
52
+ legendre_coeffs_from_df's convention; an identically-zero
53
+ spectrum -- matched-index / no-scattering limit -- stays zero
54
+ instead of raising a division-by-zero).
55
+
56
+ Raises
57
+ ------
58
+ TypeError if pf_data is DataFrame-like (np.asarray would otherwise
59
+ silently coerce it instead of failing).
60
+ ValueError if pf_data is not 1-D once materialized, or has fewer than
61
+ n_needed entries.
62
+ """
63
+ if hasattr(pf_data, "iloc"):
64
+ raise TypeError("pf_type='MOMENTS' requires a plain array, not a DataFrame")
65
+ a_raw = np.atleast_1d(np.asarray(pf_data, dtype=float))
66
+ if a_raw.ndim != 1:
67
+ raise ValueError(
68
+ "MOMENTS pf_data must be 1-D once materialized for a single "
69
+ "wavelength, got shape %s" % (a_raw.shape,)
70
+ )
71
+ if a_raw.size < n_needed:
72
+ raise ValueError(
73
+ "pf_type='MOMENTS' needs at least %d moments, got %d" % (n_needed, a_raw.size)
74
+ )
75
+ a_raw = a_raw[:n_needed].copy()
76
+ a0 = a_raw[0]
77
+ if np.isclose(a0, 0.0):
78
+ a_raw[:] = 0.0
79
+ else:
80
+ a_raw /= a0
81
+ return a_raw
82
+
83
+ def legendre_coeffs_from_df(
84
+ df, *, quad_pts=8, n_mom=None, spline_bc='not-a-knot'):
85
+ """
86
+ Exact Legendre moments a_l (no ½(2l+1) factor) for a phase function
87
+ tabulated on μ = cosθ.
88
+
89
+ Parameters
90
+ ----------
91
+ df : pandas.DataFrame
92
+ index = μ (strictly monotonic), columns = spectra
93
+ quad_pts : kept only for API symmetry (default 8)
94
+ n_mom : highest Legendre order (default 2*quad_pts)
95
+ spline_bc : boundary condition passed to CubicSpline
96
+
97
+ Returns
98
+ -------
99
+ a_raw : ndarray, shape (n_mom, nλ)
100
+ Legendre moments a_l = ∫_{-1}^1 p(μ) P_l(μ) dμ , l=0…n_mom-1
101
+ *normalised* so that a_0 = 1 for every spectrum.
102
+ """
103
+ # ---------- sanity -------------------------------------------------
104
+ if not isinstance(df, pd.DataFrame):
105
+ raise TypeError("df must be a pandas DataFrame.")
106
+
107
+ df = df.sort_index() # ascending μ for CubicSpline
108
+ mu = df.index.to_numpy() # ascending version
109
+ p = df.to_numpy().T # shape (nλ, Nμ)
110
+
111
+ if n_mom is None:
112
+ n_mom = 2 * quad_pts + 1 # Wiscombe default
113
+
114
+ nλ = p.shape[0]
115
+
116
+ # Adaptive quadrature: use fewer points for smaller n_mom
117
+ nquad = max(32, min(128, 4 * n_mom))
118
+ g_x, g_w = leggauss(nquad)
119
+
120
+ # Evaluate Legendre polynomials once at quadrature nodes
121
+ P_all = np.array([eval_legendre(l, g_x) for l in range(n_mom)]) # (n_mom, nquad)
122
+
123
+ # Vectorized spline evaluation: build all splines and evaluate at once
124
+ vals_all = np.zeros((nquad, nλ))
125
+ for col, y in enumerate(p):
126
+ f = CubicSpline(mu, y, bc_type=spline_bc)
127
+ vals_all[:, col] = f(g_x)
128
+
129
+ # Vectorized integration: compute all moments for all spectra at once
130
+ a_raw = P_all @ (vals_all * g_w[:, None]) # (n_mom, nquad) @ (nquad, nλ) = (n_mom, nλ)
131
+
132
+ # Normalize so a_0 = 1 for each spectrum. When the supplied phase
133
+ # function is identically zero (matched-index / no-scattering limits),
134
+ # leave all moments at zero instead of creating NaNs.
135
+ a0 = a_raw[0, :]
136
+ zero_mask = np.isclose(a0, 0.0)
137
+ if np.any(~zero_mask):
138
+ a_raw[:, ~zero_mask] /= a0[~zero_mask]
139
+ if np.any(zero_mask):
140
+ a_raw[:, zero_mask] = 0.0
141
+
142
+ return a_raw
143
+
144
+ # ----------------------------------------------------------------------
145
+ # unified HG / tabulated redistribution via Legendre polynomials
146
+ # ----------------------------------------------------------------------
147
+ def phase_legendre(sample, *, deltam=True):
148
+ """
149
+ Build (hp, hm) single-scatter redistribution blocks.
150
+
151
+ Modes
152
+ -----
153
+ * pf_type == 'HG' : analytic Henyey–Greenstein (fast)
154
+ * pf_type == 'TABULATED' : DataFrame in sample.pf_data
155
+ → Legendre coeffs via helper above
156
+ * pf_type == 'MOMENTS' : Legendre moments a_l supplied directly in
157
+ sample.pf_data (bypasses the spline+
158
+ quadrature TABULATED needs -- for callers
159
+ that can evaluate their phase function
160
+ exactly at arbitrary angles and integrate
161
+ it themselves, e.g. via Gauss-Legendre
162
+ quadrature on mu=cos(theta))
163
+ Parameters
164
+ ----------
165
+ sample : iadpython.Sample (must have quad_pts, nu, etc.)
166
+ deltam : bool apply δ-M forward-spike split
167
+
168
+ Returns
169
+ -------
170
+ hp, hm : ndarray (N × N)
171
+ """
172
+
173
+ if sample.nu is None:
174
+ sample.update_quadrature()
175
+
176
+ mu = sample.nu
177
+ n = sample.quad_pts
178
+ P = legvander(mu, 2*n).T # (2N+1, N)
179
+ w = sample.twonuw / (2*mu)
180
+
181
+ pf_type = getattr(sample, "pf_type", "HG").upper()
182
+
183
+ # ---------------------------------------------------- HG fast path
184
+ if pf_type == "HG":
185
+ g = float(sample.g)
186
+ if g == 0.0:
187
+ hp = hm = np.ones((n, n))
188
+ return hp, hm
189
+ a_raw = g ** np.arange(2*n+1) # analytic a_ℓ = g^ℓ
190
+
191
+ # -------------------------------------------- Custom tabulated path
192
+ elif pf_type == "TABULATED":
193
+ df = sample.pf_data
194
+ a_raw = legendre_coeffs_from_df(df,
195
+ quad_pts=n,
196
+ n_mom=2*n+1) # (2N+1 , nλ=1)
197
+ a_raw = a_raw.squeeze() # 1-D
198
+
199
+ # ----------------------------------------------- Direct moments path
200
+ elif pf_type == "MOMENTS":
201
+ a_raw = _prepare_moments(sample.pf_data, 2*n+1)
202
+
203
+ else:
204
+ raise ValueError(f"Unknown pf_type '{pf_type}'")
205
+
206
+ # -------------------------------------------- optional δ-M trunc
207
+ if deltam:
208
+ f_spike_end = 1 - 1E-10
209
+ f_spike = float(np.clip(a_raw[n], 0.0, f_spike_end)) # order-n moment; matches Sample._deltaM_forward_fraction (HG: g**n)
210
+ a_use = (a_raw[:n] - f_spike) / (1.0 - f_spike)
211
+ Lmax = n
212
+ P_use = P[:Lmax]
213
+
214
+ else:
215
+ a_use = a_raw[:2*n+1]
216
+ Lmax = 2*n+1
217
+ P_use = P[:Lmax]
218
+
219
+ chi = (2*np.arange(Lmax) + 1) * a_use # χ_ℓ weights
220
+
221
+ # -------------------------------------------- assemble blocks
222
+ hp = np.ones((n, n))
223
+ hm = np.ones((n, n))
224
+
225
+ for k in range(1, Lmax):
226
+ pk = P_use[k]
227
+ add = chi[k] * np.outer(pk, pk)
228
+ hp += add
229
+ hm += (-1)**k * add
230
+ return hp, hm
231
+
232
+
233
+ def hg_elliptic(sample):
234
+ """Calculate redistribution function using elliptic integrals.
235
+
236
+ This is the result of a direct integration of the Henyey-
237
+ Greenstein phase function.
238
+
239
+ It is not terribly useful because we cannot use the
240
+ delta-M method to more accurate model highly anisotropic
241
+ phase functions.
242
+ """
243
+ if sample.nu is None:
244
+ sample.update_quadrature()
245
+
246
+ n = sample.quad_pts
247
+ g = sample.g**n
248
+ if g == 0:
249
+ h = np.ones([n, n])
250
+ return h, h
251
+
252
+ hp = np.zeros([n, n])
253
+ hm = np.zeros([n, n])
254
+ for i in range(n):
255
+ for j in range(i + 1):
256
+ ni = sample.nu[i]
257
+ nj = sample.nu[j]
258
+ gamma = 2 * g * np.sqrt(1 - ni**2) * np.sqrt(1 - nj**2)
259
+
260
+ alpha = 1 + g**2 - 2 * g * ni * nj
261
+ const = 2 / np.pi * (1 - g**2) / (alpha - gamma) / np.sqrt(alpha + gamma)
262
+ arg = np.sqrt(2 * gamma / (alpha + gamma))
263
+ hp[i, j] = const * scipy.special.ellipe(arg)
264
+
265
+ alpha = 1 + g**2 + 2 * g * ni * nj
266
+ const = 2 / np.pi * (1 - g**2) / (alpha - gamma) / np.sqrt(alpha + gamma)
267
+ arg = np.sqrt(2 * gamma / (alpha + gamma))
268
+ hm[i, j] = const * scipy.special.ellipe(arg)
269
+
270
+ # fill symmetric entries
271
+ for i in range(n):
272
+ for j in range(i + 1, n):
273
+ hp[i, j] = hp[j, i]
274
+ hm[i, j] = hm[j, i]
275
+
276
+ return hp, hm
277
+
278
+ def legendre_moments(g=None, custom_phase_function=None, max_order=16):
279
+ """
280
+ Compute the Legendre expansion coefficients (chi_k) of a phase function.
281
+
282
+ If custom_phase_function is provided, compute chi_k via integration over [-1, 1].
283
+ Otherwise, use Henyey-Greenstein with parameter g.
284
+ If both are None, assume isotropic scattering (g=0) and issue a warning.
285
+
286
+ Arguments:
287
+ g: Anisotropy factor for Henyey-Greenstein (float).
288
+ custom_phase_function: Function of mu (float in [-1, 1]) returning P(mu).
289
+ max_order: Number of Legendre moments to compute (int).
290
+
291
+ Returns:
292
+ chi: array of chi_k coefficients.
293
+ """
294
+ chi = np.zeros(max_order)
295
+
296
+ if custom_phase_function is not None:
297
+ if g is not None:
298
+ warnings.warn("custom_phase_function is set; g will be ignored.")
299
+
300
+ for k in range(max_order):
301
+ integrand = lambda mu: custom_phase_function(mu) * scipy.special.legendre(k)(mu)
302
+ chi[k], _ = scipy.integrate.quad(integrand, -1, 1)
303
+ chi[k] *= (2 * k + 1) / 2
304
+
305
+ elif isinstance(g, (float, int)):
306
+ for k in range(max_order):
307
+ chi[k] = (2 * k + 1) * (g ** k - g ** max_order) / (1 - g ** max_order)
308
+
309
+ elif g is not None:
310
+ raise TypeError("g must be a float or int if provided.")
311
+
312
+ else:
313
+ warnings.warn("Neither g nor custom_phase_function provided. Proceeding with isotropic Henyey-Greenstein (g=0).")
314
+ g = 0
315
+ for k in range(max_order):
316
+ chi[k] = (2 * k + 1) * (g ** k - g ** max_order) / (1 - g ** max_order) if g != 1 else 0
317
+
318
+ return chi
iadpython/rxt.py ADDED
@@ -0,0 +1,201 @@
1
+ """Module for reading rxt files.
2
+
3
+ This reads an input rxt 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.rxt'
13
+ >>> exp = iadpython.read_rxt(filename)
14
+ >>> if exp.lambda0 is None:
15
+ >>> plt.plot(exp.m_r)
16
+ >>> else:
17
+ >>> plt.plot(exp.lambda0, exp.m_r)
18
+ >>> plt.ylabel("measured reflectance")
19
+ >>> plt.title(filename)
20
+ >>> plt.show()
21
+ """
22
+
23
+ import os
24
+ import re
25
+ import numpy as np
26
+ import iadpython
27
+
28
+ __all__ = ("read_rxt", "read_and_remove_notation")
29
+
30
+
31
+ def read_and_remove_notation(filename):
32
+ """Read file and remove all whitespace and comments."""
33
+ s = ""
34
+
35
+ if not os.path.exists(filename):
36
+ raise ValueError('input file "%s" must end in ".rxt"' % filename)
37
+
38
+ with open(filename, encoding="utf-8") as f:
39
+ for line in f:
40
+ line = re.sub(r"\s*#.*", "", line)
41
+ line = re.sub(r", ", " ", line)
42
+ s += line
43
+
44
+ if len(re.findall("IAD1", s)) == 0:
45
+ raise ValueError("Not an .rxt file. (Does not start with IAD1)")
46
+
47
+ s = re.sub(r"IAD1", "", s)
48
+ s = re.sub(r"\s+", " ", s)
49
+ s = s.rstrip()
50
+ s = s.lstrip()
51
+ return s
52
+
53
+
54
+ def fill_in_data_fixed(exp, data_in_columns):
55
+ """Read data the old way by fixed column position."""
56
+ col = 0
57
+ if data_in_columns[0, 0] > 1:
58
+ exp.lambda0 = data_in_columns[:, 0]
59
+ col = 1
60
+ exp.m_r = data_in_columns[:, col]
61
+ if exp.num_measures >= 2:
62
+ exp.m_t = data_in_columns[:, col + 1]
63
+ if exp.num_measures >= 3:
64
+ exp.m_u = data_in_columns[:, col + 2]
65
+ if exp.num_measures >= 4:
66
+ exp.r_sphere.r_wall = data_in_columns[:, col + 3]
67
+ if exp.num_measures >= 5:
68
+ exp.t_sphere.r_wall = data_in_columns[:, col + 4]
69
+ if exp.num_measures >= 6:
70
+ exp.r_sphere.r_std = data_in_columns[:, col + 5]
71
+ if exp.num_measures >= 7:
72
+ exp.t_sphere.r_std = data_in_columns[:, col + 6]
73
+ if exp.num_measures > 7:
74
+ raise ValueError("unimplemented")
75
+
76
+
77
+ def fill_in_data_variable(exp, data_in_columns, column_letters_str):
78
+ """Read data and interpret according to column_letters_str."""
79
+ for col, letter in enumerate(column_letters_str):
80
+ if letter == "a":
81
+ exp.default_a = data_in_columns[:, col]
82
+ elif letter == "b":
83
+ exp.default_b = data_in_columns[:, col]
84
+ elif letter == "B":
85
+ exp.d_beam = data_in_columns[:, col]
86
+ elif letter == "c":
87
+ exp.fraction_of_rc_in_mr = data_in_columns[:, col]
88
+ elif letter == "C":
89
+ exp.fraction_of_tc_in_mt = data_in_columns[:, col]
90
+ elif letter == "d":
91
+ exp.sample.d = data_in_columns[:, col]
92
+ elif letter == "D":
93
+ exp.sample.d_above = data_in_columns[:, col]
94
+ exp.sample.d_below = data_in_columns[:, col]
95
+ elif letter == "E":
96
+ exp.sample.b_above = data_in_columns[:, col]
97
+ exp.sample.b_below = data_in_columns[:, col]
98
+ elif letter == "e":
99
+ exp.error = data_in_columns[:, col]
100
+ elif letter == "g":
101
+ exp.default_g = data_in_columns[:, col]
102
+ elif letter == "t":
103
+ exp.m_t = data_in_columns[:, col]
104
+ elif letter == "L":
105
+ exp.lambda0 = data_in_columns[:, col]
106
+ elif letter == "n":
107
+ exp.n = data_in_columns[:, col]
108
+ elif letter == "N":
109
+ exp.n_above = data_in_columns[:, col]
110
+ exp.n_below = data_in_columns[:, col]
111
+ elif letter == "r":
112
+ exp.m_r = data_in_columns[:, col]
113
+ elif letter == "R":
114
+ exp.r_sphere.r_std = data_in_columns[:, col]
115
+ elif letter == "S":
116
+ exp.num_spheres = data_in_columns[:, col]
117
+ elif letter == "T":
118
+ exp.t_sphere.r_rstd = data_in_columns[:, col]
119
+ elif letter == "u":
120
+ exp.m_u = data_in_columns[:, col]
121
+ elif letter == "w":
122
+ exp.r_sphere.r_wall = data_in_columns[:, col]
123
+ elif letter == "W":
124
+ exp.t_sphere.r_wall = data_in_columns[:, col]
125
+ else:
126
+ raise ValueError('unimplemented column type "%s"' % letter)
127
+
128
+
129
+ def read_rxt(filename):
130
+ """Read an IAD input file in .rxt format.
131
+
132
+ Args:
133
+ filename: .rxt filename
134
+
135
+ Returns:
136
+ Experiment object
137
+ """
138
+ s = read_and_remove_notation(filename)
139
+ x = s.split(" ")
140
+
141
+ # Remove single-letter entries and save them
142
+ column_letters = [item for item in x if len(item) == 1 and item.isalpha()]
143
+ x = [item for item in x if not (len(item) == 1 and item.isalpha())]
144
+ column_letters_str = "".join(column_letters)
145
+
146
+ x = np.array([float(item) for item in x])
147
+
148
+ sample = iadpython.Sample(a=None, b=None, g=None)
149
+ sample.n = x[0]
150
+ sample.n_above = x[1]
151
+ sample.d = x[2]
152
+ sample.d_above = x[3]
153
+
154
+ # try and save people from themselves
155
+ if sample.d_above == 0:
156
+ sample.n_above = 1
157
+ if sample.n_above == 1:
158
+ sample.d_above = 0
159
+ if sample.n_above == 0:
160
+ sample.n_above = 1
161
+ sample.d_above = 0
162
+ sample.d_below = sample.d_above
163
+ sample.n_below = sample.n_above
164
+ exp = iadpython.Experiment(sample=sample)
165
+
166
+ exp.d_beam = x[4]
167
+ exp.rstd_r = x[5]
168
+ exp.num_spheres = x[6]
169
+ exp.method = "substitution"
170
+
171
+ if exp.num_spheres > 0:
172
+ exp.r_sphere = iadpython.Sphere(x[7], x[8], x[9], x[10], 0, x[11])
173
+
174
+ if exp.num_spheres > 0:
175
+ exp.t_sphere = iadpython.Sphere(x[12], x[13], x[14], x[15], 0, x[16])
176
+
177
+ # Read data
178
+ if column_letters_str == "":
179
+ # old style data format
180
+ exp.num_measures = x[17]
181
+ data = x[18:]
182
+ columns = int(exp.num_measures)
183
+ if data[0] > 1:
184
+ columns += 1
185
+ data_in_columns = data.reshape(-1, columns)
186
+ fill_in_data_fixed(exp, data_in_columns)
187
+ else:
188
+ # new style variable header format
189
+ exp.num_measures = 0
190
+ if "r" in column_letters_str:
191
+ exp.num_measures += 1
192
+ if "t" in column_letters_str:
193
+ exp.num_measures += 1
194
+ if "u" in column_letters_str:
195
+ exp.num_measures += 1
196
+ data = x[17:]
197
+ columns = len(column_letters_str)
198
+ data_in_columns = data.reshape(-1, columns)
199
+ fill_in_data_variable(exp, data_in_columns, column_letters_str)
200
+
201
+ return exp