BornSim 0.2.6__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.
- bornsim/__init__.py +69 -0
- bornsim/_archives.py +172 -0
- bornsim/_result_plotting.py +454 -0
- bornsim/_result_validation.py +89 -0
- bornsim/_validation.py +10 -0
- bornsim/_version.py +1 -0
- bornsim/_volume_plotting.py +395 -0
- bornsim/angular_data.py +338 -0
- bornsim/api.py +26 -0
- bornsim/directions.py +99 -0
- bornsim/ensemble.py +253 -0
- bornsim/ensemble_sampling.py +75 -0
- bornsim/geometry.py +716 -0
- bornsim/green.py +161 -0
- bornsim/grid.py +98 -0
- bornsim/material.py +46 -0
- bornsim/media.py +359 -0
- bornsim/model.py +276 -0
- bornsim/results.py +717 -0
- bornsim/rotation.py +68 -0
- bornsim/sampling.py +231 -0
- bornsim/series.py +300 -0
- bornsim/solver.py +561 -0
- bornsim/source.py +57 -0
- bornsim/units.py +81 -0
- bornsim/volume.py +319 -0
- bornsim-0.2.6.dist-info/METADATA +529 -0
- bornsim-0.2.6.dist-info/RECORD +31 -0
- bornsim-0.2.6.dist-info/WHEEL +5 -0
- bornsim-0.2.6.dist-info/licenses/LICENSE +21 -0
- bornsim-0.2.6.dist-info/top_level.txt +1 -0
bornsim/model.py
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""Vector first-order Born model, with SI lengths and unpolarized light.
|
|
2
|
+
|
|
3
|
+
The dielectric covariance uses the weak-fluctuation linearization
|
|
4
|
+
C_epsilon(r) = 4 n_background**2 C_n(r). The spectral convention is
|
|
5
|
+
Phi(q) = integral C(r) exp(-i q.r) d^3 r (no Fourier prefactor).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import TypedDict
|
|
10
|
+
import numpy as np
|
|
11
|
+
from .units import Quantity, validate_units, ureg
|
|
12
|
+
from .media import RandomMedium
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class OpticalProperties(TypedDict):
|
|
16
|
+
"""Analytical transport coefficients and dimensionless anisotropy."""
|
|
17
|
+
|
|
18
|
+
mu_s: Quantity
|
|
19
|
+
g: float | None
|
|
20
|
+
mu_s_prime: Quantity
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True, kw_only=True)
|
|
24
|
+
class AnalyticalMedium(RandomMedium):
|
|
25
|
+
"""Define random-medium statistics for the existing analytical first-order model.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
background_refractive_index : float
|
|
30
|
+
Positive, finite background refractive index. Plain numbers are required; quantities are rejected.
|
|
31
|
+
refractive_index_std : float
|
|
32
|
+
Nonnegative, finite standard deviation of refractive index fluctuations, not their
|
|
33
|
+
variance. Dimensionless;
|
|
34
|
+
correlation_length : Quantity
|
|
35
|
+
Positive, finite covariance length. Explicit length units are required;
|
|
36
|
+
the supplied units are preserved.
|
|
37
|
+
correlation : {'gaussian', 'exponential'}
|
|
38
|
+
Spatial covariance model.
|
|
39
|
+
|
|
40
|
+
Attributes
|
|
41
|
+
----------
|
|
42
|
+
background_refractive_index, refractive_index_std : float
|
|
43
|
+
Dimensionless statistics stored as numeric SI values.
|
|
44
|
+
correlation_length : Quantity
|
|
45
|
+
Covariance length with its supplied units.
|
|
46
|
+
correlation : str
|
|
47
|
+
Selected spatial covariance model.
|
|
48
|
+
|
|
49
|
+
Raises
|
|
50
|
+
------
|
|
51
|
+
ValueError
|
|
52
|
+
If a statistic is nonfinite, outside its allowed range, nonscalar,
|
|
53
|
+
has incompatible units, or the covariance model is unsupported.
|
|
54
|
+
|
|
55
|
+
Notes
|
|
56
|
+
-----
|
|
57
|
+
This concrete subclass replaces the former directly instantiated Medium.
|
|
58
|
+
It retains the existing first-order Gaussian and exponential formulas,
|
|
59
|
+
and inherits numerical voxel generation from RandomMedium.
|
|
60
|
+
The refractive index covariance is ``refractive_index_std**2 * exp(-r**2 / (2 * ell**2))``
|
|
61
|
+
for Gaussian correlation and ``refractive_index_std**2 * exp(-r / ell)`` for
|
|
62
|
+
exponential correlation, where ``ell = correlation_length``. Equal length
|
|
63
|
+
parameters therefore do not imply identical correlation profiles.
|
|
64
|
+
Dielectric contrast is linearized as ``delta_epsilon = 2 * n0 * delta_n``.
|
|
65
|
+
The Gaussian probability distribution used by :func:`random_volume` is
|
|
66
|
+
independent of this choice of spatial covariance.
|
|
67
|
+
|
|
68
|
+
Examples
|
|
69
|
+
--------
|
|
70
|
+
>>> from bornsim import AnalyticalMedium
|
|
71
|
+
>>> from bornsim.units import ureg
|
|
72
|
+
...
|
|
73
|
+
...
|
|
74
|
+
>>> medium = AnalyticalMedium(
|
|
75
|
+
... correlation_length=100 * ureg.nanometer,
|
|
76
|
+
... background_refractive_index=1.33,
|
|
77
|
+
... refractive_index_std=0.01,
|
|
78
|
+
... correlation="gaussian",
|
|
79
|
+
... )
|
|
80
|
+
>>> medium.correlation
|
|
81
|
+
'gaussian'
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
background_refractive_index: float
|
|
85
|
+
refractive_index_std: float
|
|
86
|
+
correlation_length: Quantity
|
|
87
|
+
correlation: str
|
|
88
|
+
|
|
89
|
+
def __post_init__(self):
|
|
90
|
+
super().__post_init__()
|
|
91
|
+
|
|
92
|
+
if self.correlation not in ("gaussian", "exponential"):
|
|
93
|
+
raise ValueError("AnalyticalMedium correlation must be gaussian or exponential.")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def angular_scattering(*, medium: AnalyticalMedium, wavelength: Quantity, theta: Quantity) -> Quantity:
|
|
97
|
+
"""Evaluate the unpolarized first-order differential scattering coefficient.
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
medium : AnalyticalMedium
|
|
102
|
+
Background refractive index and isotropic fluctuation statistics.
|
|
103
|
+
wavelength : Quantity
|
|
104
|
+
Positive, finite vacuum wavelength. Explicit length units are required.
|
|
105
|
+
theta : array_like or Quantity
|
|
106
|
+
Finite polar scattering angles in [0, pi]. Explicit angular units are required;
|
|
107
|
+
angular quantities may use degrees or radians. Scalars are accepted.
|
|
108
|
+
|
|
109
|
+
Returns
|
|
110
|
+
-------
|
|
111
|
+
differential : Quantity
|
|
112
|
+
Differential scattering coefficient in m^-1 sr^-1, with the same shape
|
|
113
|
+
as ``theta``. The result retains physical units.
|
|
114
|
+
|
|
115
|
+
Raises
|
|
116
|
+
------
|
|
117
|
+
ValueError
|
|
118
|
+
If the wavelength is nonpositive, nonfinite, nonscalar, or has
|
|
119
|
+
incompatible units, or angles are invalid or have incompatible units.
|
|
120
|
+
|
|
121
|
+
See Also
|
|
122
|
+
--------
|
|
123
|
+
optical_properties : Integrate the coefficient and its angular moments.
|
|
124
|
+
bornsim.solver.Solver.solve : Return analytical curves as unitful results.
|
|
125
|
+
|
|
126
|
+
Notes
|
|
127
|
+
-----
|
|
128
|
+
With ``k0 = 2*pi/wavelength`` and ``q = 2*n0*k0*sin(theta/2)``, the
|
|
129
|
+
coefficient is ``k0**4 * Phi_epsilon(q) * (1 + cos(theta)**2) / (32*pi**2)``.
|
|
130
|
+
The spectral convention is the three-dimensional Fourier transform without
|
|
131
|
+
an additional normalization prefactor. The dielectric covariance follows
|
|
132
|
+
``C_epsilon = 4 * n0**2 * C_n`` under weak-fluctuation linearization.
|
|
133
|
+
|
|
134
|
+
For nonzero total scattering, the phase function per steradian is this
|
|
135
|
+
coefficient divided by ``optical_properties(...)["mu_s"]``. Its integral
|
|
136
|
+
over solid angle is one; it is not a probability density per polar angle.
|
|
137
|
+
|
|
138
|
+
Examples
|
|
139
|
+
--------
|
|
140
|
+
>>> from bornsim import AnalyticalMedium, angular_scattering
|
|
141
|
+
>>> from bornsim.units import ureg
|
|
142
|
+
...
|
|
143
|
+
...
|
|
144
|
+
>>> curve = angular_scattering(
|
|
145
|
+
... medium=AnalyticalMedium(
|
|
146
|
+
... background_refractive_index=1.33,
|
|
147
|
+
... refractive_index_std=0.01,
|
|
148
|
+
... correlation_length=100e-9 * ureg.meter,
|
|
149
|
+
... correlation="gaussian",
|
|
150
|
+
... ),
|
|
151
|
+
... wavelength=633 * ureg.nanometer,
|
|
152
|
+
... theta=[0, 90, 180] * ureg.degree,
|
|
153
|
+
... )
|
|
154
|
+
>>> curve.shape
|
|
155
|
+
(3,)
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
if not isinstance(medium, AnalyticalMedium):
|
|
159
|
+
raise TypeError("Analytical scattering requires an AnalyticalMedium.")
|
|
160
|
+
|
|
161
|
+
validate_units(
|
|
162
|
+
wavelength,
|
|
163
|
+
unit="meter",
|
|
164
|
+
name="wavelength",
|
|
165
|
+
scalar=True,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
if not np.isfinite(wavelength) or wavelength <= 0:
|
|
169
|
+
raise ValueError("wavelength must be finite and positive.")
|
|
170
|
+
|
|
171
|
+
validate_units(
|
|
172
|
+
theta,
|
|
173
|
+
unit="radian",
|
|
174
|
+
name="theta",
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if np.any(~np.isfinite(theta)) or np.any((theta < 0) | (theta > np.pi * ureg.radian)):
|
|
178
|
+
raise ValueError("theta must be finite and between 0 and pi.")
|
|
179
|
+
|
|
180
|
+
k0 = 2 * np.pi / wavelength
|
|
181
|
+
|
|
182
|
+
q = 2 * k0 * medium.background_refractive_index * np.sin(theta / 2)
|
|
183
|
+
|
|
184
|
+
ell = medium.correlation_length
|
|
185
|
+
|
|
186
|
+
variance = (2 * medium.background_refractive_index * medium.refractive_index_std) ** 2
|
|
187
|
+
|
|
188
|
+
if medium.correlation == "gaussian":
|
|
189
|
+
spectrum = variance * (2 * np.pi) ** 1.5 * ell**3 * np.exp(-0.5 * (q * ell) ** 2)
|
|
190
|
+
else:
|
|
191
|
+
spectrum = variance * 8 * np.pi * ell**3 / (1 + (q * ell) ** 2) ** 2
|
|
192
|
+
|
|
193
|
+
return k0**4 / (16 * np.pi**2) * spectrum * (1 + np.cos(theta) ** 2) / 2 / ureg.steradian
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def optical_properties(
|
|
197
|
+
*, medium: AnalyticalMedium, wavelength: Quantity, quadrature_order: int = 256
|
|
198
|
+
) -> OpticalProperties:
|
|
199
|
+
"""Integrate analytical scattering and its moments over solid angle.
|
|
200
|
+
|
|
201
|
+
Parameters
|
|
202
|
+
----------
|
|
203
|
+
medium : AnalyticalMedium
|
|
204
|
+
Background refractive index and isotropic fluctuation statistics.
|
|
205
|
+
wavelength : Quantity
|
|
206
|
+
Positive, finite vacuum wavelength; explicit length units are required.
|
|
207
|
+
quadrature_order : int, optional
|
|
208
|
+
Number of Gauss-Legendre nodes in the cosine of the scattering angle.
|
|
209
|
+
Must be at least 16; default is 256.
|
|
210
|
+
|
|
211
|
+
Returns
|
|
212
|
+
-------
|
|
213
|
+
properties : dict
|
|
214
|
+
Unit-bearing coefficients under the following keys:
|
|
215
|
+
|
|
216
|
+
* ``mu_s`` : total scattering coefficient in m^-1.
|
|
217
|
+
* ``g`` : dimensionless mean cosine of the scattering angle, or
|
|
218
|
+
``None`` if ``mu_s`` is zero.
|
|
219
|
+
* ``mu_s_prime`` : reduced scattering coefficient in m^-1, equal to
|
|
220
|
+
``mu_s * (1 - g)`` when ``g`` is defined.
|
|
221
|
+
|
|
222
|
+
Raises
|
|
223
|
+
------
|
|
224
|
+
ValueError
|
|
225
|
+
If the quadrature order, wavelength, or wavelength units are invalid.
|
|
226
|
+
|
|
227
|
+
See Also
|
|
228
|
+
--------
|
|
229
|
+
angular_scattering : Evaluate the integrand at arbitrary polar angles.
|
|
230
|
+
|
|
231
|
+
Notes
|
|
232
|
+
-----
|
|
233
|
+
Axial symmetry gives ``mu_s = 2*pi*integral(beta(theta)*sin(theta), theta)``
|
|
234
|
+
over [0, pi], where ``beta`` is the differential coefficient. The
|
|
235
|
+
anisotropy uses the same integral with an additional ``cos(theta)`` factor.
|
|
236
|
+
These coefficients describe the analytical infinite-medium first-order
|
|
237
|
+
model, unlike the finite-sample coefficients from numerical ensembles.
|
|
238
|
+
Increase ``quadrature_order`` to check strongly forward-peaked cases.
|
|
239
|
+
|
|
240
|
+
Examples
|
|
241
|
+
--------
|
|
242
|
+
>>> from bornsim import AnalyticalMedium, optical_properties
|
|
243
|
+
...
|
|
244
|
+
>>> from bornsim.units import ureg
|
|
245
|
+
...
|
|
246
|
+
>>> properties = optical_properties(
|
|
247
|
+
... medium=AnalyticalMedium(
|
|
248
|
+
... refractive_index_std=0,
|
|
249
|
+
... background_refractive_index=1.33,
|
|
250
|
+
... correlation_length=100e-9 * ureg.meter,
|
|
251
|
+
... correlation="gaussian",
|
|
252
|
+
... ),
|
|
253
|
+
... wavelength=633 * ureg.nanometer,
|
|
254
|
+
... )
|
|
255
|
+
>>> float(properties["mu_s"].to("1 / meter").magnitude)
|
|
256
|
+
0.0
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
if not isinstance(quadrature_order, int) or quadrature_order < 16:
|
|
260
|
+
raise ValueError("quadrature_order must be an integer of at least 16.")
|
|
261
|
+
|
|
262
|
+
cosine, weights = np.polynomial.legendre.leggauss(quadrature_order)
|
|
263
|
+
|
|
264
|
+
differential = angular_scattering(
|
|
265
|
+
medium=medium,
|
|
266
|
+
wavelength=wavelength,
|
|
267
|
+
theta=np.arccos(cosine) * ureg.radian,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
mu_s = 2 * np.pi * ureg.steradian * np.sum(weights * differential)
|
|
271
|
+
|
|
272
|
+
mu_s_prime = 2 * np.pi * ureg.steradian * np.sum(weights * (1 - cosine) * differential)
|
|
273
|
+
|
|
274
|
+
g = float(2 * np.pi * ureg.steradian * np.sum(weights * cosine * differential) / mu_s) if mu_s else None
|
|
275
|
+
|
|
276
|
+
return {"mu_s": mu_s, "g": g, "mu_s_prime": mu_s_prime}
|