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/results.py
ADDED
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
"""Unitful scattering results, validated archives, and Matplotlib plots."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
import json
|
|
6
|
+
import numpy as np
|
|
7
|
+
from .source import Source
|
|
8
|
+
from ._archives import _RESULT_UNITS
|
|
9
|
+
from .angular_data import AngularData
|
|
10
|
+
from .units import Quantity, validate_units, ureg
|
|
11
|
+
|
|
12
|
+
_ANGULAR_FIELDS = (
|
|
13
|
+
"differential",
|
|
14
|
+
"directions",
|
|
15
|
+
"angles",
|
|
16
|
+
"azimuths",
|
|
17
|
+
"amplitudes",
|
|
18
|
+
"term_differential",
|
|
19
|
+
"stderr",
|
|
20
|
+
"azimuth_stderr",
|
|
21
|
+
"mu_s",
|
|
22
|
+
"sample_volume",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True, kw_only=True, repr=False, init=False)
|
|
27
|
+
class Result:
|
|
28
|
+
"""Store unitful scattering, diagnostics and reproducible settings.
|
|
29
|
+
|
|
30
|
+
Full solves retain ``differential`` and ``term_differential`` with shape
|
|
31
|
+
(order, polar angle, azimuth). Cuts and explicitly averaged results use
|
|
32
|
+
(order, observation). ``amplitudes`` always adds polarization and Cartesian
|
|
33
|
+
axes of sizes two and three to the differential shape. Ensembles retain
|
|
34
|
+
intensities and standard errors, without coherent amplitudes.
|
|
35
|
+
|
|
36
|
+
``angular`` groups read-only coordinates, intensities, amplitudes and phase
|
|
37
|
+
densities. ``azimuth_average()`` explicitly averages intensities and retains
|
|
38
|
+
the covariance-correct standard error of per-realization averaged curves.
|
|
39
|
+
Plots of full results select one sampled meridian by default.
|
|
40
|
+
|
|
41
|
+
Angles use radians, amplitudes metres, differential data m^-1 sr^-1,
|
|
42
|
+
integrated coefficients m^-1, and sample_volume m^3. Dimensional arrays require explicit units.
|
|
43
|
+
Every full phase density uses its order's solid-angle integral. Cuts have
|
|
44
|
+
no inferred integrated coefficients or phase normalization.
|
|
45
|
+
|
|
46
|
+
Numerical coefficients are finite-sample cross sections divided by the
|
|
47
|
+
entire voxel-box volume. ``differential_cross_section`` restores area per
|
|
48
|
+
steradian using stored sample_volume. Analytical coefficients describe an
|
|
49
|
+
infinite-medium model and have no finite-sample cross section.
|
|
50
|
+
|
|
51
|
+
Source and kind are required. Supply angular or individual differential
|
|
52
|
+
data, never both. Results are immutable; provenance returns a fresh copy. Other fields describe sampling,
|
|
53
|
+
isolated terms, integrated moments, uncertainty, diagnostics and provenance.
|
|
54
|
+
NaN errors represent one realization; NaN anisotropy represents zero
|
|
55
|
+
scattering. Arrays and JSON-compatible provenance are copied and validated.
|
|
56
|
+
The directional_differential and directional_amplitudes fields are legacy
|
|
57
|
+
aliases; use differential and amplitudes for both cuts and full solves.
|
|
58
|
+
|
|
59
|
+
Save writes schema 2 archives; load also promotes schema 1 directional data.
|
|
60
|
+
Numerical cumulative intensities retain coherent amplitude interference.
|
|
61
|
+
Isolated intensities must not be summed to reconstruct cumulative results.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
source: Source
|
|
65
|
+
kind: str
|
|
66
|
+
angular: AngularData
|
|
67
|
+
g: Quantity | None
|
|
68
|
+
mu_s_prime: Quantity | None
|
|
69
|
+
field_norms: Quantity | None
|
|
70
|
+
warnings: tuple
|
|
71
|
+
realizations: int | None
|
|
72
|
+
_provenance_json: str
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
*,
|
|
77
|
+
source,
|
|
78
|
+
kind,
|
|
79
|
+
angular=None,
|
|
80
|
+
differential=None,
|
|
81
|
+
sample_volume=None,
|
|
82
|
+
azimuth_averaged=False,
|
|
83
|
+
azimuth_stderr=None,
|
|
84
|
+
angles=None,
|
|
85
|
+
directions=None,
|
|
86
|
+
azimuths=None,
|
|
87
|
+
directional_differential=None,
|
|
88
|
+
mu_s=None,
|
|
89
|
+
g=None,
|
|
90
|
+
mu_s_prime=None,
|
|
91
|
+
amplitudes=None,
|
|
92
|
+
directional_amplitudes=None,
|
|
93
|
+
term_differential=None,
|
|
94
|
+
stderr=None,
|
|
95
|
+
field_norms=None,
|
|
96
|
+
warnings=(),
|
|
97
|
+
realizations=None,
|
|
98
|
+
provenance=None,
|
|
99
|
+
):
|
|
100
|
+
if not isinstance(source, Source):
|
|
101
|
+
raise TypeError("source must be a Source.")
|
|
102
|
+
|
|
103
|
+
if kind not in ("analytical", "volume", "ensemble"):
|
|
104
|
+
raise ValueError("kind must be analytical, volume, or ensemble.")
|
|
105
|
+
|
|
106
|
+
values = {name: value for name, value in locals().items() if name in _RESULT_UNITS}
|
|
107
|
+
|
|
108
|
+
if angular is not None:
|
|
109
|
+
if not isinstance(angular, AngularData):
|
|
110
|
+
raise TypeError("angular must be an AngularData.")
|
|
111
|
+
|
|
112
|
+
conflicts = any(
|
|
113
|
+
value is not None for name, value in values.items() if name not in ("g", "mu_s_prime", "field_norms")
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if conflicts or azimuth_averaged:
|
|
117
|
+
raise ValueError("Supply angular or individual angular fields, not both.")
|
|
118
|
+
|
|
119
|
+
if angular.kind != kind:
|
|
120
|
+
raise ValueError("angular.kind must match result kind.")
|
|
121
|
+
|
|
122
|
+
for name in _ANGULAR_FIELDS:
|
|
123
|
+
values[name] = getattr(angular, name)
|
|
124
|
+
|
|
125
|
+
azimuth_averaged = angular.azimuth_averaged
|
|
126
|
+
|
|
127
|
+
# Result stores explicit cut directions only; full vectors live in AngularData.
|
|
128
|
+
if angular.azimuths is not None or kind != "volume":
|
|
129
|
+
values["directions"] = None
|
|
130
|
+
|
|
131
|
+
for name, value in values.items():
|
|
132
|
+
if value is not None and (angular is None or name not in _ANGULAR_FIELDS):
|
|
133
|
+
unit = _RESULT_UNITS[name]
|
|
134
|
+
|
|
135
|
+
if unit == "dimensionless" and not isinstance(value, Quantity):
|
|
136
|
+
value = np.array(value, copy=True) * ureg.dimensionless
|
|
137
|
+
|
|
138
|
+
validate_units(
|
|
139
|
+
value,
|
|
140
|
+
unit=unit,
|
|
141
|
+
name=name,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if not np.issubdtype(np.asarray(value.magnitude).dtype, np.number):
|
|
145
|
+
raise ValueError(f"{name} must contain numeric values.")
|
|
146
|
+
|
|
147
|
+
values[name] = np.array(value.magnitude, copy=True) * value.units
|
|
148
|
+
|
|
149
|
+
provenance = {} if provenance is None else provenance
|
|
150
|
+
|
|
151
|
+
if not isinstance(provenance, dict):
|
|
152
|
+
raise ValueError("provenance must be a JSON-compatible dictionary.")
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
encoded = json.dumps(provenance, allow_nan=False)
|
|
156
|
+
|
|
157
|
+
provenance = json.loads(encoded)
|
|
158
|
+
except (TypeError, ValueError) as error:
|
|
159
|
+
raise ValueError("provenance must be a JSON-compatible dictionary with finite values.") from error
|
|
160
|
+
|
|
161
|
+
if not isinstance(warnings, (tuple, list)) or any(not isinstance(item, str) for item in warnings):
|
|
162
|
+
raise ValueError("warnings must be a sequence of strings.")
|
|
163
|
+
|
|
164
|
+
state = SimpleNamespace(**values, kind=kind, realizations=realizations, azimuth_averaged=azimuth_averaged)
|
|
165
|
+
|
|
166
|
+
from ._archives import _ResultArchive
|
|
167
|
+
|
|
168
|
+
_ResultArchive._promote_legacy_directional_data(result=state)
|
|
169
|
+
|
|
170
|
+
if state.sample_volume is None and kind != "analytical":
|
|
171
|
+
grid = provenance.get("grid")
|
|
172
|
+
|
|
173
|
+
if isinstance(grid, dict) and "shape" in grid and "spacing_m" in grid:
|
|
174
|
+
state.sample_volume = float(np.prod(grid["shape"]) * grid["spacing_m"] ** 3) * ureg.meter**3
|
|
175
|
+
|
|
176
|
+
from ._result_validation import _ResultValidator
|
|
177
|
+
|
|
178
|
+
if angular is None:
|
|
179
|
+
angular = AngularData(
|
|
180
|
+
**{name: getattr(state, name) for name in _ANGULAR_FIELDS}, kind=kind, azimuth_averaged=azimuth_averaged
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
_ResultValidator.validate(result=state)
|
|
184
|
+
|
|
185
|
+
for name, value in {
|
|
186
|
+
"source": source,
|
|
187
|
+
"kind": kind,
|
|
188
|
+
"angular": angular,
|
|
189
|
+
"warnings": tuple(warnings),
|
|
190
|
+
"realizations": state.realizations,
|
|
191
|
+
"_provenance_json": encoded,
|
|
192
|
+
}.items():
|
|
193
|
+
object.__setattr__(self, name, value)
|
|
194
|
+
|
|
195
|
+
for name in ("g", "mu_s_prime", "field_norms"):
|
|
196
|
+
value = getattr(state, name)
|
|
197
|
+
|
|
198
|
+
if value is not None:
|
|
199
|
+
value.magnitude.setflags(write=False)
|
|
200
|
+
|
|
201
|
+
object.__setattr__(self, name, value)
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def differential(self):
|
|
205
|
+
"""Read-only angular data owned by angular."""
|
|
206
|
+
|
|
207
|
+
return self.angular.differential
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def angles(self):
|
|
211
|
+
"""Read-only angular data owned by angular."""
|
|
212
|
+
|
|
213
|
+
return self.angular.angles
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def azimuths(self):
|
|
217
|
+
"""Read-only angular data owned by angular."""
|
|
218
|
+
|
|
219
|
+
return self.angular.azimuths
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def amplitudes(self):
|
|
223
|
+
"""Read-only angular data owned by angular."""
|
|
224
|
+
|
|
225
|
+
return self.angular.amplitudes
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def mu_s(self):
|
|
229
|
+
"""Read-only angular data owned by angular."""
|
|
230
|
+
|
|
231
|
+
return self.angular.mu_s
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def sample_volume(self):
|
|
235
|
+
"""Read-only angular data owned by angular."""
|
|
236
|
+
|
|
237
|
+
return self.angular.sample_volume
|
|
238
|
+
|
|
239
|
+
@property
|
|
240
|
+
def stderr(self):
|
|
241
|
+
"""Read-only angular data owned by angular."""
|
|
242
|
+
|
|
243
|
+
return self.angular.stderr
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
def azimuth_stderr(self):
|
|
247
|
+
"""Read-only angular data owned by angular."""
|
|
248
|
+
|
|
249
|
+
return self.angular.azimuth_stderr
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def term_differential(self):
|
|
253
|
+
"""Read-only angular data owned by angular."""
|
|
254
|
+
|
|
255
|
+
return self.angular.term_differential
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def azimuth_averaged(self):
|
|
259
|
+
"""Read-only angular data owned by angular."""
|
|
260
|
+
|
|
261
|
+
return self.angular.azimuth_averaged
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def directions(self):
|
|
265
|
+
"""Explicit cut coordinates; full-grid vectors are angular.directions."""
|
|
266
|
+
|
|
267
|
+
return self.angular.directions if self.angular.azimuths is None and self.kind == "volume" else None
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def directional_differential(self):
|
|
271
|
+
"""Legacy alias for full directional intensities."""
|
|
272
|
+
|
|
273
|
+
return self.differential if self.differential.ndim == 3 else None
|
|
274
|
+
|
|
275
|
+
@property
|
|
276
|
+
def directional_amplitudes(self):
|
|
277
|
+
"""Legacy alias for full coherent amplitudes."""
|
|
278
|
+
|
|
279
|
+
return self.amplitudes if self.differential.ndim == 3 else None
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def provenance(self):
|
|
283
|
+
"""Return a fresh metadata copy; edits cannot change this result."""
|
|
284
|
+
|
|
285
|
+
return json.loads(self._provenance_json)
|
|
286
|
+
|
|
287
|
+
def meridian(self, *, azimuth, method="exact"):
|
|
288
|
+
"""Select a physical azimuth from full data without interpolation."""
|
|
289
|
+
|
|
290
|
+
return self.angular.meridian(
|
|
291
|
+
azimuth=azimuth,
|
|
292
|
+
method=method,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
def azimuth_average(self):
|
|
296
|
+
"""Return explicit averaged intensities with covariance-correct errors."""
|
|
297
|
+
|
|
298
|
+
angular = self.angular.azimuth_average()
|
|
299
|
+
|
|
300
|
+
return Result(
|
|
301
|
+
source=self.source,
|
|
302
|
+
kind=self.kind,
|
|
303
|
+
angular=angular,
|
|
304
|
+
g=self.g,
|
|
305
|
+
mu_s_prime=self.mu_s_prime,
|
|
306
|
+
field_norms=self.field_norms,
|
|
307
|
+
warnings=self.warnings,
|
|
308
|
+
realizations=self.realizations,
|
|
309
|
+
provenance=self.provenance,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
@property
|
|
313
|
+
def differential_cross_section(self):
|
|
314
|
+
"""Finite-sample d-sigma/d-Omega in square metres per steradian."""
|
|
315
|
+
|
|
316
|
+
if self.sample_volume is None:
|
|
317
|
+
raise ValueError("sample_volume is unavailable; this result has no finite-sample cross section.")
|
|
318
|
+
|
|
319
|
+
return (getattr(self, "differential") * self.sample_volume).to("meter**2 / steradian")
|
|
320
|
+
|
|
321
|
+
def __repr__(self):
|
|
322
|
+
shape = self.differential.shape
|
|
323
|
+
|
|
324
|
+
available = [
|
|
325
|
+
name for name in ("amplitudes", "mu_s", "stderr", "sample_volume") if getattr(self, name) is not None
|
|
326
|
+
]
|
|
327
|
+
|
|
328
|
+
return f"Result(kind={self.kind!r}, orders={shape[0]}, angular_shape={shape[1:]}, available={available}, azimuth_averaged={self.azimuth_averaged})"
|
|
329
|
+
|
|
330
|
+
def save(self, *, path):
|
|
331
|
+
"""Save data, units, warnings, and provenance in a versioned NPZ archive.
|
|
332
|
+
|
|
333
|
+
Parameters
|
|
334
|
+
----------
|
|
335
|
+
path : str or pathlib.Path
|
|
336
|
+
Destination file, typically ending in ``.npz``. The exact path is
|
|
337
|
+
used without appending an extension. An existing file is replaced.
|
|
338
|
+
|
|
339
|
+
Returns
|
|
340
|
+
-------
|
|
341
|
+
path : pathlib.Path
|
|
342
|
+
Destination of the compressed NumPy archive.
|
|
343
|
+
|
|
344
|
+
Raises
|
|
345
|
+
------
|
|
346
|
+
ValueError
|
|
347
|
+
If result fields were modified into an invalid state.
|
|
348
|
+
OSError
|
|
349
|
+
If the destination cannot be written.
|
|
350
|
+
|
|
351
|
+
Notes
|
|
352
|
+
-----
|
|
353
|
+
Arrays are stored in SI units, including complex Born amplitudes.
|
|
354
|
+
Metadata is JSON, and loading never enables pickle. The archive stores
|
|
355
|
+
result data rather than the original voxel field; retain that field
|
|
356
|
+
separately for manual volumes. A field SHA-256 identifies the input.
|
|
357
|
+
Generated volumes also record their medium and seed. Exact seeded
|
|
358
|
+
reproduction depends on the recorded implementation versions.
|
|
359
|
+
"""
|
|
360
|
+
|
|
361
|
+
from ._archives import _ResultArchive
|
|
362
|
+
|
|
363
|
+
return _ResultArchive.save(
|
|
364
|
+
result=self,
|
|
365
|
+
path=path,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
@classmethod
|
|
369
|
+
def load(cls, *, path):
|
|
370
|
+
"""Load and validate a BornSim result archive without using pickle.
|
|
371
|
+
|
|
372
|
+
Parameters
|
|
373
|
+
----------
|
|
374
|
+
path : str or pathlib.Path
|
|
375
|
+
Archive created by ``Result.save``.
|
|
376
|
+
|
|
377
|
+
Returns
|
|
378
|
+
-------
|
|
379
|
+
result : Result
|
|
380
|
+
Restored quantities, complex amplitudes, diagnostics, and original
|
|
381
|
+
provenance. Loading does not change the recorded package version.
|
|
382
|
+
|
|
383
|
+
Raises
|
|
384
|
+
------
|
|
385
|
+
ValueError
|
|
386
|
+
If the schema is unsupported, metadata or arrays are missing or
|
|
387
|
+
inconsistent, or result validation fails.
|
|
388
|
+
OSError
|
|
389
|
+
If the archive cannot be read.
|
|
390
|
+
"""
|
|
391
|
+
|
|
392
|
+
from ._archives import _ResultArchive
|
|
393
|
+
|
|
394
|
+
return _ResultArchive.load(
|
|
395
|
+
result_type=cls,
|
|
396
|
+
path=path,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
@property
|
|
400
|
+
def phase_function(self):
|
|
401
|
+
"""Return directional phase density per steradian.
|
|
402
|
+
|
|
403
|
+
Full solves retain (order, polar angle, azimuth). Explicitly averaged
|
|
404
|
+
results retain (order, polar angle). Cuts have no normalization.
|
|
405
|
+
Every order uses its own full solid-angle scattering coefficient.
|
|
406
|
+
"""
|
|
407
|
+
|
|
408
|
+
return self.angular.phase_function
|
|
409
|
+
|
|
410
|
+
def plot(self, *, terms=False, log_y=False, title=None, azimuth=0):
|
|
411
|
+
"""Build a Matplotlib figure of differential scattering curves.
|
|
412
|
+
|
|
413
|
+
Parameters
|
|
414
|
+
----------
|
|
415
|
+
terms : bool, optional
|
|
416
|
+
Plot isolated numerical terms instead of cumulative curves. Default is
|
|
417
|
+
False. Isolated terms omit interference and ensemble error bars.
|
|
418
|
+
log_y : bool, optional
|
|
419
|
+
Use a logarithmic scattering axis. Default is False.
|
|
420
|
+
azimuth : int, optional
|
|
421
|
+
Index of the sampled meridian; default zero. Use azimuth_average()
|
|
422
|
+
explicitly to plot averaged intensities.
|
|
423
|
+
title : str, optional
|
|
424
|
+
Override the default title.
|
|
425
|
+
|
|
426
|
+
Returns
|
|
427
|
+
-------
|
|
428
|
+
figure : matplotlib.figure.Figure
|
|
429
|
+
Figure with angles in degrees and scattering in m^-1 sr^-1. Explicit
|
|
430
|
+
direction samples use input indices and markers. Ensemble cumulative
|
|
431
|
+
curves show one-standard-error bars where sampling error is known.
|
|
432
|
+
|
|
433
|
+
Raises
|
|
434
|
+
------
|
|
435
|
+
ValueError
|
|
436
|
+
If ``terms=True`` and isolated-term curves are unavailable.
|
|
437
|
+
|
|
438
|
+
Notes
|
|
439
|
+
-----
|
|
440
|
+
The figure is registered with pyplot and returned without displaying
|
|
441
|
+
it. Call ``matplotlib.pyplot.show()`` to display open figures or
|
|
442
|
+
``figure.savefig(path)`` to export a PNG, SVG, or PDF. Zero values cannot be displayed on a log axis.
|
|
443
|
+
A one-realization ensemble has unknown sampling error, so no error bars
|
|
444
|
+
are shown. This method plots differential scattering, not a normalized
|
|
445
|
+
phase function.
|
|
446
|
+
|
|
447
|
+
Examples
|
|
448
|
+
--------
|
|
449
|
+
>>> from bornsim import AnalyticalMedium, Solver, Source
|
|
450
|
+
...
|
|
451
|
+
>>> from bornsim.units import ureg
|
|
452
|
+
...
|
|
453
|
+
>>> solver = Solver(
|
|
454
|
+
... source=Source(
|
|
455
|
+
... wavelength=633e-9 * ureg.meter,
|
|
456
|
+
... )
|
|
457
|
+
... )
|
|
458
|
+
|
|
459
|
+
...
|
|
460
|
+
>>> result = solver.solve(
|
|
461
|
+
... target=AnalyticalMedium(
|
|
462
|
+
... background_refractive_index=1.33,
|
|
463
|
+
... refractive_index_std=0.01,
|
|
464
|
+
... correlation_length=100e-9 * ureg.meter,
|
|
465
|
+
... correlation="gaussian",
|
|
466
|
+
... )
|
|
467
|
+
... )
|
|
468
|
+
>>> figure = result.plot(log_y=True)
|
|
469
|
+
>>> len(figure.axes[0].lines)
|
|
470
|
+
1
|
|
471
|
+
"""
|
|
472
|
+
|
|
473
|
+
from ._result_plotting import _ResultPlotter
|
|
474
|
+
|
|
475
|
+
return _ResultPlotter(
|
|
476
|
+
result=self,
|
|
477
|
+
).plot(
|
|
478
|
+
terms=terms,
|
|
479
|
+
log_y=log_y,
|
|
480
|
+
azimuth=azimuth,
|
|
481
|
+
title=title,
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
def plot_cross_section(
|
|
485
|
+
self, *, volume=None, area_unit="nanometer**2", terms=False, log_y=False, title=None, azimuth=0
|
|
486
|
+
):
|
|
487
|
+
"""Build differential cross-section curves for a finite sample.
|
|
488
|
+
|
|
489
|
+
Parameters
|
|
490
|
+
----------
|
|
491
|
+
volume : Volume, optional
|
|
492
|
+
Optional legacy sample. Normally the recorded sample_volume
|
|
493
|
+
supplies the conversion without retaining the input voxel field.
|
|
494
|
+
area_unit : str, optional
|
|
495
|
+
Display area unit; default 'nanometer**2'.
|
|
496
|
+
terms : bool, optional
|
|
497
|
+
Show isolated terms, which omit interference and sampling error
|
|
498
|
+
bars, instead of cumulative coherent curves. Default False.
|
|
499
|
+
log_y : bool, optional
|
|
500
|
+
Use a logarithmic scattering axis. Default False.
|
|
501
|
+
azimuth : int, optional
|
|
502
|
+
Index of the sampled meridian; default zero. Use azimuth_average()
|
|
503
|
+
explicitly to plot averaged intensities.
|
|
504
|
+
title : str, optional
|
|
505
|
+
Override the default title.
|
|
506
|
+
|
|
507
|
+
Returns
|
|
508
|
+
-------
|
|
509
|
+
figure : matplotlib.figure.Figure
|
|
510
|
+
Angle or direction curves, with ensemble standard errors scaled
|
|
511
|
+
by the same sample volume. Returned without displaying it.
|
|
512
|
+
|
|
513
|
+
Raises
|
|
514
|
+
------
|
|
515
|
+
TypeError
|
|
516
|
+
If volume is not a Volume.
|
|
517
|
+
ValueError
|
|
518
|
+
If the result is analytical, recorded grid dimensions differ,
|
|
519
|
+
area_unit is invalid, or isolated curves are unavailable.
|
|
520
|
+
|
|
521
|
+
Notes
|
|
522
|
+
-----
|
|
523
|
+
Uses dσ/dΩ = V * differential. These are finite-sample cross sections,
|
|
524
|
+
not intrinsic infinite-medium transport coefficients. An optional supplied
|
|
525
|
+
volume must agree with the recorded physical volume and grid.
|
|
526
|
+
Stored scattering data and its SI units remain unchanged.
|
|
527
|
+
"""
|
|
528
|
+
|
|
529
|
+
from ._result_plotting import _ResultPlotter
|
|
530
|
+
|
|
531
|
+
return _ResultPlotter(
|
|
532
|
+
result=self,
|
|
533
|
+
).plot_cross_section(
|
|
534
|
+
volume=volume,
|
|
535
|
+
area_unit=area_unit,
|
|
536
|
+
terms=terms,
|
|
537
|
+
log_y=log_y,
|
|
538
|
+
azimuth=azimuth,
|
|
539
|
+
title=title,
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
@property
|
|
543
|
+
def directional_phase_function(self):
|
|
544
|
+
"""Compatibility alias for the primary full phase_function."""
|
|
545
|
+
|
|
546
|
+
if self.differential.ndim != 3:
|
|
547
|
+
raise ValueError(
|
|
548
|
+
"Directional phase data are unavailable; recompute with Solver.solve using AngularSampling."
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
return self.phase_function
|
|
552
|
+
|
|
553
|
+
def plot_phase_function(self, *, view="angular", order=None, log_y=False, azimuth=0, backend=None):
|
|
554
|
+
"""Plot normalized phase functions as angular curves, polar cuts, or a surface.
|
|
555
|
+
|
|
556
|
+
Parameters
|
|
557
|
+
----------
|
|
558
|
+
view : {'angular', 'polar', '3d'}, optional
|
|
559
|
+
Angular curves select a sampled meridian by default. Polar cuts
|
|
560
|
+
use its opposite azimuth for the other half of the plane. The 3D surface uses p(theta, phi), with
|
|
561
|
+
radius and color in sr^-1, retaining directional asymmetry.
|
|
562
|
+
Analytical distributions are independent of phi.
|
|
563
|
+
order : int, optional
|
|
564
|
+
One-based cumulative order. Angular and polar views show all
|
|
565
|
+
orders by default; the 3D view shows only the highest order.
|
|
566
|
+
log_y : bool, optional
|
|
567
|
+
Logarithmic vertical scale for angular curves. Default is False;
|
|
568
|
+
only supported with ``view='angular'``.
|
|
569
|
+
backend : {'plotly', 'matplotlib'}, optional
|
|
570
|
+
The 3D view defaults to Plotly. Angular and polar views use
|
|
571
|
+
Matplotlib. Select 'matplotlib' explicitly for a static 3D figure.
|
|
572
|
+
|
|
573
|
+
Returns
|
|
574
|
+
-------
|
|
575
|
+
figure : matplotlib.figure.Figure or plotly.graph_objects.Figure
|
|
576
|
+
Returned without displaying it. Call ``figure.show()`` for Plotly,
|
|
577
|
+
or ``matplotlib.pyplot.show()`` for Matplotlib. The 3D coordinates
|
|
578
|
+
are probability-density radii, not spatial positions.
|
|
579
|
+
|
|
580
|
+
Raises
|
|
581
|
+
------
|
|
582
|
+
ValueError
|
|
583
|
+
If phase normalization is unavailable, the view or order is
|
|
584
|
+
invalid, directional data are unavailable for a numerical 3D view,
|
|
585
|
+
log scaling is requested for another view, or polar/3D data do not
|
|
586
|
+
span the full [0, pi] range with at least three angles.
|
|
587
|
+
|
|
588
|
+
Notes
|
|
589
|
+
-----
|
|
590
|
+
Numerical 3D surfaces retain every sampled azimuth; no azimuth
|
|
591
|
+
averaging is applied. For one realization this displays the fixed
|
|
592
|
+
sample's directional distribution; multiple realizations display
|
|
593
|
+
the ensemble intensity average. Older archives lacking directional
|
|
594
|
+
data must be recomputed for a directional 3D view. Angular and polar
|
|
595
|
+
views select sampled meridians; averaging requires azimuth_average(). A flat phase function produces a spherical
|
|
596
|
+
surface; forward scattering extends towards +z.
|
|
597
|
+
Samples are sorted by angle for rendering. Error bars for normalized
|
|
598
|
+
ratios are omitted because the required covariance is not stored.
|
|
599
|
+
|
|
600
|
+
Examples
|
|
601
|
+
--------
|
|
602
|
+
>>> from bornsim import AnalyticalMedium, Solver, Source
|
|
603
|
+
...
|
|
604
|
+
>>> from bornsim.units import ureg
|
|
605
|
+
...
|
|
606
|
+
>>> solver = Solver(
|
|
607
|
+
... source=Source(
|
|
608
|
+
... wavelength=633e-9 * ureg.meter,
|
|
609
|
+
... )
|
|
610
|
+
... )
|
|
611
|
+
|
|
612
|
+
...
|
|
613
|
+
>>> result = solver.solve(
|
|
614
|
+
... target=AnalyticalMedium(
|
|
615
|
+
... background_refractive_index=1.33,
|
|
616
|
+
... refractive_index_std=0.01,
|
|
617
|
+
... correlation_length=100e-9 * ureg.meter,
|
|
618
|
+
... correlation="gaussian",
|
|
619
|
+
... )
|
|
620
|
+
... )
|
|
621
|
+
>>> figure = result.plot_phase_function(view="3d")
|
|
622
|
+
>>> figure.data[0].type
|
|
623
|
+
'surface'
|
|
624
|
+
"""
|
|
625
|
+
|
|
626
|
+
from ._result_plotting import _ResultPlotter
|
|
627
|
+
|
|
628
|
+
return _ResultPlotter(
|
|
629
|
+
result=self,
|
|
630
|
+
).plot_phase_function(
|
|
631
|
+
view=view,
|
|
632
|
+
order=order,
|
|
633
|
+
log_y=log_y,
|
|
634
|
+
azimuth=azimuth,
|
|
635
|
+
backend=backend,
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
def plot_field_norms(self, *, log_y=True):
|
|
639
|
+
"""Plot relative Born field-term norms for each available realization.
|
|
640
|
+
|
|
641
|
+
Parameters
|
|
642
|
+
----------
|
|
643
|
+
log_y : bool, optional
|
|
644
|
+
Use a logarithmic vertical axis. Default is True. Zero norms
|
|
645
|
+
cannot be displayed on a logarithmic axis.
|
|
646
|
+
|
|
647
|
+
Returns
|
|
648
|
+
-------
|
|
649
|
+
figure : matplotlib.figure.Figure
|
|
650
|
+
One trace per realization versus one-based field-term order.
|
|
651
|
+
Norms are dimensionless relative to the incident field.
|
|
652
|
+
|
|
653
|
+
Raises
|
|
654
|
+
------
|
|
655
|
+
ValueError
|
|
656
|
+
If no numerical field norms are available.
|
|
657
|
+
|
|
658
|
+
Notes
|
|
659
|
+
-----
|
|
660
|
+
The figure preserves per-realization diagnostics. Decreasing field
|
|
661
|
+
terms do not certify Born convergence or remove discretization error.
|
|
662
|
+
"""
|
|
663
|
+
|
|
664
|
+
from ._result_plotting import _ResultPlotter
|
|
665
|
+
|
|
666
|
+
return _ResultPlotter(
|
|
667
|
+
result=self,
|
|
668
|
+
).plot_field_norms(
|
|
669
|
+
log_y=log_y,
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
@dataclass(kw_only=True)
|
|
674
|
+
class BornResult:
|
|
675
|
+
"""Store numeric SI results for one finite-volume Born calculation.
|
|
676
|
+
|
|
677
|
+
Parameters
|
|
678
|
+
----------
|
|
679
|
+
directions : numpy.ndarray
|
|
680
|
+
Dimensionless observation vectors, shape (observation, 3).
|
|
681
|
+
amplitudes : numpy.ndarray
|
|
682
|
+
Complex isolated amplitudes in metres, shape
|
|
683
|
+
(order, observation, incident polarization, vector component).
|
|
684
|
+
differential : numpy.ndarray
|
|
685
|
+
Cumulative scattering in m^-1 sr^-1, shape (order, observation).
|
|
686
|
+
term_differential : numpy.ndarray
|
|
687
|
+
Isolated-term scattering in m^-1 sr^-1, same shape as differential.
|
|
688
|
+
field_norms : numpy.ndarray
|
|
689
|
+
Dimensionless relative field norms, shape (order,).
|
|
690
|
+
warnings : tuple of str
|
|
691
|
+
Numerical-resolution and Born-term diagnostics.
|
|
692
|
+
|
|
693
|
+
Attributes
|
|
694
|
+
----------
|
|
695
|
+
directions, amplitudes, differential, term_differential, field_norms : numpy.ndarray
|
|
696
|
+
Arrays described above, without attached units.
|
|
697
|
+
warnings : tuple of str
|
|
698
|
+
Numerical diagnostics.
|
|
699
|
+
|
|
700
|
+
See Also
|
|
701
|
+
--------
|
|
702
|
+
bornsim.series.BornSeries.solve : Produce this numeric result.
|
|
703
|
+
bornsim.results.Result : Unitful result with plotting support.
|
|
704
|
+
|
|
705
|
+
Notes
|
|
706
|
+
-----
|
|
707
|
+
Row zero denotes first order. Cumulative intensities include interference
|
|
708
|
+
between amplitudes; isolated intensities must not be summed to reconstruct
|
|
709
|
+
them. Integrated coefficients are not inferred from observation samples.
|
|
710
|
+
"""
|
|
711
|
+
|
|
712
|
+
directions: np.ndarray
|
|
713
|
+
amplitudes: np.ndarray # (order, direction, incident polarization, vector)
|
|
714
|
+
differential: np.ndarray # cumulative orders; m^-1 sr^-1
|
|
715
|
+
term_differential: np.ndarray # each term alone, excludes interference
|
|
716
|
+
field_norms: np.ndarray # ||E_j|| / ||E_inc||
|
|
717
|
+
warnings: tuple[str, ...]
|