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/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, ...]