@photivra/engine 2.19.0 → 2.21.0
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.
- package/CHANGELOG.md +289 -1
- package/CITATION.cff +1 -1
- package/NODE_SUPPORT.md +7 -5
- package/README.md +7 -6
- package/SECURITY.md +1 -1
- package/THIRD_PARTY.md +21 -10
- package/dist/capture/dng-lens-correction-opcodes.d.ts +32 -0
- package/dist/capture/dng-lens-correction-opcodes.js +119 -0
- package/dist/capture/dng-lens-correction-opcodes.js.map +1 -0
- package/dist/capture/exposure-build-up-stage.d.ts +16 -0
- package/dist/capture/exposure-build-up-stage.js +4 -0
- package/dist/capture/exposure-build-up-stage.js.map +1 -0
- package/dist/capture/native-environment-lean.d.ts +2 -1
- package/dist/capture/native-environment-lean.js +42 -78
- package/dist/capture/native-environment-lean.js.map +1 -1
- package/dist/capture/native-environment-tabulated.js +4 -3
- package/dist/capture/native-environment-tabulated.js.map +1 -1
- package/dist/capture/native-raw-dng.d.ts +6 -0
- package/dist/capture/native-raw-dng.js +16 -2
- package/dist/capture/native-raw-dng.js.map +1 -1
- package/dist/capture/native-raw.d.ts +3 -2
- package/dist/capture/native-raw.js +5 -4
- package/dist/capture/native-raw.js.map +1 -1
- package/dist/capture/sensor-raw-producer.d.ts +4 -1
- package/dist/capture/sensor-raw-producer.js +15 -8
- package/dist/capture/sensor-raw-producer.js.map +1 -1
- package/dist/core/shared-helpers.d.ts +2 -0
- package/dist/core/shared-helpers.js +4 -0
- package/dist/core/shared-helpers.js.map +1 -1
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/core/version.js.map +1 -1
- package/dist/equipment/generic-lens-catalogue.d.ts +11 -4
- package/dist/equipment/generic-lens-catalogue.js +49 -7
- package/dist/equipment/generic-lens-catalogue.js.map +1 -1
- package/dist/equipment/generic-tier-assets.d.ts +32 -0
- package/dist/equipment/generic-tier-assets.js +18 -14
- package/dist/equipment/generic-tier-assets.js.map +1 -1
- package/dist/equipment/generic-tier-presets.js +2 -3
- package/dist/equipment/generic-tier-presets.js.map +1 -1
- package/dist/equipment/sensor-profile-body.d.ts +78 -0
- package/dist/equipment/sensor-profile-body.js +77 -0
- package/dist/equipment/sensor-profile-body.js.map +1 -0
- package/dist/equipment/sensor-profiles.d.ts +8 -0
- package/dist/equipment/sensor-profiles.js +82 -0
- package/dist/equipment/sensor-profiles.js.map +1 -0
- package/dist/facade/catalogue-lens-signal.d.ts +2 -2
- package/dist/facade/catalogue-lens-signal.js +58 -71
- package/dist/facade/catalogue-lens-signal.js.map +1 -1
- package/dist/facade/chart-radiance-batch.js +2 -1
- package/dist/facade/chart-radiance-batch.js.map +1 -1
- package/dist/facade/defocus-image-space-provider.d.ts +7 -0
- package/dist/facade/defocus-image-space-provider.js +64 -76
- package/dist/facade/defocus-image-space-provider.js.map +1 -1
- package/dist/facade/display-view-gpu-session.js +2 -0
- package/dist/facade/display-view-gpu-session.js.map +1 -1
- package/dist/facade/display-view-gpu.d.ts +13 -5
- package/dist/facade/display-view-gpu.js +4 -4
- package/dist/facade/display-view-gpu.js.map +1 -1
- package/dist/facade/display-view-lens-signal.d.ts +1 -1
- package/dist/facade/display-view-lens-signal.js +30 -26
- package/dist/facade/display-view-lens-signal.js.map +1 -1
- package/dist/facade/display-view.d.ts +3 -1
- package/dist/facade/display-view.js +6 -24
- package/dist/facade/display-view.js.map +1 -1
- package/dist/facade/expected-signal-setup.d.ts +3 -1
- package/dist/facade/expected-signal-setup.js +1 -1
- package/dist/facade/expected-signal-setup.js.map +1 -1
- package/dist/facade/expected-signal.d.ts +47 -14
- package/dist/facade/expected-signal.js +117 -112
- package/dist/facade/expected-signal.js.map +1 -1
- package/dist/facade/exposure-build-up.d.ts +70 -0
- package/dist/facade/exposure-build-up.js +164 -0
- package/dist/facade/exposure-build-up.js.map +1 -0
- package/dist/facade/fast-methods.d.ts +50 -0
- package/dist/facade/fast-methods.js +58 -0
- package/dist/facade/fast-methods.js.map +1 -0
- package/dist/facade/fast-resolution.d.ts +12 -0
- package/dist/facade/fast-resolution.js +18 -0
- package/dist/facade/fast-resolution.js.map +1 -0
- package/dist/facade/generic-lens-capture.d.ts +5 -4
- package/dist/facade/generic-lens-capture.js +5 -4
- package/dist/facade/generic-lens-capture.js.map +1 -1
- package/dist/facade/image-plane-enable.d.ts +7 -0
- package/dist/facade/image-plane-enable.js +17 -0
- package/dist/facade/image-plane-enable.js.map +1 -0
- package/dist/facade/image-plane-lattice-data.d.ts +6 -0
- package/dist/facade/image-plane-lattice-data.js +10 -0
- package/dist/facade/image-plane-lattice-data.js.map +1 -0
- package/dist/facade/image-plane-support.d.ts +33 -0
- package/dist/facade/image-plane-support.js +11 -0
- package/dist/facade/image-plane-support.js.map +1 -0
- package/dist/facade/image-plane.d.ts +36 -0
- package/dist/facade/image-plane.js +353 -0
- package/dist/facade/image-plane.js.map +1 -0
- package/dist/facade/mixed-illumination.d.ts +108 -0
- package/dist/facade/mixed-illumination.js +174 -0
- package/dist/facade/mixed-illumination.js.map +1 -0
- package/dist/facade/representative-light-sources.d.ts +72 -0
- package/dist/facade/representative-light-sources.js +124 -0
- package/dist/facade/representative-light-sources.js.map +1 -0
- package/dist/facade/scene-radiance.d.ts +129 -4
- package/dist/facade/scene-radiance.js +48 -13
- package/dist/facade/scene-radiance.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +88 -19
- package/dist/facade/simulate-capture.js +374 -145
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.d.ts +3 -2
- package/dist/facade/simulate-display-view.js +36 -18
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/dist/gpu/chart-composite-gpu.d.ts +13 -1
- package/dist/gpu/chart-composite-gpu.js +48 -9
- package/dist/gpu/chart-composite-gpu.js.map +1 -1
- package/dist/gpu/chart-radiance-gpu.js +4 -30
- package/dist/gpu/chart-radiance-gpu.js.map +1 -1
- package/dist/gpu/display-view-composite-material-module.d.ts +10 -4
- package/dist/gpu/display-view-composite-material-module.js +39 -13
- package/dist/gpu/display-view-composite-material-module.js.map +1 -1
- package/dist/gpu/display-view-image-material-module.d.ts +86 -0
- package/dist/gpu/display-view-image-material-module.js +339 -0
- package/dist/gpu/display-view-image-material-module.js.map +1 -0
- package/dist/gpu/display-view-image-quality.d.ts +45 -0
- package/dist/gpu/display-view-image-quality.js +380 -0
- package/dist/gpu/display-view-image-quality.js.map +1 -0
- package/dist/gpu/display-view-quality-gpu.d.ts +67 -1
- package/dist/gpu/display-view-quality-gpu.js +136 -205
- package/dist/gpu/display-view-quality-gpu.js.map +1 -1
- package/dist/gpu/display-view-speed-gpu.d.ts +51 -3
- package/dist/gpu/display-view-speed-gpu.js +131 -93
- package/dist/gpu/display-view-speed-gpu.js.map +1 -1
- package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +29 -15
- package/dist/gpu/wgsl-display-view-quality-kernel.js +58 -36
- package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
- package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +21 -6
- package/dist/gpu/wgsl-display-view-speed-kernel.js +46 -11
- package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
- package/dist/gpu/wgsl-exact-sum.d.ts +20 -0
- package/dist/gpu/wgsl-exact-sum.js +42 -0
- package/dist/gpu/wgsl-exact-sum.js.map +1 -0
- package/dist/index.d.ts +18 -3
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/dist/motion/subject-motion.d.ts +66 -0
- package/dist/motion/subject-motion.js +52 -0
- package/dist/motion/subject-motion.js.map +1 -0
- package/dist/optics/aperture-diameter.d.ts +47 -0
- package/dist/optics/aperture-diameter.js +50 -0
- package/dist/optics/aperture-diameter.js.map +1 -0
- package/dist/optics/detail-limit.d.ts +182 -0
- package/dist/optics/detail-limit.js +336 -0
- package/dist/optics/detail-limit.js.map +1 -0
- package/dist/optics/generic-lens-lateral-color.d.ts +83 -0
- package/dist/optics/generic-lens-lateral-color.js +77 -0
- package/dist/optics/generic-lens-lateral-color.js.map +1 -0
- package/dist/optics/generic-lens-psf.d.ts +18 -2
- package/dist/optics/generic-lens-psf.js +79 -48
- package/dist/optics/generic-lens-psf.js.map +1 -1
- package/dist/optics/lens-psf-profile.d.ts +7 -0
- package/dist/optics/lens-psf-profile.js +2 -1
- package/dist/optics/lens-psf-profile.js.map +1 -1
- package/dist/optics/scene-to-sensor-irradiance.d.ts +13 -0
- package/dist/optics/scene-to-sensor-irradiance.js +7 -0
- package/dist/optics/scene-to-sensor-irradiance.js.map +1 -1
- package/dist/output/crop-to-match-focal-length.d.ts +62 -0
- package/dist/output/crop-to-match-focal-length.js +63 -0
- package/dist/output/crop-to-match-focal-length.js.map +1 -0
- package/dist/output/developed-lens-corrections.d.ts +19 -5
- package/dist/output/developed-lens-corrections.js +30 -11
- package/dist/output/developed-lens-corrections.js.map +1 -1
- package/dist/output/generic-lens-corrections.d.ts +41 -3
- package/dist/output/generic-lens-corrections.js +57 -9
- package/dist/output/generic-lens-corrections.js.map +1 -1
- package/dist/output/lens-corrections.d.ts +12 -2
- package/dist/output/lens-corrections.js +22 -7
- package/dist/output/lens-corrections.js.map +1 -1
- package/dist/output/print-contrast-reference.js +1 -1
- package/dist/output/print-contrast-reference.js.map +1 -1
- package/dist/output/print-plan.d.ts +41 -5
- package/dist/output/print-plan.js +59 -14
- package/dist/output/print-plan.js.map +1 -1
- package/dist/output/print-size-blur-noise.d.ts +110 -0
- package/dist/output/print-size-blur-noise.js +138 -0
- package/dist/output/print-size-blur-noise.js.map +1 -0
- package/dist/sensor/binomial-sample.d.ts +3 -0
- package/dist/sensor/binomial-sample.js +86 -0
- package/dist/sensor/binomial-sample.js.map +1 -0
- package/dist/sensor/eqe-electron-rate.js +1 -7
- package/dist/sensor/eqe-electron-rate.js.map +1 -1
- package/dist/sensor/exposure-window.d.ts +2 -0
- package/dist/sensor/exposure-window.js +2 -2
- package/dist/sensor/exposure-window.js.map +1 -1
- package/dist/sensor/readout-timing.js +2 -23
- package/dist/sensor/readout-timing.js.map +1 -1
- package/dist/sensor/response-application-compatibility.js +1 -6
- package/dist/sensor/response-application-compatibility.js.map +1 -1
- package/dist/sensor/responsivity-photocurrent.js +1 -7
- package/dist/sensor/responsivity-photocurrent.js.map +1 -1
- package/dist/sensor/sensor-characteristics.d.ts +147 -0
- package/dist/sensor/sensor-characteristics.js +126 -0
- package/dist/sensor/sensor-characteristics.js.map +1 -0
- package/dist/sensor/spatial-sample-reduction.js +1 -5
- package/dist/sensor/spatial-sample-reduction.js.map +1 -1
- package/dist/sensor/spectral-quadrature.js +1 -6
- package/dist/sensor/spectral-quadrature.js.map +1 -1
- package/dist/sensor/temporal-photo-signal.d.ts +3 -0
- package/dist/sensor/temporal-photo-signal.js +5 -2
- package/dist/sensor/temporal-photo-signal.js.map +1 -1
- package/dist/stabilization/system.d.ts +9 -0
- package/dist/stabilization/system.js +5 -6
- package/dist/stabilization/system.js.map +1 -1
- package/docs/AGENT_GUIDE.md +5 -0
- package/docs/API_REFERENCE.md +173 -2
- package/docs/QUICK_START.md +213 -6
- package/docs/USAGE.md +157 -3
- package/docs/api/capture-native-raw-dng.md +6 -0
- package/docs/api/core-version.md +1 -1
- package/docs/api/equipment-generic-lens-catalogue.md +17 -7
- package/docs/api/equipment-sensor-profile-body.md +64 -0
- package/docs/api/equipment-sensor-profiles.md +86 -0
- package/docs/api/exports.json +357 -2
- package/docs/api/facade-display-view-gpu.md +5 -2
- package/docs/api/facade-exposure-build-up.md +105 -0
- package/docs/api/facade-fast-methods.md +99 -0
- package/docs/api/facade-image-plane-enable.md +16 -0
- package/docs/api/facade-mixed-illumination.md +67 -0
- package/docs/api/facade-representative-light-sources.md +131 -0
- package/docs/api/facade-scene-radiance.md +54 -3
- package/docs/api/facade-simulate-capture.md +60 -15
- package/docs/api/facade-simulate-display-view.md +3 -2
- package/docs/api/motion-subject-motion.md +23 -0
- package/docs/api/optics-aperture-diameter.md +51 -0
- package/docs/api/optics-detail-limit.md +203 -0
- package/docs/api/optics-generic-lens-lateral-color.md +79 -0
- package/docs/api/output-crop-to-match-focal-length.md +67 -0
- package/docs/api/output-developed-lens-corrections.md +10 -1
- package/docs/api/output-generic-lens-corrections.md +40 -2
- package/docs/api/output-lens-corrections.md +11 -1
- package/docs/api/output-print-plan.md +35 -1
- package/docs/api/output-print-size-blur-noise.md +115 -0
- package/docs/api/sensor-sensor-characteristics.md +105 -0
- package/llms.txt +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { type SensorReadoutConversionProfile } from "./raw-readout.js";
|
|
2
|
+
import type { SensorFixedPatternNoiseProfile } from "./fixed-pattern-noise.js";
|
|
3
|
+
/**
|
|
4
|
+
* #555: sensor characteristics a capture can set one at a time (`simulateCapture({ sensor })`). Each is off by default: an
|
|
5
|
+
* omitted field keeps the camera body's value, and a request with none of them is byte-identical to one before 2.20.0.
|
|
6
|
+
* Every value set is listed in the result's `assumptions`.
|
|
7
|
+
*/
|
|
8
|
+
export interface SimulateCaptureSensorCharacteristics {
|
|
9
|
+
/**
|
|
10
|
+
* Peak external quantum efficiency of the green channel (its value at 555 nm), 0.05–0.9. Default 0.5, the facade's
|
|
11
|
+
* idealized sensor. Every channel's EQE curve is multiplied by `peakQuantumEfficiency / 0.5`, so the curves keep their
|
|
12
|
+
* shape (and the colour matrix is unchanged) and the collected charge scales in proportion.
|
|
13
|
+
*/
|
|
14
|
+
peakQuantumEfficiency?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Full-well capacity: the charge, e⁻, that reaches the RAW white level at base ISO. Default: the body's base-ISO saturation
|
|
17
|
+
* at 6 µm, scaled by the pixel area (the `sensor.isoAreaNormalization` approximation). `link` `"pixel-area"` (default)
|
|
18
|
+
* scales `electrons`, the full well at `referencePixelPitchMicrometers` (default 6), by (pitch / reference)²; `"none"`
|
|
19
|
+
* uses `electrons` at any pitch. The result is 100 e⁻ to 1e10 e⁻.
|
|
20
|
+
*/
|
|
21
|
+
fullWell?: {
|
|
22
|
+
electrons: number;
|
|
23
|
+
link?: "pixel-area" | "none";
|
|
24
|
+
referencePixelPitchMicrometers?: number;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Read noise, RMS e⁻ (0–100), of the low and the high conversion gain, and the dual-gain switch: the ISO setting from
|
|
28
|
+
* which the high conversion gain is used (any ISO from 100; above the body's highest ISO, the low gain is used throughout).
|
|
29
|
+
* An omitted field keeps the body's value. Needs readout regimes that declare their exposure index (the tiers do).
|
|
30
|
+
*/
|
|
31
|
+
readNoise?: {
|
|
32
|
+
lowGainElectrons?: number;
|
|
33
|
+
highGainElectrons?: number;
|
|
34
|
+
highGainFromIso?: number;
|
|
35
|
+
};
|
|
36
|
+
/** Readout time, s (1e-6 to 1): the rolling shutter's top-to-bottom skew. Default: the body's (tiers 8, 15 and 30 ms). */
|
|
37
|
+
readoutSeconds?: number;
|
|
38
|
+
/**
|
|
39
|
+
* ADC bit depth: 12, 14 or 16. Default: the body's (consumer 12, the others 14). The black level scales with the code range
|
|
40
|
+
* (×2^Δbits), and the conversion gain follows so the same charge reaches the same fraction of the range.
|
|
41
|
+
*/
|
|
42
|
+
adcBitDepth?: 12 | 14 | 16;
|
|
43
|
+
/**
|
|
44
|
+
* Opt-in (advanced): the body's synthetic fixed-pattern noise profile (PRNU, DSNU, hot pixels, row and column offsets; EMVA
|
|
45
|
+
* 1288 Release 4.0 non-uniformities), the same pattern in every capture. Default false. Not in `simulateDisplayView`.
|
|
46
|
+
*/
|
|
47
|
+
fixedPatternNoise?: boolean;
|
|
48
|
+
}
|
|
49
|
+
/** The option keys of {@link SimulateCaptureSensorCharacteristics}, in `sensor`. */
|
|
50
|
+
export declare const SENSOR_CHARACTERISTIC_KEYS: readonly ["peakQuantumEfficiency", "fullWell", "readNoise", "readoutSeconds", "adcBitDepth", "fixedPatternNoise"];
|
|
51
|
+
/** One RMS noise component of the mid-grey figure, e⁻ (input-referred). */
|
|
52
|
+
export interface SimulateCaptureNoiseComponents {
|
|
53
|
+
/** Photon shot noise √S (Poisson). */
|
|
54
|
+
photonShot: number;
|
|
55
|
+
/** Read noise of the capture's readout regime. */
|
|
56
|
+
read: number;
|
|
57
|
+
/** Dark-current shot noise √(D·t), D = 1 e⁻/s at 20 °C. */
|
|
58
|
+
darkCurrent: number;
|
|
59
|
+
/** ADC quantization, one code (and one more after a digital gain) as a uniform 1/12 code² variance, in e⁻. */
|
|
60
|
+
quantization: number;
|
|
61
|
+
/** With `sensor.fixedPatternNoise`: √((PRNU·S)² + (DSNU·D·t)² + row² + column²), hot pixels excluded; a spatial, not temporal, noise. */
|
|
62
|
+
fixedPattern?: number;
|
|
63
|
+
/** √ of the sum of the squares. */
|
|
64
|
+
total: number;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* #555, #535: the capture's sensor characteristics as used, and the figures calculated from them (reported, not settable).
|
|
68
|
+
* Model figures from the engine's own noise model, not measurements.
|
|
69
|
+
*/
|
|
70
|
+
export interface SimulateCaptureSensorReport {
|
|
71
|
+
pixelPitchMicrometers: number;
|
|
72
|
+
/** The green channel's peak EQE (at 555 nm). */
|
|
73
|
+
peakQuantumEfficiency: number;
|
|
74
|
+
/** Full-well capacity, e⁻: the base-ISO saturation charge (`sensor.capacityElectrons`). */
|
|
75
|
+
fullWellElectrons: number;
|
|
76
|
+
/** Read noise, RMS e⁻, of each conversion gain, and the ISO from which the high gain is used (null: one gain throughout). */
|
|
77
|
+
readNoise: {
|
|
78
|
+
lowGainElectrons: number;
|
|
79
|
+
highGainElectrons: number;
|
|
80
|
+
highGainFromIso: number | null;
|
|
81
|
+
};
|
|
82
|
+
/** The body's rolling readout skew, s (null when it declares no rolling readout). */
|
|
83
|
+
readoutSeconds: number | null;
|
|
84
|
+
/** ADC bit depth of the capture's readout regime. */
|
|
85
|
+
adcBitDepth: number;
|
|
86
|
+
fixedPatternNoise: boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Engineering dynamic range at the capture's ISO, stops: log₂(S_sat / σ_floor), with S_sat the charge at the RAW white
|
|
89
|
+
* level (full well over the analog gain, limited by the charge capacity and the ADC range) and σ_floor the read noise and
|
|
90
|
+
* ADC quantization added in quadrature (the temporal dark noise of EMVA 1288 Release 4.0 without dark current). An ideal
|
|
91
|
+
* noiseless readout is limited by the ADC alone: log₂((white − black)·√12) for unit digital gain.
|
|
92
|
+
*/
|
|
93
|
+
dynamicRangeStops: number;
|
|
94
|
+
/** The same per ISO setting of the body, in its order. */
|
|
95
|
+
dynamicRangeByIso: readonly {
|
|
96
|
+
iso: number;
|
|
97
|
+
stops: number;
|
|
98
|
+
}[];
|
|
99
|
+
/**
|
|
100
|
+
* #535: the expected signal-to-noise ratio of an 18 % Lambertian grey patch under the request's illuminant and illuminance
|
|
101
|
+
* at the capture's exposure, ISO and sensor, on the green channel next to the optical axis (no vignetting, PSF or motion).
|
|
102
|
+
* `snr` = S/σ_total and `snrDecibels` = 20·log₁₀(snr). `clipped`: S is at or above the white level, where the RAW clips and
|
|
103
|
+
* the figure no longer describes the image. Computed only when the request resolves to exact (`fast: false`, the default for
|
|
104
|
+
* DNG, JPEG and PNG exports; omitted for a fast request).
|
|
105
|
+
*/
|
|
106
|
+
midGrey?: {
|
|
107
|
+
reflectance: 0.18;
|
|
108
|
+
channel: "green";
|
|
109
|
+
signalElectrons: number;
|
|
110
|
+
noiseElectrons: SimulateCaptureNoiseComponents;
|
|
111
|
+
snr: number;
|
|
112
|
+
snrDecibels: number;
|
|
113
|
+
clipped: boolean;
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** The request's characteristics, range-checked, with the EQE scale peakQuantumEfficiency / 0.5. */
|
|
117
|
+
export type ParsedSensorCharacteristics = SimulateCaptureSensorCharacteristics & {
|
|
118
|
+
qeScale: number;
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* A field's check: [minimum, maximum] (a number), the values allowed, 0 (a nested record) or a predicate. #554 shares the checker
|
|
122
|
+
* with the sensor profile parser.
|
|
123
|
+
*/
|
|
124
|
+
export type FieldRange = readonly [number, number] | readonly unknown[] | 0 | ((value: unknown) => boolean);
|
|
125
|
+
/**
|
|
126
|
+
* Checks `keys` of `record` against `ranges` (by path below `root`; nested records by "a.b"). An undefined field is skipped, or
|
|
127
|
+
* with `required` refused, and with `required` every field `ranges` lists at that level is checked.
|
|
128
|
+
*/
|
|
129
|
+
export declare function checkFieldRanges(record: Record<string, unknown>, keys: readonly string[], prefix: string, ranges?: Readonly<Record<string, FieldRange>>, root?: string, required?: boolean): void;
|
|
130
|
+
/** Checks the characteristics of the request's `sensor` record (its other keys already checked); undefined when it sets none. */
|
|
131
|
+
export declare function parseSensorCharacteristics(sensor: Record<string, unknown>): ParsedSensorCharacteristics | undefined;
|
|
132
|
+
/** The body's dual-gain read noise: the base regime's, and the first high-conversion-gain regime's with its ISO (none: the low gain throughout). */
|
|
133
|
+
export declare function declaredReadNoise(profile: SensorReadoutConversionProfile): SimulateCaptureSensorReport["readNoise"];
|
|
134
|
+
/**
|
|
135
|
+
* The readout profile with the characteristics applied: every regime's charge-domain transfer (system conversion gain,
|
|
136
|
+
* e⁻ per code, and pre-ADC saturation, e⁻ equivalent) multiplied by `chargeScale`, so a full well `chargeScale` times the
|
|
137
|
+
* body's reaches the same white code; with `adcBitDepth`, the black level ×2^Δbits (rounded), the saturation code 2^bits − 1
|
|
138
|
+
* and the conversion gain over the new code span; with `readNoise`, one electronic read-noise component per regime, the high
|
|
139
|
+
* conversion gain's from `highGainFromIso`. Codes per unit charge of the base regime otherwise, digital gain and the ISO
|
|
140
|
+
* selection are unchanged. Re-checked by the readout parser. With nothing to change, the profile itself.
|
|
141
|
+
*/
|
|
142
|
+
export declare function characterizeReadout(profile: SensorReadoutConversionProfile, chargeScale: number, c?: ParsedSensorCharacteristics): SensorReadoutConversionProfile;
|
|
143
|
+
/**
|
|
144
|
+
* The report: the characteristics used, the dynamic range per ISO, and the mid-grey noise figure for `greyElectrons`, the
|
|
145
|
+
* green channel's expected charge from an 18 % grey patch, after a `darkElectrons` dark charge.
|
|
146
|
+
*/
|
|
147
|
+
export declare function sensorReport(profile: SensorReadoutConversionProfile, regimeId: string, capacityElectrons: number, greyElectrons: number | undefined, darkElectrons: number, base: Pick<SimulateCaptureSensorReport, "pixelPitchMicrometers" | "peakQuantumEfficiency" | "readoutSeconds">, pattern?: SensorFixedPatternNoiseProfile): SimulateCaptureSensorReport;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Adjustable sensor characteristics for `simulateCapture` (#555) and the capture's noise figure (#535): the request
|
|
4
|
+
* options that replace a camera body's sensor values one at a time, the readout they give, and the calculated dynamic
|
|
5
|
+
* range and mid-grey signal-to-noise ratio the result reports.
|
|
6
|
+
*
|
|
7
|
+
* Everything here works on the body's declared readout regimes, so it applies to a generic tier and to a caller's
|
|
8
|
+
* preset record alike, and a later sensor profile (#554) can supply the same options as data.
|
|
9
|
+
* @see docs/QUICK_START.md, "Sensor characteristics (#555)", for the options, the equations and their sources.
|
|
10
|
+
*/
|
|
11
|
+
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
12
|
+
import { requireInputObject } from "../core/record-validation.js";
|
|
13
|
+
import { calculateSiteEffectiveSaturationCode } from "../capture/raw-white-level.js";
|
|
14
|
+
import { parseSensorReadoutConversionProfile } from "./raw-readout.js";
|
|
15
|
+
/** The option keys of {@link SimulateCaptureSensorCharacteristics}, in `sensor`. */
|
|
16
|
+
export const SENSOR_CHARACTERISTIC_KEYS = ["peakQuantumEfficiency", "fullWell", "readNoise", "readoutSeconds", "adcBitDepth", "fixedPatternNoise"];
|
|
17
|
+
/** Accepted ranges (design limits) and values of each characteristic, by path below `sensor`. */
|
|
18
|
+
const RANGES = { peakQuantumEfficiency: [0.05, 0.9], fullWell: 0, "fullWell.electrons": [100, 1e10],
|
|
19
|
+
"fullWell.link": ["pixel-area", "none"], "fullWell.referencePixelPitchMicrometers": [0.5, 1000], readNoise: 0, "readNoise.lowGainElectrons": [0, 100],
|
|
20
|
+
"readNoise.highGainElectrons": [0, 100], "readNoise.highGainFromIso": [100, 1e7], readoutSeconds: [1e-6, 1], adcBitDepth: [12, 14, 16], fixedPatternNoise: [true, false] };
|
|
21
|
+
function fail(message) { throw new InvalidConfigurationError(message); }
|
|
22
|
+
/**
|
|
23
|
+
* Checks `keys` of `record` against `ranges` (by path below `root`; nested records by "a.b"). An undefined field is skipped, or
|
|
24
|
+
* with `required` refused, and with `required` every field `ranges` lists at that level is checked.
|
|
25
|
+
*/
|
|
26
|
+
export function checkFieldRanges(record, keys, prefix, ranges = RANGES, root = "sensor.", required = false) {
|
|
27
|
+
const all = required ? [...new Set([...keys, ...Object.keys(ranges).filter((k) => k.startsWith(prefix) && !k.slice(prefix.length).includes(".")).map((k) => k.slice(prefix.length))])] : keys;
|
|
28
|
+
for (const key of all) {
|
|
29
|
+
const path = prefix + key, value = record[key], range = ranges[path];
|
|
30
|
+
if (value === undefined && !required)
|
|
31
|
+
continue;
|
|
32
|
+
if (range === undefined)
|
|
33
|
+
fail("Unknown " + (root + prefix).slice(0, -1) + " field: " + key + ".");
|
|
34
|
+
if (value === undefined)
|
|
35
|
+
fail(root + path + " is required.");
|
|
36
|
+
if (range === 0) {
|
|
37
|
+
requireInputObject(value, root + path);
|
|
38
|
+
checkFieldRanges(value, Object.keys(value), path + ".", ranges, root, required);
|
|
39
|
+
}
|
|
40
|
+
else if (typeof range === "function") {
|
|
41
|
+
if (!range(value))
|
|
42
|
+
fail(root + path + " is invalid.");
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
const choice = range.length > 2 || typeof range[0] !== "number", [minimum, maximum] = range;
|
|
46
|
+
if (choice ? !range.includes(value) : typeof value !== "number" || !(value >= minimum && value <= maximum)) {
|
|
47
|
+
fail(root + path + " must be " + (choice ? range.map(String).join(" or ") : "a number from " + minimum + " to " + maximum) + ".");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Checks the characteristics of the request's `sensor` record (its other keys already checked); undefined when it sets none. */
|
|
53
|
+
export function parseSensorCharacteristics(sensor) {
|
|
54
|
+
if (!SENSOR_CHARACTERISTIC_KEYS.some((key) => sensor[key] !== undefined))
|
|
55
|
+
return undefined;
|
|
56
|
+
checkFieldRanges(sensor, SENSOR_CHARACTERISTIC_KEYS, "");
|
|
57
|
+
const c = sensor, fullWell = c.fullWell;
|
|
58
|
+
if (fullWell && fullWell.electrons === undefined)
|
|
59
|
+
fail("sensor.fullWell.electrons is required.");
|
|
60
|
+
if (fullWell?.link === "none" && fullWell.referencePixelPitchMicrometers !== undefined)
|
|
61
|
+
fail("sensor.fullWell.referencePixelPitchMicrometers applies only to link \"pixel-area\".");
|
|
62
|
+
return { ...c, qeScale: (c.peakQuantumEfficiency ?? 0.5) / 0.5 };
|
|
63
|
+
}
|
|
64
|
+
const noiseOf = (regime) => Math.hypot(...regime.readNoiseComponents.map((c) => c.rmsElectrons.value));
|
|
65
|
+
/** The body's dual-gain read noise: the base regime's, and the first high-conversion-gain regime's with its ISO (none: the low gain throughout). */
|
|
66
|
+
export function declaredReadNoise(profile) {
|
|
67
|
+
const regimes = profile.regimes, high = regimes.find((r) => r.exposureIndexGain?.conversionGainMode === "high-conversion-gain"), low = noiseOf(regimes[0]);
|
|
68
|
+
return { lowGainElectrons: low, highGainElectrons: high ? noiseOf(high) : low, highGainFromIso: high ? high.exposureIndexGain.exposureIndex : null };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The readout profile with the characteristics applied: every regime's charge-domain transfer (system conversion gain,
|
|
72
|
+
* e⁻ per code, and pre-ADC saturation, e⁻ equivalent) multiplied by `chargeScale`, so a full well `chargeScale` times the
|
|
73
|
+
* body's reaches the same white code; with `adcBitDepth`, the black level ×2^Δbits (rounded), the saturation code 2^bits − 1
|
|
74
|
+
* and the conversion gain over the new code span; with `readNoise`, one electronic read-noise component per regime, the high
|
|
75
|
+
* conversion gain's from `highGainFromIso`. Codes per unit charge of the base regime otherwise, digital gain and the ISO
|
|
76
|
+
* selection are unchanged. Re-checked by the readout parser. With nothing to change, the profile itself.
|
|
77
|
+
*/
|
|
78
|
+
export function characterizeReadout(profile, chargeScale, c) {
|
|
79
|
+
const bits = c?.adcBitDepth, n = c?.readNoise, declared = n && declaredReadNoise(profile);
|
|
80
|
+
if (chargeScale === 1 && !bits && !n)
|
|
81
|
+
return profile;
|
|
82
|
+
return parseSensorReadoutConversionProfile({ ...profile, regimes: profile.regimes.map((regime) => {
|
|
83
|
+
const adc = regime.adc, rescale = bits !== undefined && bits !== adc.bitDepth;
|
|
84
|
+
const black = rescale ? Math.round(adc.blackLevelCode * 2 ** (bits - adc.bitDepth)) : adc.blackLevelCode, saturation = rescale ? 2 ** bits - 1 : adc.digitalSaturationCode;
|
|
85
|
+
const out = { ...regime, systemConversionGainElectronsPerCode: { ...regime.systemConversionGainElectronsPerCode, value: rescale ?
|
|
86
|
+
regime.systemConversionGainElectronsPerCode.value * chargeScale * (adc.digitalSaturationCode - adc.blackLevelCode) / (saturation - black) : regime.systemConversionGainElectronsPerCode.value * chargeScale },
|
|
87
|
+
preAdcSaturationElectronEquivalent: { ...regime.preAdcSaturationElectronEquivalent, value: regime.preAdcSaturationElectronEquivalent.value * chargeScale },
|
|
88
|
+
...(rescale ? { adc: { ...adc, bitDepth: bits, blackLevelCode: black, digitalSaturationCode: saturation } } : {}) };
|
|
89
|
+
if (n) {
|
|
90
|
+
const gain = regime.exposureIndexGain ?? fail("sensor.readNoise needs readout regimes that declare their exposure index.");
|
|
91
|
+
const from = n.highGainFromIso ?? declared.highGainFromIso ?? Infinity, high = gain.exposureIndex >= from;
|
|
92
|
+
const value = high ? n.highGainElectrons ?? declared.highGainElectrons : n.lowGainElectrons ?? declared.lowGainElectrons;
|
|
93
|
+
out.readNoiseComponents = [{ componentId: "electronic", rmsElectrons: { value, evidence: regime.evidence }, evidence: regime.evidence }];
|
|
94
|
+
out.exposureIndexGain = { ...gain, conversionGainMode: high ? "high-conversion-gain" : "low-conversion-gain" };
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}) });
|
|
98
|
+
}
|
|
99
|
+
/** Charge at the RAW white level and the read-plus-quantization floor of one regime, e⁻. */
|
|
100
|
+
function regimeRange(profile, regime, capacityElectrons) {
|
|
101
|
+
const adc = regime.adc, gain = regime.systemConversionGainElectronsPerCode.value, digitalGain = adc.digitalGain ?? 1;
|
|
102
|
+
const white = calculateSiteEffectiveSaturationCode({ physicalChargeCapacityElectrons: capacityElectrons, preAdcSaturationElectronEquivalent: regime.preAdcSaturationElectronEquivalent.value,
|
|
103
|
+
systemConversionGainElectronsPerCode: gain, blackLevelCode: adc.blackLevelCode, digitalSaturationCode: adc.digitalSaturationCode, adcMaximumCode: 2 ** adc.bitDepth - 1, digitalGain }, profile, regime.regimeId);
|
|
104
|
+
// Output codes per electron d/G; quantization 1/12 code² of the ADC times d², plus 1/12 of the rounding after a digital gain.
|
|
105
|
+
const perCode = gain / digitalGain;
|
|
106
|
+
return { saturation: (white - adc.blackLevelCode) * perCode, quantization: perCode * Math.sqrt(digitalGain * digitalGain / 12 + (digitalGain === 1 ? 0 : 1 / 12)), read: noiseOf(regime) };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The report: the characteristics used, the dynamic range per ISO, and the mid-grey noise figure for `greyElectrons`, the
|
|
110
|
+
* green channel's expected charge from an 18 % grey patch, after a `darkElectrons` dark charge.
|
|
111
|
+
*/
|
|
112
|
+
export function sensorReport(profile, regimeId, capacityElectrons, greyElectrons, darkElectrons, base, pattern) {
|
|
113
|
+
const stops = (r) => { const q = regimeRange(profile, r, capacityElectrons); return Math.log2(q.saturation / Math.hypot(q.read, q.quantization)); };
|
|
114
|
+
const regime = profile.regimes.find((r) => r.regimeId === regimeId), range = regimeRange(profile, regime, capacityElectrons);
|
|
115
|
+
const report = { ...base, fullWellElectrons: capacityElectrons, readNoise: declaredReadNoise(profile), adcBitDepth: regime.adc.bitDepth, fixedPatternNoise: !!pattern,
|
|
116
|
+
dynamicRangeStops: stops(regime), dynamicRangeByIso: profile.regimes.map((r) => ({ iso: r.exposureIndexGain?.exposureIndex ?? NaN, stops: stops(r) })).filter((e) => e.iso > 0) };
|
|
117
|
+
if (greyElectrons === undefined)
|
|
118
|
+
return report;
|
|
119
|
+
const fixedPattern = pattern && Math.hypot(pattern.photoResponseNonUniformity.relativeStandardDeviation.value * greyElectrons, pattern.darkSignalNonUniformity.relativeStandardDeviation.value * darkElectrons, pattern.rowOffset.rmsElectrons.value, pattern.columnOffset.rmsElectrons.value);
|
|
120
|
+
const noise = { photonShot: Math.sqrt(greyElectrons), read: range.read, darkCurrent: Math.sqrt(darkElectrons), quantization: range.quantization, ...(fixedPattern === undefined ? {} : { fixedPattern }), total: 0 };
|
|
121
|
+
noise.total = Math.hypot(noise.photonShot, noise.read, noise.darkCurrent, noise.quantization, fixedPattern ?? 0);
|
|
122
|
+
const snr = greyElectrons / noise.total;
|
|
123
|
+
report.midGrey = { reflectance: 0.18, channel: "green", signalElectrons: greyElectrons, noiseElectrons: noise, snr, snrDecibels: 20 * Math.log10(snr || Number.MIN_VALUE), clipped: greyElectrons + darkElectrons >= range.saturation };
|
|
124
|
+
return report;
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=sensor-characteristics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sensor-characteristics.js","sourceRoot":"","sources":["../../src/sensor/sensor-characteristics.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Adjustable sensor characteristics for `simulateCapture` (#555) and the capture's noise figure (#535): the request\n * options that replace a camera body's sensor values one at a time, the readout they give, and the calculated dynamic\n * range and mid-grey signal-to-noise ratio the result reports.\n *\n * Everything here works on the body's declared readout regimes, so it applies to a generic tier and to a caller's\n * preset record alike, and a later sensor profile (#554) can supply the same options as data.\n * @see docs/QUICK_START.md, \"Sensor characteristics (#555)\", for the options, the equations and their sources.\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { requireInputObject } from \"../core/record-validation.js\";\nimport { calculateSiteEffectiveSaturationCode } from \"../capture/raw-white-level.js\";\nimport { parseSensorReadoutConversionProfile, type SensorReadoutConversionProfile, type SensorReadoutConversionRegime } from \"./raw-readout.js\";\nimport type { SensorFixedPatternNoiseProfile } from \"./fixed-pattern-noise.js\";\n\n/**\n * #555: sensor characteristics a capture can set one at a time (`simulateCapture({ sensor })`). Each is off by default: an\n * omitted field keeps the camera body's value, and a request with none of them is byte-identical to one before 2.20.0.\n * Every value set is listed in the result's `assumptions`.\n */\nexport interface SimulateCaptureSensorCharacteristics {\n /**\n * Peak external quantum efficiency of the green channel (its value at 555 nm), 0.05–0.9. Default 0.5, the facade's\n * idealized sensor. Every channel's EQE curve is multiplied by `peakQuantumEfficiency / 0.5`, so the curves keep their\n * shape (and the colour matrix is unchanged) and the collected charge scales in proportion.\n */\n peakQuantumEfficiency?: number;\n /**\n * Full-well capacity: the charge, e⁻, that reaches the RAW white level at base ISO. Default: the body's base-ISO saturation\n * at 6 µm, scaled by the pixel area (the `sensor.isoAreaNormalization` approximation). `link` `\"pixel-area\"` (default)\n * scales `electrons`, the full well at `referencePixelPitchMicrometers` (default 6), by (pitch / reference)²; `\"none\"`\n * uses `electrons` at any pitch. The result is 100 e⁻ to 1e10 e⁻.\n */\n fullWell?: { electrons: number; link?: \"pixel-area\" | \"none\"; referencePixelPitchMicrometers?: number };\n /**\n * Read noise, RMS e⁻ (0–100), of the low and the high conversion gain, and the dual-gain switch: the ISO setting from\n * which the high conversion gain is used (any ISO from 100; above the body's highest ISO, the low gain is used throughout).\n * An omitted field keeps the body's value. Needs readout regimes that declare their exposure index (the tiers do).\n */\n readNoise?: { lowGainElectrons?: number; highGainElectrons?: number; highGainFromIso?: number };\n /** Readout time, s (1e-6 to 1): the rolling shutter's top-to-bottom skew. Default: the body's (tiers 8, 15 and 30 ms). */\n readoutSeconds?: number;\n /**\n * ADC bit depth: 12, 14 or 16. Default: the body's (consumer 12, the others 14). The black level scales with the code range\n * (×2^Δbits), and the conversion gain follows so the same charge reaches the same fraction of the range.\n */\n adcBitDepth?: 12 | 14 | 16;\n /**\n * Opt-in (advanced): the body's synthetic fixed-pattern noise profile (PRNU, DSNU, hot pixels, row and column offsets; EMVA\n * 1288 Release 4.0 non-uniformities), the same pattern in every capture. Default false. Not in `simulateDisplayView`.\n */\n fixedPatternNoise?: boolean;\n}\n\n/** The option keys of {@link SimulateCaptureSensorCharacteristics}, in `sensor`. */\nexport const SENSOR_CHARACTERISTIC_KEYS = [\"peakQuantumEfficiency\", \"fullWell\", \"readNoise\", \"readoutSeconds\", \"adcBitDepth\", \"fixedPatternNoise\"] as const;\n\n/** One RMS noise component of the mid-grey figure, e⁻ (input-referred). */\nexport interface SimulateCaptureNoiseComponents {\n /** Photon shot noise √S (Poisson). */\n photonShot: number;\n /** Read noise of the capture's readout regime. */\n read: number;\n /** Dark-current shot noise √(D·t), D = 1 e⁻/s at 20 °C. */\n darkCurrent: number;\n /** ADC quantization, one code (and one more after a digital gain) as a uniform 1/12 code² variance, in e⁻. */\n quantization: number;\n /** With `sensor.fixedPatternNoise`: √((PRNU·S)² + (DSNU·D·t)² + row² + column²), hot pixels excluded; a spatial, not temporal, noise. */\n fixedPattern?: number;\n /** √ of the sum of the squares. */\n total: number;\n}\n\n/**\n * #555, #535: the capture's sensor characteristics as used, and the figures calculated from them (reported, not settable).\n * Model figures from the engine's own noise model, not measurements.\n */\nexport interface SimulateCaptureSensorReport {\n pixelPitchMicrometers: number;\n /** The green channel's peak EQE (at 555 nm). */\n peakQuantumEfficiency: number;\n /** Full-well capacity, e⁻: the base-ISO saturation charge (`sensor.capacityElectrons`). */\n fullWellElectrons: number;\n /** Read noise, RMS e⁻, of each conversion gain, and the ISO from which the high gain is used (null: one gain throughout). */\n readNoise: { lowGainElectrons: number; highGainElectrons: number; highGainFromIso: number | null };\n /** The body's rolling readout skew, s (null when it declares no rolling readout). */\n readoutSeconds: number | null;\n /** ADC bit depth of the capture's readout regime. */\n adcBitDepth: number;\n fixedPatternNoise: boolean;\n /**\n * Engineering dynamic range at the capture's ISO, stops: log₂(S_sat / σ_floor), with S_sat the charge at the RAW white\n * level (full well over the analog gain, limited by the charge capacity and the ADC range) and σ_floor the read noise and\n * ADC quantization added in quadrature (the temporal dark noise of EMVA 1288 Release 4.0 without dark current). An ideal\n * noiseless readout is limited by the ADC alone: log₂((white − black)·√12) for unit digital gain.\n */\n dynamicRangeStops: number;\n /** The same per ISO setting of the body, in its order. */\n dynamicRangeByIso: readonly { iso: number; stops: number }[];\n /**\n * #535: the expected signal-to-noise ratio of an 18 % Lambertian grey patch under the request's illuminant and illuminance\n * at the capture's exposure, ISO and sensor, on the green channel next to the optical axis (no vignetting, PSF or motion).\n * `snr` = S/σ_total and `snrDecibels` = 20·log₁₀(snr). `clipped`: S is at or above the white level, where the RAW clips and\n * the figure no longer describes the image. Computed only when the request resolves to exact (`fast: false`, the default for\n * DNG, JPEG and PNG exports; omitted for a fast request).\n */\n midGrey?: { reflectance: 0.18; channel: \"green\"; signalElectrons: number; noiseElectrons: SimulateCaptureNoiseComponents; snr: number; snrDecibels: number; clipped: boolean };\n}\n\n/** The request's characteristics, range-checked, with the EQE scale peakQuantumEfficiency / 0.5. */\nexport type ParsedSensorCharacteristics = SimulateCaptureSensorCharacteristics & { qeScale: number };\n\n/**\n * A field's check: [minimum, maximum] (a number), the values allowed, 0 (a nested record) or a predicate. #554 shares the checker\n * with the sensor profile parser.\n */\nexport type FieldRange = readonly [number, number] | readonly unknown[] | 0 | ((value: unknown) => boolean);\n/** Accepted ranges (design limits) and values of each characteristic, by path below `sensor`. */\nconst RANGES: Readonly<Record<string, FieldRange>> = { peakQuantumEfficiency: [0.05, 0.9], fullWell: 0, \"fullWell.electrons\": [100, 1e10],\n \"fullWell.link\": [\"pixel-area\", \"none\"], \"fullWell.referencePixelPitchMicrometers\": [0.5, 1000], readNoise: 0, \"readNoise.lowGainElectrons\": [0, 100],\n \"readNoise.highGainElectrons\": [0, 100], \"readNoise.highGainFromIso\": [100, 1e7], readoutSeconds: [1e-6, 1], adcBitDepth: [12, 14, 16], fixedPatternNoise: [true, false] };\nfunction fail(message: string): never { throw new InvalidConfigurationError(message); }\n/**\n * Checks `keys` of `record` against `ranges` (by path below `root`; nested records by \"a.b\"). An undefined field is skipped, or\n * with `required` refused, and with `required` every field `ranges` lists at that level is checked.\n */\nexport function checkFieldRanges(record: Record<string, unknown>, keys: readonly string[], prefix: string, ranges: Readonly<Record<string, FieldRange>> = RANGES, root = \"sensor.\", required = false): void {\n const all = required ? [...new Set([...keys, ...Object.keys(ranges).filter((k) => k.startsWith(prefix) && !k.slice(prefix.length).includes(\".\")).map((k) => k.slice(prefix.length))])] : keys;\n for (const key of all) {\n const path = prefix + key, value = record[key], range = ranges[path];\n if (value === undefined && !required) continue;\n if (range === undefined) fail(\"Unknown \" + (root + prefix).slice(0, -1) + \" field: \" + key + \".\");\n if (value === undefined) fail(root + path + \" is required.\");\n if (range === 0) {\n requireInputObject(value, root + path);\n checkFieldRanges(value as Record<string, unknown>, Object.keys(value as object), path + \".\", ranges, root, required);\n } else if (typeof range === \"function\") {\n if (!range(value)) fail(root + path + \" is invalid.\");\n } else {\n const choice = range.length > 2 || typeof range[0] !== \"number\", [minimum, maximum] = range as readonly [number, number];\n if (choice ? !(range as readonly unknown[]).includes(value) : typeof value !== \"number\" || !(value >= minimum && value <= maximum)) {\n fail(root + path + \" must be \" + (choice ? range.map(String).join(\" or \") : \"a number from \" + minimum + \" to \" + maximum) + \".\");\n }\n }\n }\n}\n\n/** Checks the characteristics of the request's `sensor` record (its other keys already checked); undefined when it sets none. */\nexport function parseSensorCharacteristics(sensor: Record<string, unknown>): ParsedSensorCharacteristics | undefined {\n if (!SENSOR_CHARACTERISTIC_KEYS.some((key) => sensor[key] !== undefined)) return undefined;\n checkFieldRanges(sensor, SENSOR_CHARACTERISTIC_KEYS, \"\");\n const c = sensor as SimulateCaptureSensorCharacteristics, fullWell = c.fullWell;\n if (fullWell && fullWell.electrons === undefined) fail(\"sensor.fullWell.electrons is required.\");\n if (fullWell?.link === \"none\" && fullWell.referencePixelPitchMicrometers !== undefined) fail(\"sensor.fullWell.referencePixelPitchMicrometers applies only to link \\\"pixel-area\\\".\");\n return { ...c, qeScale: (c.peakQuantumEfficiency ?? 0.5) / 0.5 };\n}\n\nconst noiseOf = (regime: SensorReadoutConversionRegime): number => Math.hypot(...regime.readNoiseComponents.map((c) => c.rmsElectrons.value));\n/** The body's dual-gain read noise: the base regime's, and the first high-conversion-gain regime's with its ISO (none: the low gain throughout). */\nexport function declaredReadNoise(profile: SensorReadoutConversionProfile): SimulateCaptureSensorReport[\"readNoise\"] {\n const regimes = profile.regimes, high = regimes.find((r) => r.exposureIndexGain?.conversionGainMode === \"high-conversion-gain\"), low = noiseOf(regimes[0]!);\n return { lowGainElectrons: low, highGainElectrons: high ? noiseOf(high) : low, highGainFromIso: high ? high.exposureIndexGain!.exposureIndex : null };\n}\n\n/**\n * The readout profile with the characteristics applied: every regime's charge-domain transfer (system conversion gain,\n * e⁻ per code, and pre-ADC saturation, e⁻ equivalent) multiplied by `chargeScale`, so a full well `chargeScale` times the\n * body's reaches the same white code; with `adcBitDepth`, the black level ×2^Δbits (rounded), the saturation code 2^bits − 1\n * and the conversion gain over the new code span; with `readNoise`, one electronic read-noise component per regime, the high\n * conversion gain's from `highGainFromIso`. Codes per unit charge of the base regime otherwise, digital gain and the ISO\n * selection are unchanged. Re-checked by the readout parser. With nothing to change, the profile itself.\n */\nexport function characterizeReadout(profile: SensorReadoutConversionProfile, chargeScale: number, c?: ParsedSensorCharacteristics): SensorReadoutConversionProfile {\n const bits = c?.adcBitDepth, n = c?.readNoise, declared = n && declaredReadNoise(profile);\n if (chargeScale === 1 && !bits && !n) return profile;\n return parseSensorReadoutConversionProfile({ ...profile, regimes: profile.regimes.map((regime) => {\n const adc = regime.adc, rescale = bits !== undefined && bits !== adc.bitDepth;\n const black = rescale ? Math.round(adc.blackLevelCode * 2 ** (bits - adc.bitDepth)) : adc.blackLevelCode, saturation = rescale ? 2 ** bits - 1 : adc.digitalSaturationCode;\n const out = { ...regime, systemConversionGainElectronsPerCode: { ...regime.systemConversionGainElectronsPerCode, value: rescale ?\n regime.systemConversionGainElectronsPerCode.value * chargeScale * (adc.digitalSaturationCode - adc.blackLevelCode) / (saturation - black) : regime.systemConversionGainElectronsPerCode.value * chargeScale },\n preAdcSaturationElectronEquivalent: { ...regime.preAdcSaturationElectronEquivalent, value: regime.preAdcSaturationElectronEquivalent.value * chargeScale },\n ...(rescale ? { adc: { ...adc, bitDepth: bits, blackLevelCode: black, digitalSaturationCode: saturation } } : {}) };\n if (n) {\n const gain = regime.exposureIndexGain ?? fail(\"sensor.readNoise needs readout regimes that declare their exposure index.\");\n const from = n.highGainFromIso ?? declared!.highGainFromIso ?? Infinity, high = gain.exposureIndex >= from;\n const value = high ? n.highGainElectrons ?? declared!.highGainElectrons : n.lowGainElectrons ?? declared!.lowGainElectrons;\n out.readNoiseComponents = [{ componentId: \"electronic\", rmsElectrons: { value, evidence: regime.evidence }, evidence: regime.evidence }];\n out.exposureIndexGain = { ...gain, conversionGainMode: high ? \"high-conversion-gain\" : \"low-conversion-gain\" };\n }\n return out;\n }) });\n}\n\n/** Charge at the RAW white level and the read-plus-quantization floor of one regime, e⁻. */\nfunction regimeRange(profile: SensorReadoutConversionProfile, regime: SensorReadoutConversionRegime, capacityElectrons: number): { saturation: number; quantization: number; read: number } {\n const adc = regime.adc, gain = regime.systemConversionGainElectronsPerCode.value, digitalGain = adc.digitalGain ?? 1;\n const white = calculateSiteEffectiveSaturationCode({ physicalChargeCapacityElectrons: capacityElectrons, preAdcSaturationElectronEquivalent: regime.preAdcSaturationElectronEquivalent.value,\n systemConversionGainElectronsPerCode: gain, blackLevelCode: adc.blackLevelCode, digitalSaturationCode: adc.digitalSaturationCode, adcMaximumCode: 2 ** adc.bitDepth - 1, digitalGain }, profile, regime.regimeId);\n // Output codes per electron d/G; quantization 1/12 code² of the ADC times d², plus 1/12 of the rounding after a digital gain.\n const perCode = gain / digitalGain;\n return { saturation: (white - adc.blackLevelCode) * perCode, quantization: perCode * Math.sqrt(digitalGain * digitalGain / 12 + (digitalGain === 1 ? 0 : 1 / 12)), read: noiseOf(regime) };\n}\n\n/**\n * The report: the characteristics used, the dynamic range per ISO, and the mid-grey noise figure for `greyElectrons`, the\n * green channel's expected charge from an 18 % grey patch, after a `darkElectrons` dark charge.\n */\nexport function sensorReport(profile: SensorReadoutConversionProfile, regimeId: string, capacityElectrons: number, greyElectrons: number | undefined, darkElectrons: number,\n base: Pick<SimulateCaptureSensorReport, \"pixelPitchMicrometers\" | \"peakQuantumEfficiency\" | \"readoutSeconds\">, pattern?: SensorFixedPatternNoiseProfile): SimulateCaptureSensorReport {\n const stops = (r: SensorReadoutConversionRegime): number => { const q = regimeRange(profile, r, capacityElectrons); return Math.log2(q.saturation / Math.hypot(q.read, q.quantization)); };\n const regime = profile.regimes.find((r) => r.regimeId === regimeId)!, range = regimeRange(profile, regime, capacityElectrons);\n const report: SimulateCaptureSensorReport = { ...base, fullWellElectrons: capacityElectrons, readNoise: declaredReadNoise(profile), adcBitDepth: regime.adc.bitDepth, fixedPatternNoise: !!pattern,\n dynamicRangeStops: stops(regime), dynamicRangeByIso: profile.regimes.map((r) => ({ iso: r.exposureIndexGain?.exposureIndex ?? NaN, stops: stops(r) })).filter((e) => e.iso > 0) };\n if (greyElectrons === undefined) return report;\n const fixedPattern = pattern && Math.hypot(pattern.photoResponseNonUniformity.relativeStandardDeviation.value * greyElectrons,\n pattern.darkSignalNonUniformity.relativeStandardDeviation.value * darkElectrons, pattern.rowOffset.rmsElectrons.value, pattern.columnOffset.rmsElectrons.value);\n const noise = { photonShot: Math.sqrt(greyElectrons), read: range.read, darkCurrent: Math.sqrt(darkElectrons), quantization: range.quantization, ...(fixedPattern === undefined ? {} : { fixedPattern }), total: 0 };\n noise.total = Math.hypot(noise.photonShot, noise.read, noise.darkCurrent, noise.quantization, fixedPattern ?? 0);\n const snr = greyElectrons / noise.total;\n report.midGrey = { reflectance: 0.18, channel: \"green\", signalElectrons: greyElectrons, noiseElectrons: noise, snr, snrDecibels: 20 * Math.log10(snr || Number.MIN_VALUE), clipped: greyElectrons + darkElectrons >= range.saturation };\n return report;\n}\n"]}
|
|
@@ -15,6 +15,7 @@ import { InvalidScientificInputError } from "../core/validation.js";
|
|
|
15
15
|
import { requireInputKeys, requireInputNonNegativeSafeInteger as requireSafeNonNegativeInteger } from "../core/record-validation.js";
|
|
16
16
|
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
17
17
|
import { requirePositive as requirePositiveFinite } from "../exposure/flash-ttl.js";
|
|
18
|
+
import { approximatelyEqual } from "../core/shared-helpers.js";
|
|
18
19
|
const SQUARE_MICROMETERS_TO_SQUARE_METERS = 1e-12;
|
|
19
20
|
const WEIGHT_TOLERANCE = 1e-10;
|
|
20
21
|
const AREA_TOLERANCE = 1e-10;
|
|
@@ -95,11 +96,6 @@ function validateQuadratureNode(node, index) {
|
|
|
95
96
|
"].combinedAreaMeasureSquareMicrometers");
|
|
96
97
|
return identity;
|
|
97
98
|
}
|
|
98
|
-
function approximatelyEqual(actual, expected, tolerance) {
|
|
99
|
-
const scale = Math.max(1, Math.abs(actual), Math.abs(expected));
|
|
100
|
-
return (Math.abs(actual - expected) <=
|
|
101
|
-
tolerance * scale);
|
|
102
|
-
}
|
|
103
99
|
/**
|
|
104
100
|
* Rechecks node coordinates, weights, geometric measures and coverage before a spatial plan is trusted
|
|
105
101
|
* as scientific input.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spatial-sample-reduction.js","sourceRoot":"","sources":["../../src/sensor/spatial-sample-reduction.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Spatial sample reduction: combines values sampled at the spatial quadrature nodes of one sensor site\n * (its aperture plus the anti-aliasing (AA) split) into a site average and an area integral.\n *\n * Each supplied value is matched to its node by an explicit identity key. Average weights and\n * geometric area measures (in square micrometres) are kept separate. Negative or non-finite irradiance,\n * and missing or duplicate nodes, are rejected. The geometry does not establish QE (quantum\n * efficiency), an effective light-collection area or calibrated light transport.\n * @see docs/MOTION_AND_SIGNAL.md for equations, coordinate/unit conventions, blockers and support\n * limits.\n */\n\nimport {\n approximationResult,\n type CalculationResult\n} from \"../core/calculation-result.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport type {\n SensorSpatialSamplingQuadrature,\n SensorSpatialSamplingQuadratureNode\n} from \"./spatial-sampling-quadrature.js\";\nimport { requireInputKeys, requireInputNonNegativeSafeInteger as requireSafeNonNegativeInteger } from \"../core/record-validation.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { requirePositive as requirePositiveFinite } from \"../exposure/flash-ttl.js\";\n\nconst SQUARE_MICROMETERS_TO_SQUARE_METERS = 1e-12;\nconst WEIGHT_TOLERANCE = 1e-10;\nconst AREA_TOLERANCE = 1e-10;\n\nexport type SensorSpatialSampleValueDomain =\n | {\n kind: \"relative-linear\";\n unit: \"relative\";\n semantic:\n \"nonnegative-sensor-plane-irradiance-proxy\";\n }\n | {\n kind: \"radiometric-irradiance\";\n unit: \"W/m^2\";\n semantic: \"sensor-plane-irradiance\";\n };\n\nexport interface SensorSpatialQuadratureNodeIdentity {\n antiAliasingComponentIndex: number;\n apertureSampleXIndex: number;\n apertureSampleYIndex: number;\n}\n\nexport interface SensorSpatialQuadratureNodeValue {\n node: SensorSpatialQuadratureNodeIdentity;\n /**\n * A nonnegative, finite number in the declared value domain.\n *\n * It is evaluated at the matching node's preAntiAliasingSourcePointMm.\n */\n value: number;\n}\n\nexport interface ReduceSensorSpatialSamplingQuadratureInput {\n quadrature: SensorSpatialSamplingQuadrature;\n valueDomain: SensorSpatialSampleValueDomain;\n /**\n * Exactly one value per quadrature node, each with its node identity.\n *\n * Order does not matter; the node identity does.\n */\n nodeValues: readonly SensorSpatialQuadratureNodeValue[];\n}\n\nexport type SensorSpatialSampleReductionValue =\n | {\n kind: \"relative-linear\";\n unit: \"relative\";\n semantic:\n \"nonnegative-sensor-plane-irradiance-proxy\";\n /**\n * Weighted spatial average (dimensionless), in the same relative domain.\n */\n normalizedSpatialAverageRelative: number;\n /**\n * Relative value × square micrometres.\n *\n * A geometric, area-weighted diagnostic, not a radiometric power.\n */\n geometricAreaIntegralRelativeSquareMicrometers: number;\n }\n | {\n kind: \"radiometric-irradiance\";\n unit: \"W/m^2\";\n semantic: \"sensor-plane-irradiance\";\n /**\n * Weighted mean irradiance over the geometric aperture, after the\n * declared normalized AA spreading.\n */\n normalizedSpatialAverageIrradianceWattsPerSquareMeter: number;\n /**\n * Incident radiant flux: the supplied irradiance integrated over the\n * geometric aperture, with the declared normalized AA spreading.\n * Units: watts.\n *\n * It is before any CFA response, QE or microlens response, and excludes\n * any optical-stack throughput not already in the caller's irradiance values.\n */\n geometricApertureIncidentFluxWatts: number;\n };\n\nexport interface SensorSpatialSampleReduction {\n site: {\n x: number;\n y: number;\n };\n /**\n * Label of the destination CFA (colour filter array) site only.\n *\n * This reducer applies no spectral filter response.\n */\n channelId: string;\n outputMeaning:\n \"channel-tagged-pre-response-spatial-sample\";\n valueDomain: SensorSpatialSampleValueDomain;\n nodeCount: number;\n sourceValuesMatchedBy:\n \"quadrature-node-identity\";\n sourceValuesSuppliedForAllNodes: true;\n outsideImagingAreaSourceValuesRequired: boolean;\n normalizedSpatialWeightSum: number;\n geometricApertureAreaSquareMicrometers: number;\n reducedValue: SensorSpatialSampleReductionValue;\n cfaSpectralFilteringApplied: false;\n channelIdIsSpectralResponse: false;\n temporalIntegrationApplied: false;\n exposureDurationApplied: false;\n quantumEfficiencyApplied: false;\n microlensResponseApplied: false;\n opticalThroughputAppliedByReducer: false;\n photonsCalculated: false;\n electronsCalculated: false;\n shotNoiseApplied: false;\n readNoiseApplied: false;\n adcQuantizationApplied: false;\n blackLevelApplied: false;\n saturationApplied: false;\n rawCodeValueProduced: false;\n demosaicOrReconstructionApplied: false;\n}\n\nfunction requireNonNegativeFinite(\n value: unknown,\n path: string\n): number {\n if (\n typeof value !== \"number\" ||\n !Number.isFinite(value) ||\n value < 0\n ) {\n throw new InvalidScientificInputError(\n path +\n \" must be a finite number greater than or equal to zero.\"\n );\n }\n return value;\n}\n\nfunction parseValueDomain(\n value: unknown\n): SensorSpatialSampleValueDomain {\n if (\n typeof value !== \"object\" ||\n value === null ||\n Array.isArray(value)\n ) {\n throw new InvalidConfigurationError(\n \"valueDomain must be an object.\"\n );\n }\n\n const record = value as Record<string, unknown>;\n\n if (record.kind === \"relative-linear\") {\n if (\n record.unit !== \"relative\" ||\n record.semantic !==\n \"nonnegative-sensor-plane-irradiance-proxy\"\n ) {\n throw new InvalidScientificInputError(\n \"relative-linear valueDomain must use unit \\\"relative\\\" and the canonical nonnegative sensor-plane irradiance-proxy semantic.\"\n );\n }\n return {\n kind: \"relative-linear\",\n unit: \"relative\",\n semantic:\n \"nonnegative-sensor-plane-irradiance-proxy\"\n };\n }\n\n if (\n record.kind === \"radiometric-irradiance\"\n ) {\n if (\n record.unit !== \"W/m^2\" ||\n record.semantic !==\n \"sensor-plane-irradiance\"\n ) {\n throw new InvalidScientificInputError(\n \"radiometric-irradiance valueDomain must use unit \\\"W/m^2\\\" and semantic \\\"sensor-plane-irradiance\\\".\"\n );\n }\n return {\n kind: \"radiometric-irradiance\",\n unit: \"W/m^2\",\n semantic: \"sensor-plane-irradiance\"\n };\n }\n\n throw new InvalidConfigurationError(\n \"valueDomain.kind is invalid.\"\n );\n}\n\n/**\n * Makes the deterministic key used to match a value to its spatial quadrature node. It identifies the\n * sample; it is not a cryptographic checksum.\n * @param identity - The input; see `SensorSpatialQuadratureNodeIdentity` for its fields, units and coordinates.\n * @returns The result. Success does not imply calibration.\n *\n * @see docs/API_REFERENCE.md for the package export and its full type definitions.\n */\nexport function sensorSpatialQuadratureNodeIdentityKey(\n identity: SensorSpatialQuadratureNodeIdentity\n): string {\n return (\n identity.antiAliasingComponentIndex +\n \":\" +\n identity.apertureSampleXIndex +\n \":\" +\n identity.apertureSampleYIndex\n );\n}\n\nfunction validateQuadratureNode(\n node: SensorSpatialSamplingQuadratureNode,\n index: number\n): SensorSpatialQuadratureNodeIdentity {\n const identity = {\n antiAliasingComponentIndex:\n requireSafeNonNegativeInteger(\n node.antiAliasingComponentIndex,\n \"quadrature.nodes[\" +\n index +\n \"].antiAliasingComponentIndex\"\n ),\n apertureSampleXIndex:\n requireSafeNonNegativeInteger(\n node.apertureSampleXIndex,\n \"quadrature.nodes[\" +\n index +\n \"].apertureSampleXIndex\"\n ),\n apertureSampleYIndex:\n requireSafeNonNegativeInteger(\n node.apertureSampleYIndex,\n \"quadrature.nodes[\" +\n index +\n \"].apertureSampleYIndex\"\n )\n };\n\n requireNonNegativeFinite(\n node.combinedNormalizedSpatialWeight,\n \"quadrature.nodes[\" +\n index +\n \"].combinedNormalizedSpatialWeight\"\n );\n requireNonNegativeFinite(\n node.combinedAreaMeasureSquareMicrometers,\n \"quadrature.nodes[\" +\n index +\n \"].combinedAreaMeasureSquareMicrometers\"\n );\n\n return identity;\n}\n\nfunction approximatelyEqual(\n actual: number,\n expected: number,\n tolerance: number\n): boolean {\n const scale = Math.max(\n 1,\n Math.abs(actual),\n Math.abs(expected)\n );\n return (\n Math.abs(actual - expected) <=\n tolerance * scale\n );\n}\n\n/**\n * Rechecks node coordinates, weights, geometric measures and coverage before a spatial plan is trusted\n * as scientific input.\n *\n * Average weights and geometric area measures are kept separate. The geometry does not establish QE,\n * an effective light-collection area or calibrated light transport.\n * @param quadrature - The input; see `SensorSpatialSamplingQuadrature` for its fields, units and coordinates.\n * @returns The result. Success does not imply calibration.\n *\n * @see docs/API_REFERENCE.md for the package export and its full type definitions.\n */\nexport function validateSensorSpatialSamplingQuadrature(\n quadrature: SensorSpatialSamplingQuadrature\n): Map<string, SensorSpatialSamplingQuadratureNode> {\n if (\n typeof quadrature !== \"object\" ||\n quadrature === null ||\n !Array.isArray(quadrature.nodes)\n ) {\n throw new InvalidConfigurationError(\n \"quadrature must contain a nodes array.\"\n );\n }\n\n const totalNodeCount =\n requireSafeNonNegativeInteger(\n quadrature.totalNodeCount,\n \"quadrature.totalNodeCount\"\n );\n\n if (\n totalNodeCount === 0 ||\n quadrature.nodes.length !==\n totalNodeCount\n ) {\n throw new InvalidScientificInputError(\n \"quadrature.totalNodeCount must be positive and exactly match quadrature.nodes.length.\"\n );\n }\n\n if (\n quadrature.colorSamplingProfileId !== undefined &&\n (\n typeof quadrature.colorSamplingProfileId !== \"string\" ||\n quadrature.colorSamplingProfileId.trim().length === 0\n )\n ) {\n throw new InvalidConfigurationError(\n \"quadrature.colorSamplingProfileId must be a non-empty string when supplied.\"\n );\n }\n\n if (\n typeof quadrature.channelId !== \"string\" ||\n quadrature.channelId.trim().length === 0\n ) {\n throw new InvalidConfigurationError(\n \"quadrature.channelId must be a non-empty string.\"\n );\n }\n\n requireSafeNonNegativeInteger(\n quadrature.site.x,\n \"quadrature.site.x\"\n );\n requireSafeNonNegativeInteger(\n quadrature.site.y,\n \"quadrature.site.y\"\n );\n\n const geometricArea =\n requirePositiveFinite(\n quadrature.geometricApertureAreaSquareMicrometers,\n \"quadrature.geometricApertureAreaSquareMicrometers\"\n );\n\n const nodesByKey =\n new Map<\n string,\n SensorSpatialSamplingQuadratureNode\n >();\n let weightSum = 0;\n let areaSum = 0;\n\n quadrature.nodes.forEach(\n (node, index) => {\n const identity =\n validateQuadratureNode(node, index);\n const key = sensorSpatialQuadratureNodeIdentityKey(identity);\n\n if (nodesByKey.has(key)) {\n throw new InvalidScientificInputError(\n \"quadrature contains duplicate node identities.\"\n );\n }\n\n nodesByKey.set(key, node);\n weightSum +=\n node.combinedNormalizedSpatialWeight;\n areaSum +=\n node.combinedAreaMeasureSquareMicrometers;\n\n if (\n !Number.isFinite(weightSum) ||\n !Number.isFinite(areaSum)\n ) {\n throw new InvalidScientificInputError(\n \"quadrature aggregate weights/measures must remain finite.\"\n );\n }\n }\n );\n\n if (\n !approximatelyEqual(\n weightSum,\n 1,\n WEIGHT_TOLERANCE\n )\n ) {\n throw new InvalidScientificInputError(\n \"quadrature combined normalized spatial weights must sum to 1.\"\n );\n }\n\n if (\n !approximatelyEqual(\n areaSum,\n geometricArea,\n AREA_TOLERANCE\n )\n ) {\n throw new InvalidScientificInputError(\n \"quadrature combined geometric area measures must sum to the geometric aperture area.\"\n );\n }\n\n if (\n !approximatelyEqual(\n quadrature.normalizedSpatialWeightSum,\n weightSum,\n WEIGHT_TOLERANCE\n )\n ) {\n throw new InvalidScientificInputError(\n \"quadrature.normalizedSpatialWeightSum does not match node weights.\"\n );\n }\n\n if (\n !approximatelyEqual(\n quadrature.combinedAreaMeasureSumSquareMicrometers,\n areaSum,\n AREA_TOLERANCE\n )\n ) {\n throw new InvalidScientificInputError(\n \"quadrature.combinedAreaMeasureSumSquareMicrometers does not match node measures.\"\n );\n }\n\n return nodesByKey;\n}\n\nfunction parseNodeValues(\n nodeValues: readonly SensorSpatialQuadratureNodeValue[],\n nodesByKey: ReadonlyMap<\n string,\n SensorSpatialSamplingQuadratureNode\n >\n): Map<string, number> {\n if (!Array.isArray(nodeValues)) {\n throw new InvalidConfigurationError(\n \"nodeValues must be an array.\"\n );\n }\n\n if (\n nodeValues.length !==\n nodesByKey.size\n ) {\n throw new InvalidScientificInputError(\n \"nodeValues must contain exactly one value for every quadrature node.\"\n );\n }\n\n const valuesByKey = new Map<string, number>();\n\n nodeValues.forEach((entry, index) => {\n if (\n typeof entry !== \"object\" ||\n entry === null ||\n typeof entry.node !== \"object\" ||\n entry.node === null\n ) {\n throw new InvalidScientificInputError(\n \"nodeValues[\" +\n index +\n \"] must contain a node identity object.\"\n );\n }\n\n const identity = {\n antiAliasingComponentIndex:\n requireSafeNonNegativeInteger(\n entry.node\n .antiAliasingComponentIndex,\n \"nodeValues[\" +\n index +\n \"].node.antiAliasingComponentIndex\"\n ),\n apertureSampleXIndex:\n requireSafeNonNegativeInteger(\n entry.node.apertureSampleXIndex,\n \"nodeValues[\" +\n index +\n \"].node.apertureSampleXIndex\"\n ),\n apertureSampleYIndex:\n requireSafeNonNegativeInteger(\n entry.node.apertureSampleYIndex,\n \"nodeValues[\" +\n index +\n \"].node.apertureSampleYIndex\"\n )\n };\n const key = sensorSpatialQuadratureNodeIdentityKey(identity);\n\n if (!nodesByKey.has(key)) {\n throw new InvalidScientificInputError(\n \"nodeValues[\" +\n index +\n \"] identifies a node that is not present in quadrature.\"\n );\n }\n if (valuesByKey.has(key)) {\n throw new InvalidScientificInputError(\n \"nodeValues contains duplicate node identities.\"\n );\n }\n\n valuesByKey.set(\n key,\n requireNonNegativeFinite(\n entry.value,\n \"nodeValues[\" +\n index +\n \"].value\"\n )\n );\n });\n\n return valuesByKey;\n}\n\n/**\n * Combines nonnegative linear values, each tagged with its node, over a\n * validated sensor spatial quadrature.\n *\n * It does not sample a renderer or optical model itself. Callers evaluate one\n * value at every node's preAntiAliasingSourcePointMm and pass the values with\n * their exact node identity. Order does not matter.\n *\n * Two value domains are supported:\n *\n * - relative-linear: a dimensionless, nonnegative stand-in for irradiance, for\n * deterministic renderer and science regression tests;\n * - radiometric-irradiance: physical irradiance on the sensor plane in W/m^2.\n *\n * The destination site's channelId is a label only. No CFA spectral\n * transmittance or sensitivity is applied, so do not call the output a\n * colour-filtered mosaic sample or a RAW value.\n */\nexport function reduceSensorSpatialSamplingQuadrature(\n input: ReduceSensorSpatialSamplingQuadratureInput\n): CalculationResult<SensorSpatialSampleReduction> {\n requireInputKeys(input, \"reduceSensorSpatialSamplingQuadrature\", [\"quadrature\", \"valueDomain\", \"nodeValues\"]);\n const domain = parseValueDomain(\n input.valueDomain\n );\n const nodesByKey = validateSensorSpatialSamplingQuadrature(\n input.quadrature\n );\n const valuesByKey = parseNodeValues(\n input.nodeValues,\n nodesByKey\n );\n\n let weightedAverage = 0;\n let areaWeightedIntegral = 0;\n\n nodesByKey.forEach((node, key) => {\n const value = valuesByKey.get(key);\n if (value === undefined) {\n throw new InvalidScientificInputError(\n \"A quadrature node is missing its explicit source value.\"\n );\n }\n\n weightedAverage +=\n value *\n node.combinedNormalizedSpatialWeight;\n areaWeightedIntegral +=\n value *\n node.combinedAreaMeasureSquareMicrometers;\n\n if (\n !Number.isFinite(weightedAverage) ||\n !Number.isFinite(areaWeightedIntegral)\n ) {\n throw new InvalidScientificInputError(\n \"Reduced spatial sample must remain finite.\"\n );\n }\n });\n\n const reducedValue:\n SensorSpatialSampleReductionValue =\n domain.kind === \"relative-linear\"\n ? {\n kind: \"relative-linear\",\n unit: \"relative\",\n semantic:\n \"nonnegative-sensor-plane-irradiance-proxy\",\n normalizedSpatialAverageRelative:\n weightedAverage,\n geometricAreaIntegralRelativeSquareMicrometers:\n areaWeightedIntegral\n }\n : {\n kind: \"radiometric-irradiance\",\n unit: \"W/m^2\",\n semantic:\n \"sensor-plane-irradiance\",\n normalizedSpatialAverageIrradianceWattsPerSquareMeter:\n weightedAverage,\n geometricApertureIncidentFluxWatts:\n areaWeightedIntegral *\n SQUARE_MICROMETERS_TO_SQUARE_METERS\n };\n\n return approximationResult(\n {\n site: {\n ...input.quadrature.site\n },\n channelId:\n input.quadrature.channelId,\n outputMeaning:\n \"channel-tagged-pre-response-spatial-sample\",\n valueDomain: domain,\n nodeCount:\n input.quadrature.totalNodeCount,\n sourceValuesMatchedBy:\n \"quadrature-node-identity\",\n sourceValuesSuppliedForAllNodes: true,\n outsideImagingAreaSourceValuesRequired:\n input.quadrature\n .preAntiAliasingSourceOutsideImagingAreaNodeCount >\n 0,\n normalizedSpatialWeightSum:\n input.quadrature\n .normalizedSpatialWeightSum,\n geometricApertureAreaSquareMicrometers:\n input.quadrature\n .geometricApertureAreaSquareMicrometers,\n reducedValue,\n cfaSpectralFilteringApplied: false,\n channelIdIsSpectralResponse: false,\n temporalIntegrationApplied: false,\n exposureDurationApplied: false,\n quantumEfficiencyApplied: false,\n microlensResponseApplied: false,\n opticalThroughputAppliedByReducer:\n false,\n photonsCalculated: false,\n electronsCalculated: false,\n shotNoiseApplied: false,\n readNoiseApplied: false,\n adcQuantizationApplied: false,\n blackLevelApplied: false,\n saturationApplied: false,\n rawCodeValueProduced: false,\n demosaicOrReconstructionApplied: false\n },\n \"sensor-spatial-sample-reduction\",\n \"1.0.0\",\n [\n \"Input values are nonnegative linear scalars evaluated at each quadrature node's pre-AA source coordinate.\",\n \"Node values are matched by explicit quadrature-node identity rather than positional array order.\",\n \"The destination CFA/color channel is metadata only; no spectral transmittance, sensor spectral responsivity or quantum efficiency is applied.\",\n \"Relative-linear values are dimensionless irradiance-like proxies for deterministic testing and do not become physical radiometry.\",\n \"Radiometric-irradiance values use W/m^2; geometric aperture integration converts square micrometres to square metres to report incident radiant flux in watts.\",\n \"The physical irradiance branch reports geometric-aperture incident flux under the caller-supplied irradiance field and normalized AA spatial redistribution; it does not add omitted optical-stack throughput, microlens response, CFA response or sensor conversion.\",\n \"Every quadrature node requires an explicit source value, including pre-AA source points outside the active imaging area; no automatic clamping, zero-fill, extrapolation or renormalization occurs.\",\n \"Negative, NaN and infinite source values are rejected; signed intermediate color-transform values are outside this pre-response sensor-plane contract.\",\n \"Temporal exposure integration, wavelength integration, photons/electrons, noise, saturation, black level, ADC quantization, RAW code values and reconstruction remain separate future work.\"\n ]\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"spatial-sample-reduction.js","sourceRoot":"","sources":["../../src/sensor/spatial-sample-reduction.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Spatial sample reduction: combines values sampled at the spatial quadrature nodes of one sensor site\n * (its aperture plus the anti-aliasing (AA) split) into a site average and an area integral.\n *\n * Each supplied value is matched to its node by an explicit identity key. Average weights and\n * geometric area measures (in square micrometres) are kept separate. Negative or non-finite irradiance,\n * and missing or duplicate nodes, are rejected. The geometry does not establish QE (quantum\n * efficiency), an effective light-collection area or calibrated light transport.\n * @see docs/MOTION_AND_SIGNAL.md for equations, coordinate/unit conventions, blockers and support\n * limits.\n */\n\nimport {\n approximationResult,\n type CalculationResult\n} from \"../core/calculation-result.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport type {\n SensorSpatialSamplingQuadrature,\n SensorSpatialSamplingQuadratureNode\n} from \"./spatial-sampling-quadrature.js\";\nimport { requireInputKeys, requireInputNonNegativeSafeInteger as requireSafeNonNegativeInteger } from \"../core/record-validation.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { requirePositive as requirePositiveFinite } from \"../exposure/flash-ttl.js\";\nimport { approximatelyEqual } from \"../core/shared-helpers.js\";\n\nconst SQUARE_MICROMETERS_TO_SQUARE_METERS = 1e-12;\nconst WEIGHT_TOLERANCE = 1e-10;\nconst AREA_TOLERANCE = 1e-10;\n\nexport type SensorSpatialSampleValueDomain =\n | {\n kind: \"relative-linear\";\n unit: \"relative\";\n semantic:\n \"nonnegative-sensor-plane-irradiance-proxy\";\n }\n | {\n kind: \"radiometric-irradiance\";\n unit: \"W/m^2\";\n semantic: \"sensor-plane-irradiance\";\n };\n\nexport interface SensorSpatialQuadratureNodeIdentity {\n antiAliasingComponentIndex: number;\n apertureSampleXIndex: number;\n apertureSampleYIndex: number;\n}\n\nexport interface SensorSpatialQuadratureNodeValue {\n node: SensorSpatialQuadratureNodeIdentity;\n /**\n * A nonnegative, finite number in the declared value domain.\n *\n * It is evaluated at the matching node's preAntiAliasingSourcePointMm.\n */\n value: number;\n}\n\nexport interface ReduceSensorSpatialSamplingQuadratureInput {\n quadrature: SensorSpatialSamplingQuadrature;\n valueDomain: SensorSpatialSampleValueDomain;\n /**\n * Exactly one value per quadrature node, each with its node identity.\n *\n * Order does not matter; the node identity does.\n */\n nodeValues: readonly SensorSpatialQuadratureNodeValue[];\n}\n\nexport type SensorSpatialSampleReductionValue =\n | {\n kind: \"relative-linear\";\n unit: \"relative\";\n semantic:\n \"nonnegative-sensor-plane-irradiance-proxy\";\n /**\n * Weighted spatial average (dimensionless), in the same relative domain.\n */\n normalizedSpatialAverageRelative: number;\n /**\n * Relative value × square micrometres.\n *\n * A geometric, area-weighted diagnostic, not a radiometric power.\n */\n geometricAreaIntegralRelativeSquareMicrometers: number;\n }\n | {\n kind: \"radiometric-irradiance\";\n unit: \"W/m^2\";\n semantic: \"sensor-plane-irradiance\";\n /**\n * Weighted mean irradiance over the geometric aperture, after the\n * declared normalized AA spreading.\n */\n normalizedSpatialAverageIrradianceWattsPerSquareMeter: number;\n /**\n * Incident radiant flux: the supplied irradiance integrated over the\n * geometric aperture, with the declared normalized AA spreading.\n * Units: watts.\n *\n * It is before any CFA response, QE or microlens response, and excludes\n * any optical-stack throughput not already in the caller's irradiance values.\n */\n geometricApertureIncidentFluxWatts: number;\n };\n\nexport interface SensorSpatialSampleReduction {\n site: {\n x: number;\n y: number;\n };\n /**\n * Label of the destination CFA (colour filter array) site only.\n *\n * This reducer applies no spectral filter response.\n */\n channelId: string;\n outputMeaning:\n \"channel-tagged-pre-response-spatial-sample\";\n valueDomain: SensorSpatialSampleValueDomain;\n nodeCount: number;\n sourceValuesMatchedBy:\n \"quadrature-node-identity\";\n sourceValuesSuppliedForAllNodes: true;\n outsideImagingAreaSourceValuesRequired: boolean;\n normalizedSpatialWeightSum: number;\n geometricApertureAreaSquareMicrometers: number;\n reducedValue: SensorSpatialSampleReductionValue;\n cfaSpectralFilteringApplied: false;\n channelIdIsSpectralResponse: false;\n temporalIntegrationApplied: false;\n exposureDurationApplied: false;\n quantumEfficiencyApplied: false;\n microlensResponseApplied: false;\n opticalThroughputAppliedByReducer: false;\n photonsCalculated: false;\n electronsCalculated: false;\n shotNoiseApplied: false;\n readNoiseApplied: false;\n adcQuantizationApplied: false;\n blackLevelApplied: false;\n saturationApplied: false;\n rawCodeValueProduced: false;\n demosaicOrReconstructionApplied: false;\n}\n\nfunction requireNonNegativeFinite(\n value: unknown,\n path: string\n): number {\n if (\n typeof value !== \"number\" ||\n !Number.isFinite(value) ||\n value < 0\n ) {\n throw new InvalidScientificInputError(\n path +\n \" must be a finite number greater than or equal to zero.\"\n );\n }\n return value;\n}\n\nfunction parseValueDomain(\n value: unknown\n): SensorSpatialSampleValueDomain {\n if (\n typeof value !== \"object\" ||\n value === null ||\n Array.isArray(value)\n ) {\n throw new InvalidConfigurationError(\n \"valueDomain must be an object.\"\n );\n }\n\n const record = value as Record<string, unknown>;\n\n if (record.kind === \"relative-linear\") {\n if (\n record.unit !== \"relative\" ||\n record.semantic !==\n \"nonnegative-sensor-plane-irradiance-proxy\"\n ) {\n throw new InvalidScientificInputError(\n \"relative-linear valueDomain must use unit \\\"relative\\\" and the canonical nonnegative sensor-plane irradiance-proxy semantic.\"\n );\n }\n return {\n kind: \"relative-linear\",\n unit: \"relative\",\n semantic:\n \"nonnegative-sensor-plane-irradiance-proxy\"\n };\n }\n\n if (\n record.kind === \"radiometric-irradiance\"\n ) {\n if (\n record.unit !== \"W/m^2\" ||\n record.semantic !==\n \"sensor-plane-irradiance\"\n ) {\n throw new InvalidScientificInputError(\n \"radiometric-irradiance valueDomain must use unit \\\"W/m^2\\\" and semantic \\\"sensor-plane-irradiance\\\".\"\n );\n }\n return {\n kind: \"radiometric-irradiance\",\n unit: \"W/m^2\",\n semantic: \"sensor-plane-irradiance\"\n };\n }\n\n throw new InvalidConfigurationError(\n \"valueDomain.kind is invalid.\"\n );\n}\n\n/**\n * Makes the deterministic key used to match a value to its spatial quadrature node. It identifies the\n * sample; it is not a cryptographic checksum.\n * @param identity - The input; see `SensorSpatialQuadratureNodeIdentity` for its fields, units and coordinates.\n * @returns The result. Success does not imply calibration.\n *\n * @see docs/API_REFERENCE.md for the package export and its full type definitions.\n */\nexport function sensorSpatialQuadratureNodeIdentityKey(\n identity: SensorSpatialQuadratureNodeIdentity\n): string {\n return (\n identity.antiAliasingComponentIndex +\n \":\" +\n identity.apertureSampleXIndex +\n \":\" +\n identity.apertureSampleYIndex\n );\n}\n\nfunction validateQuadratureNode(\n node: SensorSpatialSamplingQuadratureNode,\n index: number\n): SensorSpatialQuadratureNodeIdentity {\n const identity = {\n antiAliasingComponentIndex:\n requireSafeNonNegativeInteger(\n node.antiAliasingComponentIndex,\n \"quadrature.nodes[\" +\n index +\n \"].antiAliasingComponentIndex\"\n ),\n apertureSampleXIndex:\n requireSafeNonNegativeInteger(\n node.apertureSampleXIndex,\n \"quadrature.nodes[\" +\n index +\n \"].apertureSampleXIndex\"\n ),\n apertureSampleYIndex:\n requireSafeNonNegativeInteger(\n node.apertureSampleYIndex,\n \"quadrature.nodes[\" +\n index +\n \"].apertureSampleYIndex\"\n )\n };\n\n requireNonNegativeFinite(\n node.combinedNormalizedSpatialWeight,\n \"quadrature.nodes[\" +\n index +\n \"].combinedNormalizedSpatialWeight\"\n );\n requireNonNegativeFinite(\n node.combinedAreaMeasureSquareMicrometers,\n \"quadrature.nodes[\" +\n index +\n \"].combinedAreaMeasureSquareMicrometers\"\n );\n\n return identity;\n}\n\n\n/**\n * Rechecks node coordinates, weights, geometric measures and coverage before a spatial plan is trusted\n * as scientific input.\n *\n * Average weights and geometric area measures are kept separate. The geometry does not establish QE,\n * an effective light-collection area or calibrated light transport.\n * @param quadrature - The input; see `SensorSpatialSamplingQuadrature` for its fields, units and coordinates.\n * @returns The result. Success does not imply calibration.\n *\n * @see docs/API_REFERENCE.md for the package export and its full type definitions.\n */\nexport function validateSensorSpatialSamplingQuadrature(\n quadrature: SensorSpatialSamplingQuadrature\n): Map<string, SensorSpatialSamplingQuadratureNode> {\n if (\n typeof quadrature !== \"object\" ||\n quadrature === null ||\n !Array.isArray(quadrature.nodes)\n ) {\n throw new InvalidConfigurationError(\n \"quadrature must contain a nodes array.\"\n );\n }\n\n const totalNodeCount =\n requireSafeNonNegativeInteger(\n quadrature.totalNodeCount,\n \"quadrature.totalNodeCount\"\n );\n\n if (\n totalNodeCount === 0 ||\n quadrature.nodes.length !==\n totalNodeCount\n ) {\n throw new InvalidScientificInputError(\n \"quadrature.totalNodeCount must be positive and exactly match quadrature.nodes.length.\"\n );\n }\n\n if (\n quadrature.colorSamplingProfileId !== undefined &&\n (\n typeof quadrature.colorSamplingProfileId !== \"string\" ||\n quadrature.colorSamplingProfileId.trim().length === 0\n )\n ) {\n throw new InvalidConfigurationError(\n \"quadrature.colorSamplingProfileId must be a non-empty string when supplied.\"\n );\n }\n\n if (\n typeof quadrature.channelId !== \"string\" ||\n quadrature.channelId.trim().length === 0\n ) {\n throw new InvalidConfigurationError(\n \"quadrature.channelId must be a non-empty string.\"\n );\n }\n\n requireSafeNonNegativeInteger(\n quadrature.site.x,\n \"quadrature.site.x\"\n );\n requireSafeNonNegativeInteger(\n quadrature.site.y,\n \"quadrature.site.y\"\n );\n\n const geometricArea =\n requirePositiveFinite(\n quadrature.geometricApertureAreaSquareMicrometers,\n \"quadrature.geometricApertureAreaSquareMicrometers\"\n );\n\n const nodesByKey =\n new Map<\n string,\n SensorSpatialSamplingQuadratureNode\n >();\n let weightSum = 0;\n let areaSum = 0;\n\n quadrature.nodes.forEach(\n (node, index) => {\n const identity =\n validateQuadratureNode(node, index);\n const key = sensorSpatialQuadratureNodeIdentityKey(identity);\n\n if (nodesByKey.has(key)) {\n throw new InvalidScientificInputError(\n \"quadrature contains duplicate node identities.\"\n );\n }\n\n nodesByKey.set(key, node);\n weightSum +=\n node.combinedNormalizedSpatialWeight;\n areaSum +=\n node.combinedAreaMeasureSquareMicrometers;\n\n if (\n !Number.isFinite(weightSum) ||\n !Number.isFinite(areaSum)\n ) {\n throw new InvalidScientificInputError(\n \"quadrature aggregate weights/measures must remain finite.\"\n );\n }\n }\n );\n\n if (\n !approximatelyEqual(\n weightSum,\n 1,\n WEIGHT_TOLERANCE\n )\n ) {\n throw new InvalidScientificInputError(\n \"quadrature combined normalized spatial weights must sum to 1.\"\n );\n }\n\n if (\n !approximatelyEqual(\n areaSum,\n geometricArea,\n AREA_TOLERANCE\n )\n ) {\n throw new InvalidScientificInputError(\n \"quadrature combined geometric area measures must sum to the geometric aperture area.\"\n );\n }\n\n if (\n !approximatelyEqual(\n quadrature.normalizedSpatialWeightSum,\n weightSum,\n WEIGHT_TOLERANCE\n )\n ) {\n throw new InvalidScientificInputError(\n \"quadrature.normalizedSpatialWeightSum does not match node weights.\"\n );\n }\n\n if (\n !approximatelyEqual(\n quadrature.combinedAreaMeasureSumSquareMicrometers,\n areaSum,\n AREA_TOLERANCE\n )\n ) {\n throw new InvalidScientificInputError(\n \"quadrature.combinedAreaMeasureSumSquareMicrometers does not match node measures.\"\n );\n }\n\n return nodesByKey;\n}\n\nfunction parseNodeValues(\n nodeValues: readonly SensorSpatialQuadratureNodeValue[],\n nodesByKey: ReadonlyMap<\n string,\n SensorSpatialSamplingQuadratureNode\n >\n): Map<string, number> {\n if (!Array.isArray(nodeValues)) {\n throw new InvalidConfigurationError(\n \"nodeValues must be an array.\"\n );\n }\n\n if (\n nodeValues.length !==\n nodesByKey.size\n ) {\n throw new InvalidScientificInputError(\n \"nodeValues must contain exactly one value for every quadrature node.\"\n );\n }\n\n const valuesByKey = new Map<string, number>();\n\n nodeValues.forEach((entry, index) => {\n if (\n typeof entry !== \"object\" ||\n entry === null ||\n typeof entry.node !== \"object\" ||\n entry.node === null\n ) {\n throw new InvalidScientificInputError(\n \"nodeValues[\" +\n index +\n \"] must contain a node identity object.\"\n );\n }\n\n const identity = {\n antiAliasingComponentIndex:\n requireSafeNonNegativeInteger(\n entry.node\n .antiAliasingComponentIndex,\n \"nodeValues[\" +\n index +\n \"].node.antiAliasingComponentIndex\"\n ),\n apertureSampleXIndex:\n requireSafeNonNegativeInteger(\n entry.node.apertureSampleXIndex,\n \"nodeValues[\" +\n index +\n \"].node.apertureSampleXIndex\"\n ),\n apertureSampleYIndex:\n requireSafeNonNegativeInteger(\n entry.node.apertureSampleYIndex,\n \"nodeValues[\" +\n index +\n \"].node.apertureSampleYIndex\"\n )\n };\n const key = sensorSpatialQuadratureNodeIdentityKey(identity);\n\n if (!nodesByKey.has(key)) {\n throw new InvalidScientificInputError(\n \"nodeValues[\" +\n index +\n \"] identifies a node that is not present in quadrature.\"\n );\n }\n if (valuesByKey.has(key)) {\n throw new InvalidScientificInputError(\n \"nodeValues contains duplicate node identities.\"\n );\n }\n\n valuesByKey.set(\n key,\n requireNonNegativeFinite(\n entry.value,\n \"nodeValues[\" +\n index +\n \"].value\"\n )\n );\n });\n\n return valuesByKey;\n}\n\n/**\n * Combines nonnegative linear values, each tagged with its node, over a\n * validated sensor spatial quadrature.\n *\n * It does not sample a renderer or optical model itself. Callers evaluate one\n * value at every node's preAntiAliasingSourcePointMm and pass the values with\n * their exact node identity. Order does not matter.\n *\n * Two value domains are supported:\n *\n * - relative-linear: a dimensionless, nonnegative stand-in for irradiance, for\n * deterministic renderer and science regression tests;\n * - radiometric-irradiance: physical irradiance on the sensor plane in W/m^2.\n *\n * The destination site's channelId is a label only. No CFA spectral\n * transmittance or sensitivity is applied, so do not call the output a\n * colour-filtered mosaic sample or a RAW value.\n */\nexport function reduceSensorSpatialSamplingQuadrature(\n input: ReduceSensorSpatialSamplingQuadratureInput\n): CalculationResult<SensorSpatialSampleReduction> {\n requireInputKeys(input, \"reduceSensorSpatialSamplingQuadrature\", [\"quadrature\", \"valueDomain\", \"nodeValues\"]);\n const domain = parseValueDomain(\n input.valueDomain\n );\n const nodesByKey = validateSensorSpatialSamplingQuadrature(\n input.quadrature\n );\n const valuesByKey = parseNodeValues(\n input.nodeValues,\n nodesByKey\n );\n\n let weightedAverage = 0;\n let areaWeightedIntegral = 0;\n\n nodesByKey.forEach((node, key) => {\n const value = valuesByKey.get(key);\n if (value === undefined) {\n throw new InvalidScientificInputError(\n \"A quadrature node is missing its explicit source value.\"\n );\n }\n\n weightedAverage +=\n value *\n node.combinedNormalizedSpatialWeight;\n areaWeightedIntegral +=\n value *\n node.combinedAreaMeasureSquareMicrometers;\n\n if (\n !Number.isFinite(weightedAverage) ||\n !Number.isFinite(areaWeightedIntegral)\n ) {\n throw new InvalidScientificInputError(\n \"Reduced spatial sample must remain finite.\"\n );\n }\n });\n\n const reducedValue:\n SensorSpatialSampleReductionValue =\n domain.kind === \"relative-linear\"\n ? {\n kind: \"relative-linear\",\n unit: \"relative\",\n semantic:\n \"nonnegative-sensor-plane-irradiance-proxy\",\n normalizedSpatialAverageRelative:\n weightedAverage,\n geometricAreaIntegralRelativeSquareMicrometers:\n areaWeightedIntegral\n }\n : {\n kind: \"radiometric-irradiance\",\n unit: \"W/m^2\",\n semantic:\n \"sensor-plane-irradiance\",\n normalizedSpatialAverageIrradianceWattsPerSquareMeter:\n weightedAverage,\n geometricApertureIncidentFluxWatts:\n areaWeightedIntegral *\n SQUARE_MICROMETERS_TO_SQUARE_METERS\n };\n\n return approximationResult(\n {\n site: {\n ...input.quadrature.site\n },\n channelId:\n input.quadrature.channelId,\n outputMeaning:\n \"channel-tagged-pre-response-spatial-sample\",\n valueDomain: domain,\n nodeCount:\n input.quadrature.totalNodeCount,\n sourceValuesMatchedBy:\n \"quadrature-node-identity\",\n sourceValuesSuppliedForAllNodes: true,\n outsideImagingAreaSourceValuesRequired:\n input.quadrature\n .preAntiAliasingSourceOutsideImagingAreaNodeCount >\n 0,\n normalizedSpatialWeightSum:\n input.quadrature\n .normalizedSpatialWeightSum,\n geometricApertureAreaSquareMicrometers:\n input.quadrature\n .geometricApertureAreaSquareMicrometers,\n reducedValue,\n cfaSpectralFilteringApplied: false,\n channelIdIsSpectralResponse: false,\n temporalIntegrationApplied: false,\n exposureDurationApplied: false,\n quantumEfficiencyApplied: false,\n microlensResponseApplied: false,\n opticalThroughputAppliedByReducer:\n false,\n photonsCalculated: false,\n electronsCalculated: false,\n shotNoiseApplied: false,\n readNoiseApplied: false,\n adcQuantizationApplied: false,\n blackLevelApplied: false,\n saturationApplied: false,\n rawCodeValueProduced: false,\n demosaicOrReconstructionApplied: false\n },\n \"sensor-spatial-sample-reduction\",\n \"1.0.0\",\n [\n \"Input values are nonnegative linear scalars evaluated at each quadrature node's pre-AA source coordinate.\",\n \"Node values are matched by explicit quadrature-node identity rather than positional array order.\",\n \"The destination CFA/color channel is metadata only; no spectral transmittance, sensor spectral responsivity or quantum efficiency is applied.\",\n \"Relative-linear values are dimensionless irradiance-like proxies for deterministic testing and do not become physical radiometry.\",\n \"Radiometric-irradiance values use W/m^2; geometric aperture integration converts square micrometres to square metres to report incident radiant flux in watts.\",\n \"The physical irradiance branch reports geometric-aperture incident flux under the caller-supplied irradiance field and normalized AA spatial redistribution; it does not add omitted optical-stack throughput, microlens response, CFA response or sensor conversion.\",\n \"Every quadrature node requires an explicit source value, including pre-AA source points outside the active imaging area; no automatic clamping, zero-fill, extrapolation or renormalization occurs.\",\n \"Negative, NaN and infinite source values are rejected; signed intermediate color-transform values are outside this pre-response sensor-plane contract.\",\n \"Temporal exposure integration, wavelength integration, photons/electrons, noise, saturation, black level, ADC quantization, RAW code values and reconstruction remain separate future work.\"\n ]\n );\n}\n"]}
|
|
@@ -17,6 +17,7 @@ import { createSensorSpectralResponseChannelBinding, parseSensorSpectralResponse
|
|
|
17
17
|
import { requireInputKeys, requireInputPositiveFinite as requirePositiveFinite, requireInputNonNegativeSafeInteger as requireNonNegativeSafeInteger } from "../core/record-validation.js";
|
|
18
18
|
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
19
19
|
import { responseCurves } from "./spectral-coverage.js";
|
|
20
|
+
import { approximatelyEqual } from "../core/shared-helpers.js";
|
|
20
21
|
const MAX_SPECTRAL_QUADRATURE_NODES = 100_000;
|
|
21
22
|
function channelResponse(profile, channelId) {
|
|
22
23
|
if (typeof channelId !== "string" ||
|
|
@@ -142,12 +143,6 @@ function planSegments(boundaries, maximumSubintervalWidthNanometers) {
|
|
|
142
143
|
totalNodeCount
|
|
143
144
|
};
|
|
144
145
|
}
|
|
145
|
-
const SPECTRAL_VALIDATION_TOLERANCE = 1e-10;
|
|
146
|
-
function approximatelyEqual(actual, expected, tolerance = SPECTRAL_VALIDATION_TOLERANCE) {
|
|
147
|
-
const scale = Math.max(1, Math.abs(actual), Math.abs(expected));
|
|
148
|
-
return (Math.abs(actual - expected) <=
|
|
149
|
-
tolerance * scale);
|
|
150
|
-
}
|
|
151
146
|
/**
|
|
152
147
|
* Rechecks a supplied wavelength plan (node identities, coverage and widths in nanometres) before it
|
|
153
148
|
* is used.
|