@photivra/engine 2.19.0 → 2.20.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 +112 -1
- package/CITATION.cff +1 -1
- package/NODE_SUPPORT.md +7 -5
- package/README.md +6 -6
- package/SECURITY.md +1 -1
- package/THIRD_PARTY.md +14 -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/native-environment-lean.js +1 -5
- package/dist/capture/native-environment-lean.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/sensor-raw-producer.js +3 -5
- 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 +8 -2
- package/dist/equipment/generic-lens-catalogue.js +31 -2
- package/dist/equipment/generic-lens-catalogue.js.map +1 -1
- package/dist/equipment/generic-tier-assets.js +2 -3
- 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/facade/display-view-gpu.d.ts +7 -4
- 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 +5 -3
- 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 +1 -1
- package/dist/facade/display-view.js.map +1 -1
- package/dist/facade/fast-methods.d.ts +50 -0
- package/dist/facade/fast-methods.js +50 -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 +3 -2
- package/dist/facade/generic-lens-capture.js +3 -2
- package/dist/facade/generic-lens-capture.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +51 -11
- package/dist/facade/simulate-capture.js +144 -75
- 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 +22 -12
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- 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-psf.d.ts +18 -2
- package/dist/optics/generic-lens-psf.js +25 -2
- package/dist/optics/generic-lens-psf.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.js +2 -2
- package/dist/output/developed-lens-corrections.js.map +1 -1
- package/dist/output/generic-lens-corrections.d.ts +3 -1
- package/dist/output/generic-lens-corrections.js +8 -4
- 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-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/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 +137 -0
- package/dist/sensor/sensor-characteristics.js +115 -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 +4 -0
- package/docs/API_REFERENCE.md +76 -2
- package/docs/QUICK_START.md +88 -6
- package/docs/USAGE.md +65 -2
- 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 +7 -2
- package/docs/api/exports.json +152 -2
- package/docs/api/facade-display-view-gpu.md +5 -2
- package/docs/api/facade-fast-methods.md +91 -0
- package/docs/api/facade-simulate-capture.md +36 -11
- package/docs/api/facade-simulate-display-view.md +3 -2
- package/docs/api/optics-aperture-diameter.md +51 -0
- package/docs/api/optics-detail-limit.md +203 -0
- package/docs/api/output-crop-to-match-focal-length.md +67 -0
- package/docs/api/output-generic-lens-corrections.md +3 -1
- package/docs/api/output-lens-corrections.md +11 -1
- package/docs/api/output-print-size-blur-noise.md +115 -0
- package/docs/api/sensor-sensor-characteristics.md +105 -0
- package/llms.txt +4 -0
- package/package.json +1 -1
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/** Print size limit that counts the capture's blur and noise as well as its pixels (#574).
|
|
3
|
+
* A pure function: it makes no image and is not used by the capture path.
|
|
4
|
+
* @see docs/PRINT_PLANNING.md, "Print size limit with blur and noise", for the equations, sources and limits.
|
|
5
|
+
*/
|
|
6
|
+
import { approximationResult } from "../core/calculation-result.js";
|
|
7
|
+
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
8
|
+
import { requireAllowlistedRecord, requireInputKeys, requireInputNonNegativeFinite, requireInputPositiveFinite } from "../core/record-validation.js";
|
|
9
|
+
import { requirePositiveInteger } from "../core/validation.js";
|
|
10
|
+
import { ENGINE_API_VERSION } from "../core/version.js";
|
|
11
|
+
import { calculateSignalToNoise } from "../sensor/signal-noise.js";
|
|
12
|
+
const UNIT_MM = { mm: 1, cm: 10, inches: 25.4, m: 1000 };
|
|
13
|
+
const RAD_PER_ARCMIN = Math.PI / 10800;
|
|
14
|
+
const BLUR_PARTS = ["lens", "defocus", "diffraction", "motion"];
|
|
15
|
+
function record(value, keys) {
|
|
16
|
+
return requireAllowlistedRecord(value, keys, "Invalid or unknown print size limit fields.");
|
|
17
|
+
}
|
|
18
|
+
function parseBlur(value) {
|
|
19
|
+
const r = record(value, ["kind", "lensMm", "defocusMm", "diffractionMm", "motionMm", "diameterMm"]);
|
|
20
|
+
if (r.kind === "measured-total") {
|
|
21
|
+
record(value, ["kind", "diameterMm"]);
|
|
22
|
+
const diameterMm = requireInputNonNegativeFinite(r.diameterMm, "blur.diameterMm");
|
|
23
|
+
return { diameterMm, largestComponent: diameterMm > 0 ? "measured-total" : null };
|
|
24
|
+
}
|
|
25
|
+
if (r.kind !== "components")
|
|
26
|
+
throw new InvalidConfigurationError("Invalid print blur kind.");
|
|
27
|
+
let sumSquares = 0, largest = 0, largestComponent = null;
|
|
28
|
+
for (const part of BLUR_PARTS) {
|
|
29
|
+
const raw = r[`${part}Mm`];
|
|
30
|
+
if (raw === undefined)
|
|
31
|
+
continue;
|
|
32
|
+
const d = requireInputNonNegativeFinite(raw, `blur.${part}Mm`);
|
|
33
|
+
sumSquares += d * d;
|
|
34
|
+
if (d > largest) {
|
|
35
|
+
largest = d;
|
|
36
|
+
largestComponent = part;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return { diameterMm: Math.sqrt(sumSquares), largestComponent };
|
|
40
|
+
}
|
|
41
|
+
/** The one place the noise input becomes a per-pixel SNR, including a capture's own noise figure (#535). */
|
|
42
|
+
function resolvePixelSnr(value) {
|
|
43
|
+
const r = record(value, ["kind", "signalElectrons", "readNoiseElectrons", "snrLinear", "midGrey"]);
|
|
44
|
+
switch (r.kind) {
|
|
45
|
+
case "shot-and-read-noise": {
|
|
46
|
+
record(value, ["kind", "signalElectrons", "readNoiseElectrons"]);
|
|
47
|
+
return calculateSignalToNoise({ signalElectrons: r.signalElectrons, readNoiseElectrons: r.readNoiseElectrons }).value.snrLinear;
|
|
48
|
+
}
|
|
49
|
+
case "pixel-snr":
|
|
50
|
+
record(value, ["kind", "snrLinear"]);
|
|
51
|
+
return requireInputNonNegativeFinite(r.snrLinear, "noise.snrLinear");
|
|
52
|
+
case "capture-noise-figure": {
|
|
53
|
+
record(value, ["kind", "midGrey"]);
|
|
54
|
+
const figure = r.midGrey;
|
|
55
|
+
if (typeof figure !== "object" || figure === null || typeof figure.noiseElectrons !== "object" || figure.noiseElectrons === null)
|
|
56
|
+
throw new InvalidConfigurationError("noise.midGrey must be a capture's result.sensor.midGrey (simulateCapture with fast: false).");
|
|
57
|
+
if (figure.clipped !== false)
|
|
58
|
+
throw new InvalidConfigurationError("noise.midGrey is clipped (or not a capture's figure): the patch reaches white, where the figure no longer describes the image.");
|
|
59
|
+
return requireInputNonNegativeFinite(figure.signalElectrons, "noise.midGrey.signalElectrons") / requireInputPositiveFinite(figure.noiseElectrons.total, "noise.midGrey.noiseElectrons.total");
|
|
60
|
+
}
|
|
61
|
+
default: throw new InvalidConfigurationError("Invalid print noise kind.");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Calculates the largest print, at a fixed viewing distance, at which the capture still looks sharp and clean, and names
|
|
65
|
+
* the factor that limits it: pixels, blur or noise. An approximation throughout; the equations are exact under its model.
|
|
66
|
+
*
|
|
67
|
+
* With θ the criterion angle, d the viewing distance, s = 2 d tan(θ/2) the length θ covers on the print, N_w the raster
|
|
68
|
+
* width in pixels, b the blur diameter in pixels and SNR_px the per-pixel SNR (print width W, height in proportion):
|
|
69
|
+
* - pixels: a print pixel (W / N_w) may cover at most s, so W ≤ N_w·s (as `calculatePrintPlan` with
|
|
70
|
+
* `angular-pixel-pitch`, before its whole-pixel rounding);
|
|
71
|
+
* - blur: the blur on the print (b·W / N_w) may cover at most s, so W ≤ N_w·s / b: the criterion of an acceptable circle
|
|
72
|
+
* of confusion with the eye's resolution as the limit;
|
|
73
|
+
* - noise: the eye averages the (W / N_w) wide pixels inside s, so the visible SNR is SNR_px·(N_w·s / W) while a pixel is
|
|
74
|
+
* smaller than s (the mean of n independent values has 1/√n of their standard deviation); W ≤ N_w·s·SNR_px / SNR_min,
|
|
75
|
+
* and noise never limits when SNR_px ≥ SNR_min.
|
|
76
|
+
* One arcminute is normal (20/20) acuity under optimal conditions (Z. Alomar, K. D. Bojikian, "Visual Acuity Testing in
|
|
77
|
+
* Adults", EyeWiki, American Academy of Ophthalmology, https://eyewiki.aao.org/Visual_Acuity_Testing_in_Adults, last edited
|
|
78
|
+
* June 16, 2026); the SNR threshold has no default and is the caller's. Blur parts add in root-sum-square, which is exact
|
|
79
|
+
* for Gaussian spots (variances of independent displacements add: C. M. Grinstead, J. L. Snell, "Introduction to
|
|
80
|
+
* Probability", 2nd rev. ed., AMS 1997, sec. 6.2, free under the GNU FDL) and an approximation for discs and Airy patterns.
|
|
81
|
+
*/
|
|
82
|
+
export function calculatePrintSizeLimitWithBlurAndNoise(value) {
|
|
83
|
+
const keys = ["raster", "pixelPitchMm", "viewingDistance", "criterion", "blur", "noise"];
|
|
84
|
+
requireInputKeys(value, "calculatePrintSizeLimitWithBlurAndNoise", keys);
|
|
85
|
+
const r = record(value, keys);
|
|
86
|
+
const raster = record(r.raster, ["pixelWidth", "pixelHeight"]);
|
|
87
|
+
const pixelWidth = requireInputPositiveFinite(raster.pixelWidth, "raster.pixelWidth"), pixelHeight = requireInputPositiveFinite(raster.pixelHeight, "raster.pixelHeight");
|
|
88
|
+
requirePositiveInteger("raster.pixelWidth", pixelWidth);
|
|
89
|
+
requirePositiveInteger("raster.pixelHeight", pixelHeight);
|
|
90
|
+
const pixelPitchMm = requireInputPositiveFinite(r.pixelPitchMm, "pixelPitchMm");
|
|
91
|
+
const distance = record(r.viewingDistance, ["value", "unit"]);
|
|
92
|
+
if (typeof distance.unit !== "string" || !Object.hasOwn(UNIT_MM, distance.unit))
|
|
93
|
+
throw new InvalidConfigurationError("Invalid Print length unit.");
|
|
94
|
+
const viewingDistanceMm = requireInputPositiveFinite(distance.value, "viewingDistance.value") * UNIT_MM[distance.unit];
|
|
95
|
+
const criterion = record(r.criterion, ["kind", "arcminutes", "minimumVisibleSnr"]);
|
|
96
|
+
if (criterion.kind !== "visual-resolution")
|
|
97
|
+
throw new InvalidConfigurationError("Invalid print criterion kind.");
|
|
98
|
+
const arcminutes = requireInputPositiveFinite(criterion.arcminutes, "criterion.arcminutes");
|
|
99
|
+
if (arcminutes >= 10800)
|
|
100
|
+
throw new InvalidConfigurationError("criterion.arcminutes must lie in (0, 10800).");
|
|
101
|
+
const minimumVisibleSnr = requireInputPositiveFinite(criterion.minimumVisibleSnr, "criterion.minimumVisibleSnr");
|
|
102
|
+
const blur = parseBlur(r.blur);
|
|
103
|
+
const pixelSnr = resolvePixelSnr(r.noise);
|
|
104
|
+
const resolvedSpotMm = 2 * viewingDistanceMm * Math.tan((arcminutes * RAD_PER_ARCMIN) / 2);
|
|
105
|
+
const pixelLimitWidth = pixelWidth * resolvedSpotMm;
|
|
106
|
+
const blurPixels = blur.diameterMm / pixelPitchMm;
|
|
107
|
+
const blurLimitWidth = blurPixels > 0 ? pixelLimitWidth / blurPixels : null;
|
|
108
|
+
const noiseLimitWidth = pixelSnr < minimumVisibleSnr ? pixelLimitWidth * (pixelSnr / minimumVisibleSnr) : null;
|
|
109
|
+
let limitingFactor = "pixels", width = pixelLimitWidth;
|
|
110
|
+
if (blurLimitWidth !== null && blurLimitWidth < width) {
|
|
111
|
+
limitingFactor = "blur";
|
|
112
|
+
width = blurLimitWidth;
|
|
113
|
+
}
|
|
114
|
+
if (noiseLimitWidth !== null && noiseLimitWidth < width) {
|
|
115
|
+
limitingFactor = "noise";
|
|
116
|
+
width = noiseLimitWidth;
|
|
117
|
+
}
|
|
118
|
+
const size = (w) => ({ width: w, height: w * (pixelHeight / pixelWidth) });
|
|
119
|
+
const visibleSnrAtMaximum = width > 0 ? pixelSnr * Math.max(1, pixelLimitWidth / width) : 0;
|
|
120
|
+
const result = {
|
|
121
|
+
engineApiVersion: ENGINE_API_VERSION, status: width > 0 ? "finite" : "no-positive-size", limitingFactor,
|
|
122
|
+
maximumImageMm: size(width),
|
|
123
|
+
limits: { pixels: size(pixelLimitWidth), blur: blurLimitWidth === null ? null : size(blurLimitWidth), noise: noiseLimitWidth === null ? null : size(noiseLimitWidth) },
|
|
124
|
+
viewingDistanceMm, resolvedSpotMm,
|
|
125
|
+
blur: { diameterMm: blur.diameterMm, diameterPixels: blurPixels, largestComponent: blur.largestComponent },
|
|
126
|
+
noise: { pixelSnr, visibleSnrAtMaximum }
|
|
127
|
+
};
|
|
128
|
+
const numbers = [viewingDistanceMm, resolvedSpotMm, pixelLimitWidth, size(pixelLimitWidth).height, blurPixels, blurLimitWidth ?? 0, width, visibleSnrAtMaximum];
|
|
129
|
+
if (!numbers.every(Number.isFinite) || !(pixelLimitWidth > 0))
|
|
130
|
+
throw new InvalidConfigurationError("Print size limit lies outside the supported numeric range.");
|
|
131
|
+
return approximationResult(result, "print-size-limit-blur-noise", "1.0.0", [
|
|
132
|
+
"Flat print viewed at a fixed distance, at its center; one visual-resolution angle for pixels, blur and noise.",
|
|
133
|
+
"Blur parts combine in root-sum-square: exact for Gaussian spots, an approximation for discs and Airy patterns.",
|
|
134
|
+
"Pixel noise is white (independent pixels); demosaic, denoise and sharpening correlations are not modeled.",
|
|
135
|
+
"The eye averages noise over a square of the criterion angle; contrast sensitivity and print medium are not modeled."
|
|
136
|
+
]);
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=print-size-blur-noise.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"print-size-blur-noise.js","sourceRoot":"","sources":["../../src/output/print-size-blur-noise.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/** Print size limit that counts the capture's blur and noise as well as its pixels (#574).\n * A pure function: it makes no image and is not used by the capture path.\n * @see docs/PRINT_PLANNING.md, \"Print size limit with blur and noise\", for the equations, sources and limits.\n */\nimport { approximationResult, type CalculationResult } from \"../core/calculation-result.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { requireAllowlistedRecord, requireInputKeys, requireInputNonNegativeFinite, requireInputPositiveFinite } from \"../core/record-validation.js\";\nimport { requirePositiveInteger } from \"../core/validation.js\";\nimport { ENGINE_API_VERSION } from \"../core/version.js\";\nimport { calculateSignalToNoise } from \"../sensor/signal-noise.js\";\nimport type { RasterDimensions } from \"../sensor/sensor-geometry.js\";\nimport type { PrintLength } from \"./print-plan.js\";\nimport type { SimulateCaptureSensorReport } from \"../sensor/sensor-characteristics.js\";\n\n/** The capture's blur at the sensor plane, as blur-spot diameters in millimetres.\n * `components` combines the parts by root-sum-square (an approximation, see the docs); `measured-total` is one measured diameter.\n * A zero component is allowed and means that part is left out. Engine helpers give the parts: `calculateDefocusCircle` (defocus),\n * `calculateAiryDisk` (diffraction, micrometres), `calculateProjectedMotionBlur` (motion length).\n */\nexport type PrintBlurInput =\n | { kind: \"components\"; lensMm?: number; defocusMm?: number; diffractionMm?: number; motionMm?: number }\n | { kind: \"measured-total\"; diameterMm: number };\n/** The noise of one pixel of the printed raster, for the tone the caller cares about (for example mid-gray).\n * `shot-and-read-noise` uses the engine's noise model (`calculateSignalToNoise`: Poisson shot noise plus RMS read noise);\n * `pixel-snr` is a per-pixel signal-to-noise ratio the caller already has. `capture-noise-figure` (2.20.0) takes a capture's own\n * mid-gray noise figure (#535: `result.sensor.midGrey` of a `simulateCapture` with `fast: false`), whose SNR S/σ_total counts photon\n * shot, read, dark-current, quantization and any fixed-pattern noise; it is one sensor site's, so it applies as the pixel SNR when one\n * printed pixel is one capture pixel. A clipped figure (the patch at white) is refused.\n */\nexport type PrintNoiseInput =\n | { kind: \"shot-and-read-noise\"; signalElectrons: number; readNoiseElectrons: number }\n | { kind: \"pixel-snr\"; snrLinear: number }\n | { kind: \"capture-noise-figure\"; midGrey: NonNullable<SimulateCaptureSensorReport[\"midGrey\"]> };\n/** What \"looks sharp and clean\" means: the smallest angle the viewer resolves, and the lowest signal-to-noise ratio the viewer\n * accepts after the eye averages over that angle. Both are the caller's choice; one arcminute is normal (20/20) acuity.\n */\nexport interface PrintSharpCleanCriterion {\n kind: \"visual-resolution\";\n arcminutes: number;\n minimumVisibleSnr: number;\n}\n/** Input. The raster is the retained image that is printed (after any crop), with square pixels; `pixelPitchMm` is the\n * sensor length one of those pixels covers. The print keeps the raster's aspect ratio. The viewing distance is fixed.\n */\nexport interface PrintSizeLimitWithBlurAndNoiseInput {\n raster: RasterDimensions;\n pixelPitchMm: number;\n viewingDistance: PrintLength;\n criterion: PrintSharpCleanCriterion;\n blur: PrintBlurInput;\n noise: PrintNoiseInput;\n}\n/** A print size in millimetres at the raster's aspect ratio. */\nexport interface PrintImageMm { width: number; height: number }\n/** Result. Each factor's own limit is the largest print it allows on its own; null means that factor never limits.\n * `limitingFactor` names the smallest; on a tie the order is pixels, blur, noise.\n */\nexport interface PrintSizeLimitWithBlurAndNoise {\n engineApiVersion: string;\n status: \"finite\" | \"no-positive-size\";\n limitingFactor: \"pixels\" | \"blur\" | \"noise\";\n maximumImageMm: PrintImageMm;\n limits: { pixels: PrintImageMm; blur: PrintImageMm | null; noise: PrintImageMm | null };\n viewingDistanceMm: number;\n /** The criterion angle as a length on the print at the viewing distance: 2 d tan(θ/2). */\n resolvedSpotMm: number;\n blur: { diameterMm: number; diameterPixels: number; largestComponent: \"lens\" | \"defocus\" | \"diffraction\" | \"motion\" | \"measured-total\" | null };\n noise: { pixelSnr: number; visibleSnrAtMaximum: number };\n}\n\nconst UNIT_MM = { mm: 1, cm: 10, inches: 25.4, m: 1000 } as const;\nconst RAD_PER_ARCMIN = Math.PI / 10800;\nconst BLUR_PARTS = [\"lens\", \"defocus\", \"diffraction\", \"motion\"] as const;\n\nfunction record(value: unknown, keys: readonly string[]): Record<string, unknown> {\n return requireAllowlistedRecord(value, keys, \"Invalid or unknown print size limit fields.\");\n}\nfunction parseBlur(value: unknown): { diameterMm: number; largestComponent: PrintSizeLimitWithBlurAndNoise[\"blur\"][\"largestComponent\"] } {\n const r = record(value, [\"kind\", \"lensMm\", \"defocusMm\", \"diffractionMm\", \"motionMm\", \"diameterMm\"]);\n if (r.kind === \"measured-total\") {\n record(value, [\"kind\", \"diameterMm\"]);\n const diameterMm = requireInputNonNegativeFinite(r.diameterMm, \"blur.diameterMm\");\n return { diameterMm, largestComponent: diameterMm > 0 ? \"measured-total\" : null };\n }\n if (r.kind !== \"components\") throw new InvalidConfigurationError(\"Invalid print blur kind.\");\n let sumSquares = 0, largest = 0, largestComponent: PrintSizeLimitWithBlurAndNoise[\"blur\"][\"largestComponent\"] = null;\n for (const part of BLUR_PARTS) {\n const raw = r[`${part}Mm`];\n if (raw === undefined) continue;\n const d = requireInputNonNegativeFinite(raw, `blur.${part}Mm`);\n sumSquares += d * d;\n if (d > largest) { largest = d; largestComponent = part; }\n }\n return { diameterMm: Math.sqrt(sumSquares), largestComponent };\n}\n/** The one place the noise input becomes a per-pixel SNR, including a capture's own noise figure (#535). */\nfunction resolvePixelSnr(value: unknown): number {\n const r = record(value, [\"kind\", \"signalElectrons\", \"readNoiseElectrons\", \"snrLinear\", \"midGrey\"]);\n switch (r.kind) {\n case \"shot-and-read-noise\": {\n record(value, [\"kind\", \"signalElectrons\", \"readNoiseElectrons\"]);\n return calculateSignalToNoise({ signalElectrons: r.signalElectrons as number, readNoiseElectrons: r.readNoiseElectrons as number }).value.snrLinear;\n }\n case \"pixel-snr\":\n record(value, [\"kind\", \"snrLinear\"]);\n return requireInputNonNegativeFinite(r.snrLinear, \"noise.snrLinear\");\n case \"capture-noise-figure\": {\n record(value, [\"kind\", \"midGrey\"]);\n const figure = r.midGrey as { signalElectrons?: unknown; noiseElectrons?: { total?: unknown }; clipped?: unknown } | null;\n if (typeof figure !== \"object\" || figure === null || typeof figure.noiseElectrons !== \"object\" || figure.noiseElectrons === null)\n throw new InvalidConfigurationError(\"noise.midGrey must be a capture's result.sensor.midGrey (simulateCapture with fast: false).\");\n if (figure.clipped !== false) throw new InvalidConfigurationError(\"noise.midGrey is clipped (or not a capture's figure): the patch reaches white, where the figure no longer describes the image.\");\n return requireInputNonNegativeFinite(figure.signalElectrons, \"noise.midGrey.signalElectrons\") / requireInputPositiveFinite(figure.noiseElectrons.total, \"noise.midGrey.noiseElectrons.total\");\n }\n default: throw new InvalidConfigurationError(\"Invalid print noise kind.\");\n }\n}\n\n/** Calculates the largest print, at a fixed viewing distance, at which the capture still looks sharp and clean, and names\n * the factor that limits it: pixels, blur or noise. An approximation throughout; the equations are exact under its model.\n *\n * With θ the criterion angle, d the viewing distance, s = 2 d tan(θ/2) the length θ covers on the print, N_w the raster\n * width in pixels, b the blur diameter in pixels and SNR_px the per-pixel SNR (print width W, height in proportion):\n * - pixels: a print pixel (W / N_w) may cover at most s, so W ≤ N_w·s (as `calculatePrintPlan` with\n * `angular-pixel-pitch`, before its whole-pixel rounding);\n * - blur: the blur on the print (b·W / N_w) may cover at most s, so W ≤ N_w·s / b: the criterion of an acceptable circle\n * of confusion with the eye's resolution as the limit;\n * - noise: the eye averages the (W / N_w) wide pixels inside s, so the visible SNR is SNR_px·(N_w·s / W) while a pixel is\n * smaller than s (the mean of n independent values has 1/√n of their standard deviation); W ≤ N_w·s·SNR_px / SNR_min,\n * and noise never limits when SNR_px ≥ SNR_min.\n * One arcminute is normal (20/20) acuity under optimal conditions (Z. Alomar, K. D. Bojikian, \"Visual Acuity Testing in\n * Adults\", EyeWiki, American Academy of Ophthalmology, https://eyewiki.aao.org/Visual_Acuity_Testing_in_Adults, last edited\n * June 16, 2026); the SNR threshold has no default and is the caller's. Blur parts add in root-sum-square, which is exact\n * for Gaussian spots (variances of independent displacements add: C. M. Grinstead, J. L. Snell, \"Introduction to\n * Probability\", 2nd rev. ed., AMS 1997, sec. 6.2, free under the GNU FDL) and an approximation for discs and Airy patterns.\n */\nexport function calculatePrintSizeLimitWithBlurAndNoise(value: PrintSizeLimitWithBlurAndNoiseInput): CalculationResult<PrintSizeLimitWithBlurAndNoise> {\n const keys = [\"raster\", \"pixelPitchMm\", \"viewingDistance\", \"criterion\", \"blur\", \"noise\"];\n requireInputKeys(value, \"calculatePrintSizeLimitWithBlurAndNoise\", keys);\n const r = record(value, keys);\n const raster = record(r.raster, [\"pixelWidth\", \"pixelHeight\"]);\n const pixelWidth = requireInputPositiveFinite(raster.pixelWidth, \"raster.pixelWidth\"), pixelHeight = requireInputPositiveFinite(raster.pixelHeight, \"raster.pixelHeight\");\n requirePositiveInteger(\"raster.pixelWidth\", pixelWidth); requirePositiveInteger(\"raster.pixelHeight\", pixelHeight);\n const pixelPitchMm = requireInputPositiveFinite(r.pixelPitchMm, \"pixelPitchMm\");\n const distance = record(r.viewingDistance, [\"value\", \"unit\"]);\n if (typeof distance.unit !== \"string\" || !Object.hasOwn(UNIT_MM, distance.unit)) throw new InvalidConfigurationError(\"Invalid Print length unit.\");\n const viewingDistanceMm = requireInputPositiveFinite(distance.value, \"viewingDistance.value\") * UNIT_MM[distance.unit as PrintLength[\"unit\"]];\n const criterion = record(r.criterion, [\"kind\", \"arcminutes\", \"minimumVisibleSnr\"]);\n if (criterion.kind !== \"visual-resolution\") throw new InvalidConfigurationError(\"Invalid print criterion kind.\");\n const arcminutes = requireInputPositiveFinite(criterion.arcminutes, \"criterion.arcminutes\");\n if (arcminutes >= 10800) throw new InvalidConfigurationError(\"criterion.arcminutes must lie in (0, 10800).\");\n const minimumVisibleSnr = requireInputPositiveFinite(criterion.minimumVisibleSnr, \"criterion.minimumVisibleSnr\");\n const blur = parseBlur(r.blur);\n const pixelSnr = resolvePixelSnr(r.noise);\n\n const resolvedSpotMm = 2 * viewingDistanceMm * Math.tan((arcminutes * RAD_PER_ARCMIN) / 2);\n const pixelLimitWidth = pixelWidth * resolvedSpotMm;\n const blurPixels = blur.diameterMm / pixelPitchMm;\n const blurLimitWidth = blurPixels > 0 ? pixelLimitWidth / blurPixels : null;\n const noiseLimitWidth = pixelSnr < minimumVisibleSnr ? pixelLimitWidth * (pixelSnr / minimumVisibleSnr) : null;\n let limitingFactor: PrintSizeLimitWithBlurAndNoise[\"limitingFactor\"] = \"pixels\", width = pixelLimitWidth;\n if (blurLimitWidth !== null && blurLimitWidth < width) { limitingFactor = \"blur\"; width = blurLimitWidth; }\n if (noiseLimitWidth !== null && noiseLimitWidth < width) { limitingFactor = \"noise\"; width = noiseLimitWidth; }\n const size = (w: number): PrintImageMm => ({ width: w, height: w * (pixelHeight / pixelWidth) });\n const visibleSnrAtMaximum = width > 0 ? pixelSnr * Math.max(1, pixelLimitWidth / width) : 0;\n const result: PrintSizeLimitWithBlurAndNoise = {\n engineApiVersion: ENGINE_API_VERSION, status: width > 0 ? \"finite\" : \"no-positive-size\", limitingFactor,\n maximumImageMm: size(width),\n limits: { pixels: size(pixelLimitWidth), blur: blurLimitWidth === null ? null : size(blurLimitWidth), noise: noiseLimitWidth === null ? null : size(noiseLimitWidth) },\n viewingDistanceMm, resolvedSpotMm,\n blur: { diameterMm: blur.diameterMm, diameterPixels: blurPixels, largestComponent: blur.largestComponent },\n noise: { pixelSnr, visibleSnrAtMaximum }\n };\n const numbers = [viewingDistanceMm, resolvedSpotMm, pixelLimitWidth, size(pixelLimitWidth).height, blurPixels, blurLimitWidth ?? 0, width, visibleSnrAtMaximum];\n if (!numbers.every(Number.isFinite) || !(pixelLimitWidth > 0)) throw new InvalidConfigurationError(\"Print size limit lies outside the supported numeric range.\");\n return approximationResult(result, \"print-size-limit-blur-noise\", \"1.0.0\", [\n \"Flat print viewed at a fixed distance, at its center; one visual-resolution angle for pixels, blur and noise.\",\n \"Blur parts combine in root-sum-square: exact for Gaussian spots, an approximation for discs and Airy patterns.\",\n \"Pixel noise is white (independent pixels); demosaic, denoise and sharpening correlations are not modeled.\",\n \"The eye averages noise over a square of the criterion angle; contrast sensitivity and print medium are not modeled.\"\n ]);\n}\n"]}
|
|
@@ -17,13 +17,7 @@ import { parseSensorColorSamplingProfile } from "./color-sampling.js";
|
|
|
17
17
|
import { createSensorSpectralResponseChannelBinding, requireCurrentSensorSpectralResponseChannelBinding, parseSensorSpectralResponseProfile, resolveParsedSensorSpectralResponseAtWavelength } from "./spectral-response.js";
|
|
18
18
|
import { requireInputObject, requireInputKeys, requireInputNonNegativeFinite as requireNonNegativeFinite, requireInputPositiveFinite as requirePositiveFinite, requireInputNonNegativeSafeInteger as requireSafeNonNegativeInteger } from "../core/record-validation.js";
|
|
19
19
|
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
20
|
-
import { addCompensated } from "../core/shared-helpers.js";
|
|
21
|
-
const REDUCTION_TOLERANCE = 1e-10;
|
|
22
|
-
function approximatelyEqual(actual, expected) {
|
|
23
|
-
const scale = Math.max(1, Math.abs(actual), Math.abs(expected));
|
|
24
|
-
return (Math.abs(actual - expected) <=
|
|
25
|
-
REDUCTION_TOLERANCE * scale);
|
|
26
|
-
}
|
|
20
|
+
import { addCompensated, approximatelyEqual } from "../core/shared-helpers.js";
|
|
27
21
|
/**
|
|
28
22
|
* Identity string for parsed plain data that ignores key order, so records with
|
|
29
23
|
* the same content compare equal whatever order their properties were added in.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eqe-electron-rate.js","sourceRoot":"","sources":["../../src/sensor/eqe-electron-rate.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * EQE electron rate: converts the light power reaching a sensor site into a photon rate and an\n * expected electron rate, using the sensor's EQE (external quantum efficiency, the fraction of\n * incident photons that produce a collected electron).\n *\n * The response is applied at each wavelength node. One broadband-average QE is never applied to the\n * total power. Outputs are rates per second; integrating over the exposure time happens later.\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 type {\n EvidenceProvenance\n} from \"../core/evidence-provenance.js\";\nimport { stringifyCanonicalJson } from \"../core/canonical-json.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport {\n calculatePhotonEnergyFromWavelength,\n type AirRefractiveIndexConditionPolicy,\n type AirRefractiveIndexReferenceConditions,\n type SourcedAirPhaseRefractiveIndex\n} from \"./photon-energy.js\";\nimport type {\n SensorResponseApplicationCompatibilityAssessment\n} from \"./response-application-compatibility.js\";\nimport type {\n SensorResponseOperatingRangeAssessment\n} from \"./response-operating-range.js\";\nimport {\n parseSensorColorSamplingProfile,\n type SensorColorSamplingProfile\n} from \"./color-sampling.js\";\nimport {\n createSensorSpectralResponseChannelBinding,\n requireCurrentSensorSpectralResponseChannelBinding,\n parseSensorSpectralResponseProfile,\n resolveParsedSensorSpectralResponseAtWavelength,\n type ResolvedSensorSpectralResponseValue,\n type SensorSpectralResponseProfile\n} from \"./spectral-response.js\";\nimport type {\n SensorSpatioSpectralIrradianceReduction,\n SensorSpatioSpectralWavelengthReduction\n} from \"./spatio-spectral-reduction.js\";\nimport { requireInputObject, requireInputKeys, requireInputNonNegativeFinite as requireNonNegativeFinite, requireInputPositiveFinite as requirePositiveFinite, requireInputNonNegativeSafeInteger as requireSafeNonNegativeInteger } from \"../core/record-validation.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { addCompensated } from \"../core/shared-helpers.js\";\n\nconst REDUCTION_TOLERANCE = 1e-10;\n\nexport interface SensorEqeAirRefractiveIndexSample {\n spectralSampleIndex: number;\n refractiveIndex:\n SourcedAirPhaseRefractiveIndex;\n}\n\nexport interface SensorEqeAirPhotonEnergyContext {\n samples:\n readonly SensorEqeAirRefractiveIndexSample[];\n operatingConditions?:\n AirRefractiveIndexReferenceConditions;\n conditionPolicy:\n AirRefractiveIndexConditionPolicy;\n}\n\nexport interface CalculateSensorEqeElectronRateInput {\n reduction:\n SensorSpatioSpectralIrradianceReduction;\n compatibility:\n SensorResponseApplicationCompatibilityAssessment;\n operatingRange:\n SensorResponseOperatingRangeAssessment;\n colorSamplingProfile:\n SensorColorSamplingProfile;\n spectralResponseProfile:\n SensorSpectralResponseProfile;\n airPhotonEnergyContext?:\n SensorEqeAirPhotonEnergyContext;\n}\n\nexport interface SensorEqeWavelengthRateContribution {\n spectralSampleIndex: number;\n wavelengthNanometers: number;\n wavelengthMeasureNanometers: number;\n geometricApertureIncidentSpectralFluxWattsPerNanometer:\n number;\n radiantPowerContributionWatts: number;\n vacuumWavelengthNanometers: number;\n photonEnergyJoules: number;\n wavelengthConversion:\n | \"vacuum-identity\"\n | \"air-to-vacuum-via-phase-refractive-index\";\n phaseRefractiveIndexUsed?: number;\n airConditionCompatibility?:\n | \"exact-match\"\n | \"assumed-compatible\";\n airRefractiveIndexEvidence?:\n readonly EvidenceProvenance[];\n incidentPhotonRatePerSecond: number;\n effectiveExternalQuantumEfficiency: number;\n responseComposition:\n | \"direct-effective-response\"\n | \"channel-filter-transmittance-times-detector-eqe\";\n channelFilterTransmittance?: number;\n detectorExternalQuantumEfficiency?: number;\n responseInterpolationUsed: boolean;\n expectedGeneratedElectronRatePerSecond:\n number;\n}\n\nexport interface SensorEqeElectronRate {\n colorSamplingProfileId: string;\n responseProfileId: string;\n responseApplicationProfileId: string;\n operatingRangeProfileId: string;\n /** The exact source colour site. Results produced by the engine fill this in. */\n site?: {\n x: number;\n y: number;\n };\n channelId: string;\n sourceResponseKind:\n | \"effective-external-quantum-efficiency\"\n | \"separable-channel-filter-and-detector-eqe\";\n responseScope:\n SensorSpatioSpectralIrradianceReduction[\"responseScope\"];\n responseReferencePlane:\n SensorResponseApplicationCompatibilityAssessment[\"requiredSourcePlane\"];\n wavelengthBasis:\n SensorSpatioSpectralIrradianceReduction[\"wavelengthBasis\"];\n wavelengthRangeNanometers:\n SensorSpatioSpectralIrradianceReduction[\"wavelengthRangeNanometers\"];\n spectralNodeCount: number;\n perWavelength:\n readonly SensorEqeWavelengthRateContribution[];\n incidentPhotonRatePerSecond: number;\n expectedGeneratedElectronRatePerSecond:\n number;\n summationMethod:\n \"kahan-compensated\";\n responseApplicationPerformed: true;\n quantumEfficiencyApplied: true;\n spectralResponsivityApplied: false;\n channelFilterTransmissionApplied:\n boolean;\n photonRateCalculated: true;\n electronRateCalculated: true;\n photonCountCalculated: false;\n electronCountCalculated: false;\n currentCalculated: false;\n temporalIntegrationApplied: false;\n exposureDurationApplied: false;\n saturationAssessed: false;\n shotNoiseApplied: false;\n readNoiseApplied: false;\n adcQuantizationApplied: false;\n rawCodeValueProduced: false;\n responseUncertaintyPropagated: false;\n photonEnergyUncertaintyPropagated: false;\n quadratureConvergenceErrorEstimated:\n false;\n}\n\ninterface CompensatedSum {\n sum: number;\n correction: number;\n}\n\nfunction approximatelyEqual(\n actual: number,\n expected: 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 REDUCTION_TOLERANCE * scale\n );\n}\n\n/**\n * Identity string for parsed plain data that ignores key order, so records with\n * the same content compare equal whatever order their properties were added in.\n */\nfunction canonicalJson(\n value: unknown\n): string {\n if (value === undefined) return \"undefined\";\n return stringifyCanonicalJson(value, {\n undefinedObjectProperties: \"omit\",\n nonFiniteNumberMessage:\n \"EQE electron-rate identity data must contain finite numbers.\",\n unsupportedValueMessage:\n \"EQE electron-rate identity data must be plain JSON data.\"\n });\n}\n\nfunction requireEquivalent(\n actual: unknown,\n expected: unknown,\n message: string\n): void {\n if (\n canonicalJson(actual) !==\n canonicalJson(expected)\n ) {\n throw new InvalidScientificInputError(\n message\n );\n }\n}\n\nfunction validateAuthorizationChain(\n reduction:\n SensorSpatioSpectralIrradianceReduction,\n compatibility:\n SensorResponseApplicationCompatibilityAssessment,\n operatingRange:\n SensorResponseOperatingRangeAssessment\n): void {\n requireInputObject(reduction, \"reduction input\");\n if (\n reduction.outputMeaning !==\n \"pre-response-spatio-spectral-radiometric-reduction\" ||\n reduction\n .spectralResponseApplicationPerformed !==\n false ||\n reduction.quantumEfficiencyApplied !==\n false ||\n reduction.temporalIntegrationApplied !==\n false\n ) {\n throw new InvalidScientificInputError(\n \"reduction must remain pre-response and pre-temporal before EQE rate conversion.\"\n );\n }\n\n if (\n !compatibility\n .structuralCompatibilityEstablished ||\n compatibility.compatibilityStatus ===\n \"blocked\" ||\n compatibility.blockers.length !== 0 ||\n !compatibility.sourcePlaneMatched ||\n compatibility.requiredSignalPath !==\n \"photon-rate-to-electrons\"\n ) {\n throw new InvalidScientificInputError(\n \"compatibility must establish a non-blocked photon-rate-to-electrons response path.\"\n );\n }\n\n if (\n compatibility.spectralResponseProfileId !==\n reduction.responseProfileId ||\n compatibility.colorSamplingProfileId !==\n reduction.colorSamplingProfileId ||\n compatibility.channelId !==\n reduction.channelId ||\n compatibility.responseScope !==\n reduction.responseScope\n ) {\n throw new InvalidScientificInputError(\n \"compatibility identities must exactly match the reduction.\"\n );\n }\n\n if (\n !operatingRange\n .operatingRangeCompatibilityAssessed ||\n !operatingRange\n .instantaneousResponseRangeCompatible ||\n !operatingRange\n .responseRateConversionAuthorized ||\n operatingRange.status === \"blocked\" ||\n operatingRange.blockers.length !== 0 ||\n operatingRange.requiredSignalPath !==\n \"photon-rate-to-electrons\" ||\n operatingRange\n .responseApplicationPerformed !==\n false ||\n operatingRange\n .photonConversionPerformed !== false ||\n operatingRange\n .electronConversionPerformed !== false ||\n operatingRange\n .temporalIntegrationAuthorized !== false\n ) {\n throw new InvalidScientificInputError(\n \"operatingRange must authorize a clean photon-rate-to-electrons conversion path.\"\n );\n }\n\n if (\n !operatingRange\n .spectralNodeRangeCompatibilityAssessed ||\n operatingRange\n .spectralInputModel.kind !==\n \"per-spectral-bin\"\n ) {\n throw new InvalidScientificInputError(\n \"operatingRange must establish per-spectral-bin input-range applicability before wavelength-dependent EQE conversion.\"\n );\n }\n\n if (\n operatingRange\n .spatialLinearityModel.kind !==\n \"linear-superposition-over-geometric-aperture\"\n ) {\n throw new InvalidScientificInputError(\n \"operatingRange must establish linear superposition over the geometric aperture.\"\n );\n }\n\n if (\n operatingRange.spectralResponseProfileId !==\n reduction.responseProfileId ||\n operatingRange.colorSamplingProfileId !==\n reduction.colorSamplingProfileId ||\n operatingRange.channelId !==\n reduction.channelId ||\n operatingRange.responseApplicationProfileId !==\n compatibility.applicationProfileId\n ) {\n throw new InvalidScientificInputError(\n \"operatingRange identities must exactly match the reduction and compatibility assessment.\"\n );\n }\n}\n\nfunction validateReductionSpectralNodes(\n reduction:\n SensorSpatioSpectralIrradianceReduction\n): readonly SensorSpatioSpectralWavelengthReduction[] {\n if (\n !Array.isArray(\n reduction.perWavelength\n ) ||\n !Number.isSafeInteger(\n reduction.spectralNodeCount\n ) ||\n reduction.spectralNodeCount <= 0 ||\n reduction.perWavelength.length !==\n reduction.spectralNodeCount\n ) {\n throw new InvalidScientificInputError(\n \"reduction.perWavelength must contain exactly spectralNodeCount entries.\"\n );\n }\n\n let previousWavelength =\n Number.NEGATIVE_INFINITY;\n let normalizedWeightSum = 0;\n const integratedPower:\n CompensatedSum = {\n sum: 0,\n correction: 0\n };\n\n reduction.perWavelength.forEach(\n (entry, arrayIndex) => {\n const sampleIndex =\n requireSafeNonNegativeInteger(\n entry.spectralSampleIndex,\n \"reduction.perWavelength[\" +\n arrayIndex +\n \"].spectralSampleIndex\"\n );\n if (sampleIndex !== arrayIndex) {\n throw new InvalidScientificInputError(\n \"reduction spectralSampleIndex values must form the exact contiguous range 0..spectralNodeCount-1.\"\n );\n }\n\n const wavelength =\n requirePositiveFinite(\n entry.wavelengthNanometers,\n \"reduction.perWavelength[\" +\n arrayIndex +\n \"].wavelengthNanometers\"\n );\n if (\n wavelength <=\n previousWavelength\n ) {\n throw new InvalidScientificInputError(\n \"reduction per-wavelength samples must be strictly increasing in wavelength.\"\n );\n }\n previousWavelength =\n wavelength;\n\n const measure =\n requirePositiveFinite(\n entry.wavelengthMeasureNanometers,\n \"reduction.perWavelength[\" +\n arrayIndex +\n \"].wavelengthMeasureNanometers\"\n );\n const normalizedWeight =\n requirePositiveFinite(\n entry.normalizedWavelengthWeight,\n \"reduction.perWavelength[\" +\n arrayIndex +\n \"].normalizedWavelengthWeight\"\n );\n const fluxDensity =\n requireNonNegativeFinite(\n entry\n .geometricApertureIncidentSpectralFluxWattsPerNanometer,\n \"reduction.perWavelength[\" +\n arrayIndex +\n \"].geometricApertureIncidentSpectralFluxWattsPerNanometer\"\n );\n const contribution =\n requireNonNegativeFinite(\n entry\n .wavelengthIntegratedGeometricApertureIncidentFluxContributionWatts,\n \"reduction.perWavelength[\" +\n arrayIndex +\n \"].wavelengthIntegratedGeometricApertureIncidentFluxContributionWatts\"\n );\n const expectedContribution =\n fluxDensity * measure;\n if (\n !Number.isFinite(\n expectedContribution\n ) ||\n !approximatelyEqual(\n contribution,\n expectedContribution\n )\n ) {\n throw new InvalidScientificInputError(\n \"reduction per-wavelength radiant-power contribution must equal spectral flux density × d-lambda.\"\n );\n }\n\n normalizedWeightSum +=\n normalizedWeight;\n if (\n !Number.isFinite(\n normalizedWeightSum\n )\n ) {\n throw new InvalidScientificInputError(\n \"reduction normalized wavelength weights must remain finite.\"\n );\n }\n addCompensated(\n integratedPower,\n contribution\n );\n }\n );\n\n if (\n !approximatelyEqual(\n normalizedWeightSum,\n 1\n )\n ) {\n throw new InvalidScientificInputError(\n \"reduction normalized wavelength weights must sum to 1.\"\n );\n }\n\n const totalPower =\n requireNonNegativeFinite(\n reduction\n .wavelengthIntegratedGeometricApertureIncidentFluxWatts,\n \"reduction.wavelengthIntegratedGeometricApertureIncidentFluxWatts\"\n );\n if (\n !approximatelyEqual(\n integratedPower.sum,\n totalPower\n )\n ) {\n throw new InvalidScientificInputError(\n \"reduction total geometric-aperture radiant power must match the sum of per-wavelength contributions.\"\n );\n }\n\n return reduction.perWavelength;\n}\n\nfunction validateOperatingRangeNodeBinding(\n reduction:\n SensorSpatioSpectralIrradianceReduction,\n nodes:\n readonly SensorSpatioSpectralWavelengthReduction[],\n operatingRange:\n SensorResponseOperatingRangeAssessment\n): void {\n const evaluated =\n operatingRange\n .evaluatedSpectralNodeInputs;\n\n if (\n evaluated === undefined ||\n evaluated.length !== nodes.length\n ) {\n throw new InvalidScientificInputError(\n \"operatingRange must carry one evaluated spectral-node input for every reduction wavelength node.\"\n );\n }\n\n if (\n operatingRange\n .evaluatedWavelengthRangeNanometers\n .minimum !==\n reduction.wavelengthRangeNanometers\n .minimum ||\n operatingRange\n .evaluatedWavelengthRangeNanometers\n .maximum !==\n reduction.wavelengthRangeNanometers\n .maximum\n ) {\n throw new InvalidScientificInputError(\n \"operatingRange wavelength range must exactly match the reduction.\"\n );\n }\n\n evaluated.forEach(\n (entry, index) => {\n const node = nodes[index]!;\n if (\n entry.spectralSampleIndex !==\n node.spectralSampleIndex ||\n entry.wavelengthNanometers !==\n node.wavelengthNanometers ||\n entry.wavelengthMeasureNanometers !==\n node.wavelengthMeasureNanometers\n ) {\n throw new InvalidScientificInputError(\n \"operatingRange spectral-node identities must exactly match the reduction.\"\n );\n }\n\n const expected =\n operatingRange.inputRange.kind ===\n \"wavelength-integrated-geometric-aperture-radiant-power\"\n ? node\n .wavelengthIntegratedGeometricApertureIncidentFluxContributionWatts\n : node\n .wavelengthIntegratedSpatialAverageContributionWattsPerSquareMeter;\n\n if (\n entry.kind !==\n operatingRange.inputRange.kind ||\n entry.unit !==\n operatingRange.inputRange.unit ||\n !approximatelyEqual(\n entry.value,\n expected\n )\n ) {\n throw new InvalidScientificInputError(\n \"operatingRange spectral-node values must match the reduction values in the calibrated input domain.\"\n );\n }\n }\n );\n}\n\nfunction parseAirContext(\n reduction:\n SensorSpatioSpectralIrradianceReduction,\n nodes:\n readonly SensorSpatioSpectralWavelengthReduction[],\n context:\n SensorEqeAirPhotonEnergyContext |\n undefined\n): ReadonlyMap<\n number,\n SourcedAirPhaseRefractiveIndex\n> {\n if (\n reduction.wavelengthBasis ===\n \"vacuum\"\n ) {\n if (context !== undefined) {\n throw new InvalidScientificInputError(\n \"airPhotonEnergyContext must be omitted for vacuum-basis EQE conversion.\"\n );\n }\n return new Map();\n }\n\n if (\n reduction.wavelengthBasis !== \"air\"\n ) {\n throw new InvalidScientificInputError(\n \"EQE electron-rate conversion requires a resolved air or vacuum wavelength basis.\"\n );\n }\n\n if (\n context === undefined ||\n !Array.isArray(context.samples) ||\n context.samples.length !==\n nodes.length\n ) {\n throw new InvalidScientificInputError(\n \"Air-basis EQE conversion requires exactly one refractive-index sample for every spectral node.\"\n );\n }\n\n const byIndex =\n new Map<\n number,\n SourcedAirPhaseRefractiveIndex\n >();\n\n context.samples.forEach(\n (entry, arrayIndex) => {\n if (\n typeof entry !== \"object\" ||\n entry === null\n ) {\n throw new InvalidConfigurationError(\n \"airPhotonEnergyContext.samples[\" +\n arrayIndex +\n \"] must be an object.\"\n );\n }\n\n const spectralSampleIndex =\n requireSafeNonNegativeInteger(\n entry.spectralSampleIndex,\n \"airPhotonEnergyContext.samples[\" +\n arrayIndex +\n \"].spectralSampleIndex\"\n );\n if (\n spectralSampleIndex >=\n nodes.length ||\n byIndex.has(\n spectralSampleIndex\n )\n ) {\n throw new InvalidScientificInputError(\n \"airPhotonEnergyContext must contain unique in-range spectral sample identities.\"\n );\n }\n byIndex.set(\n spectralSampleIndex,\n entry.refractiveIndex\n );\n }\n );\n\n return byIndex;\n}\n\nfunction responseEvidenceMatches(\n actual:\n readonly EvidenceProvenance[],\n expected:\n readonly EvidenceProvenance[]\n): boolean {\n return (\n canonicalJson(actual) ===\n canonicalJson(expected)\n );\n}\n\nfunction validateResponseBinding(\n resolved:\n ReturnType<\n typeof resolveParsedSensorSpectralResponseAtWavelength\n >[\"value\"],\n reduction:\n SensorSpatioSpectralIrradianceReduction\n): void {\n if (\n resolved.profileId !==\n reduction.responseProfileId ||\n resolved.colorSamplingProfileId !==\n reduction.colorSamplingProfileId ||\n resolved.channelId !==\n reduction.channelId ||\n resolved.sourceResponseKind !==\n reduction.sourceResponseKind ||\n resolved.responseScope !==\n reduction.responseScope ||\n resolved.scientificStatus !==\n reduction.responseScientificStatus\n ) {\n throw new InvalidScientificInputError(\n \"Resolved spectral response metadata must exactly match the response used to construct the reduction.\"\n );\n }\n\n requireEquivalent(\n resolved.uncertainty,\n reduction.responseUncertainty,\n \"Resolved spectral response uncertainty must match the reduction.\"\n );\n\n const expectedEvidence =\n reduction.componentEvidence.spectral;\n if (\n !responseEvidenceMatches(\n resolved.componentEvidence.profile,\n expectedEvidence.profile\n ) ||\n !responseEvidenceMatches(\n resolved.componentEvidence.channel,\n expectedEvidence.channel\n ) ||\n canonicalJson(\n resolved.componentEvidence.curves\n ) !==\n canonicalJson(\n expectedEvidence.curves\n )\n ) {\n throw new InvalidScientificInputError(\n \"Resolved spectral response evidence must match the calibration evidence carried by the reduction.\"\n );\n }\n}\n\nfunction validateExactResponseDataBinding(\n responseProfile:\n SensorSpectralResponseProfile,\n reduction:\n SensorSpatioSpectralIrradianceReduction\n): void {\n const response =\n responseProfile.channels.find(\n (entry) =>\n entry.channelId ===\n reduction.channelId\n );\n if (reduction.responseChannelBinding !== undefined) {\n requireCurrentSensorSpectralResponseChannelBinding(\n reduction.responseChannelBinding\n );\n }\n if (\n response === undefined ||\n reduction.responseChannelBinding ===\n undefined ||\n canonicalJson(\n createSensorSpectralResponseChannelBinding(\n response\n )\n ) !==\n canonicalJson(\n reduction.responseChannelBinding\n )\n ) {\n throw new InvalidScientificInputError(\n \"Supplied spectral response channel data must exactly match the response-channel binding carried by the reduction.\"\n );\n }\n}\n\nfunction requireEqeResponse(\n response:\n ResolvedSensorSpectralResponseValue\n): Extract<\n ResolvedSensorSpectralResponseValue,\n {\n kind:\n \"effective-external-quantum-efficiency\";\n }\n> {\n if (\n response.kind !==\n \"effective-external-quantum-efficiency\"\n ) {\n throw new InvalidScientificInputError(\n \"EQE electron-rate conversion cannot consume A/W spectral responsivity.\"\n );\n }\n return response;\n}\n\n/**\n * Converts the spectral radiant power collected by the site's geometric aperture\n * (before the sensor response) into an incident photon rate and an expected\n * generated electron rate.\n *\n * The response is applied at each wavelength node. One broadband-average QE is\n * never applied to the total power.\n *\n * Integration over exposure time happens later, so all outputs are rates per\n * second, not photon or electron counts.\n */\nexport function calculateSensorEqeElectronRate(\n input:\n CalculateSensorEqeElectronRateInput\n): CalculationResult<SensorEqeElectronRate> {\n requireInputKeys(input, \"calculateSensorEqeElectronRate\", [\"reduction\", \"compatibility\", \"operatingRange\", \"colorSamplingProfile\", \"spectralResponseProfile\", \"airPhotonEnergyContext\"]);\n validateAuthorizationChain(\n input.reduction,\n input.compatibility,\n input.operatingRange\n );\n\n if (\n input.reduction\n .sourceResponseKind ===\n \"effective-spectral-responsivity\"\n ) {\n throw new InvalidScientificInputError(\n \"EQE electron-rate conversion requires an EQE or filter×detector-EQE response, not A/W responsivity.\"\n );\n }\n\n const colorProfile =\n parseSensorColorSamplingProfile(\n input.colorSamplingProfile\n );\n const responseProfile =\n parseSensorSpectralResponseProfile(\n input.spectralResponseProfile\n );\n\n if (\n colorProfile.profileId !==\n input.reduction\n .colorSamplingProfileId ||\n responseProfile.profileId !==\n input.reduction\n .responseProfileId ||\n responseProfile\n .colorSamplingProfileId !==\n colorProfile.profileId\n ) {\n throw new InvalidScientificInputError(\n \"Supplied color/spectral response profiles must exactly match the reduction identities.\"\n );\n }\n\n validateExactResponseDataBinding(\n responseProfile,\n input.reduction\n );\n\n const nodes =\n validateReductionSpectralNodes(\n input.reduction\n );\n validateOperatingRangeNodeBinding(\n input.reduction,\n nodes,\n input.operatingRange\n );\n\n const airIndices =\n parseAirContext(\n input.reduction,\n nodes,\n input.airPhotonEnergyContext\n );\n\n const photonRateSum:\n CompensatedSum = {\n sum: 0,\n correction: 0\n };\n const electronRateSum:\n CompensatedSum = {\n sum: 0,\n correction: 0\n };\n const perWavelength:\n SensorEqeWavelengthRateContribution[] =\n [];\n\n let responseBindingValidated =\n false;\n let channelFilterTransmissionApplied =\n false;\n\n for (const node of nodes) {\n const resolved =\n resolveParsedSensorSpectralResponseAtWavelength({\n colorSamplingProfile:\n colorProfile,\n spectralResponseProfile:\n responseProfile,\n channelId:\n input.reduction.channelId,\n wavelengthNanometers:\n node.wavelengthNanometers,\n wavelengthBasis:\n input.reduction\n .wavelengthBasis\n });\n\n if (!responseBindingValidated) {\n validateResponseBinding(\n resolved.value,\n input.reduction\n );\n responseBindingValidated =\n true;\n }\n\n const eqe =\n requireEqeResponse(\n resolved.value.response\n );\n\n const airIndex =\n airIndices.get(\n node.spectralSampleIndex\n );\n const photonEnergy =\n calculatePhotonEnergyFromWavelength({\n wavelengthNanometers:\n node.wavelengthNanometers,\n wavelengthBasis:\n input.reduction\n .wavelengthBasis,\n ...(input.reduction\n .wavelengthBasis === \"air\"\n ? {\n airPhaseRefractiveIndex:\n airIndex!,\n ...(input.airPhotonEnergyContext!\n .operatingConditions ===\n undefined\n ? {}\n : {\n airOperatingConditions:\n input\n .airPhotonEnergyContext!\n .operatingConditions\n }),\n airConditionPolicy:\n input.airPhotonEnergyContext!\n .conditionPolicy\n }\n : {})\n }).value;\n\n const radiantPowerContributionWatts =\n node\n .geometricApertureIncidentSpectralFluxWattsPerNanometer *\n node.wavelengthMeasureNanometers;\n const incidentPhotonRatePerSecond =\n radiantPowerContributionWatts /\n photonEnergy.photonEnergyJoules;\n const expectedGeneratedElectronRatePerSecond =\n incidentPhotonRatePerSecond *\n eqe.externalQuantumEfficiency;\n\n if (\n !Number.isFinite(\n radiantPowerContributionWatts\n ) ||\n !Number.isFinite(\n incidentPhotonRatePerSecond\n ) ||\n !Number.isFinite(\n expectedGeneratedElectronRatePerSecond\n )\n ) {\n throw new InvalidScientificInputError(\n \"EQE wavelength-node rate calculation must remain finite.\"\n );\n }\n\n addCompensated(\n photonRateSum,\n incidentPhotonRatePerSecond\n );\n addCompensated(\n electronRateSum,\n expectedGeneratedElectronRatePerSecond\n );\n\n if (\n eqe.composition ===\n \"channel-filter-transmittance-times-detector-eqe\"\n ) {\n channelFilterTransmissionApplied =\n true;\n }\n\n perWavelength.push({\n spectralSampleIndex:\n node.spectralSampleIndex,\n wavelengthNanometers:\n node.wavelengthNanometers,\n wavelengthMeasureNanometers:\n node.wavelengthMeasureNanometers,\n geometricApertureIncidentSpectralFluxWattsPerNanometer:\n node\n .geometricApertureIncidentSpectralFluxWattsPerNanometer,\n radiantPowerContributionWatts,\n vacuumWavelengthNanometers:\n photonEnergy\n .vacuumWavelengthNanometers,\n photonEnergyJoules:\n photonEnergy\n .photonEnergyJoules,\n wavelengthConversion:\n photonEnergy\n .wavelengthConversion,\n ...(photonEnergy\n .phaseRefractiveIndexUsed ===\n undefined\n ? {}\n : {\n phaseRefractiveIndexUsed:\n photonEnergy\n .phaseRefractiveIndexUsed\n }),\n ...(photonEnergy\n .airConditionCompatibility ===\n undefined\n ? {}\n : {\n airConditionCompatibility:\n photonEnergy\n .airConditionCompatibility\n }),\n ...(photonEnergy\n .airRefractiveIndexEvidence ===\n undefined\n ? {}\n : {\n airRefractiveIndexEvidence:\n photonEnergy\n .airRefractiveIndexEvidence\n }),\n incidentPhotonRatePerSecond,\n effectiveExternalQuantumEfficiency:\n eqe.externalQuantumEfficiency,\n responseComposition:\n eqe.composition,\n ...(eqe.channelFilterTransmittance ===\n undefined\n ? {}\n : {\n channelFilterTransmittance:\n eqe.channelFilterTransmittance\n }),\n ...(eqe\n .detectorExternalQuantumEfficiency ===\n undefined\n ? {}\n : {\n detectorExternalQuantumEfficiency:\n eqe\n .detectorExternalQuantumEfficiency\n }),\n responseInterpolationUsed:\n resolved.value\n .interpolationUsed,\n expectedGeneratedElectronRatePerSecond\n });\n }\n\n if (\n !Number.isFinite(\n photonRateSum.sum\n ) ||\n !Number.isFinite(\n electronRateSum.sum\n )\n ) {\n throw new InvalidScientificInputError(\n \"Total EQE photon/electron rates must remain finite.\"\n );\n }\n\n return approximationResult(\n {\n colorSamplingProfileId:\n input.reduction\n .colorSamplingProfileId,\n responseProfileId:\n input.reduction\n .responseProfileId,\n responseApplicationProfileId:\n input.compatibility\n .applicationProfileId,\n operatingRangeProfileId:\n input.operatingRange\n .operatingRangeProfileId,\n site: {\n ...input.reduction.site\n },\n channelId:\n input.reduction.channelId,\n sourceResponseKind:\n input.reduction\n .sourceResponseKind,\n responseScope:\n input.reduction.responseScope,\n responseReferencePlane:\n input.compatibility\n .requiredSourcePlane,\n wavelengthBasis:\n input.reduction\n .wavelengthBasis,\n wavelengthRangeNanometers: {\n ...input.reduction\n .wavelengthRangeNanometers\n },\n spectralNodeCount:\n nodes.length,\n perWavelength,\n incidentPhotonRatePerSecond:\n photonRateSum.sum,\n expectedGeneratedElectronRatePerSecond:\n electronRateSum.sum,\n summationMethod:\n \"kahan-compensated\",\n responseApplicationPerformed:\n true,\n quantumEfficiencyApplied:\n true,\n spectralResponsivityApplied:\n false,\n channelFilterTransmissionApplied,\n photonRateCalculated: true,\n electronRateCalculated: true,\n photonCountCalculated: false,\n electronCountCalculated: false,\n currentCalculated: false,\n temporalIntegrationApplied:\n false,\n exposureDurationApplied: false,\n saturationAssessed: false,\n shotNoiseApplied: false,\n readNoiseApplied: false,\n adcQuantizationApplied:\n false,\n rawCodeValueProduced: false,\n responseUncertaintyPropagated:\n false,\n photonEnergyUncertaintyPropagated:\n false,\n quadratureConvergenceErrorEstimated:\n false\n },\n \"sensor-eqe-electron-rate\",\n \"1.0.0\",\n [\n \"Response is evaluated independently at every wavelength quadrature node; broadband radiant power is never multiplied by an average QE.\",\n \"Each node converts radiant power to photon rate using photon energy hν, then applies effective external QE to obtain expected generated-electron rate.\",\n \"Direct effective EQE is preserved as direct response; explicitly separable channel-filter transmittance × detector EQE is multiplied only through the existing spectral-response resolver.\",\n \"The response calibration metadata/evidence must match the spectral evidence carried by the pre-response reduction; matching profile IDs alone is insufficient.\",\n \"The structural compatibility and operating-range assessments must both be non-blocked and authorize the photon-rate→electron path.\",\n \"Operating-range authorization must be per spectral bin, with spectral-node identities and calibrated-domain values bound back to the exact reduction used for conversion.\",\n \"Operating-range authorization must establish linear superposition over the geometric aperture so spatial reduction may precede response application without hiding sub-aperture nonlinear behavior.\",\n \"Air-basis spectral nodes require one exact-wavelength sourced phase refractive index per spectral sample plus the photon-energy atmosphere compatibility policy.\",\n \"Kahan compensated summation is used across wavelength nodes to reduce loss of small contributions when rates span large dynamic ranges.\",\n \"Outputs are instantaneous expected rates only. Exposure duration, photon/electron counts, full-well saturation, shot/read noise, ADC/RAW conversion and reconstruction remain downstream.\",\n \"Response/refractive-index uncertainties and quadrature convergence error are not propagated into a combined output uncertainty by this version.\"\n ]\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"eqe-electron-rate.js","sourceRoot":"","sources":["../../src/sensor/eqe-electron-rate.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * EQE electron rate: converts the light power reaching a sensor site into a photon rate and an\n * expected electron rate, using the sensor's EQE (external quantum efficiency, the fraction of\n * incident photons that produce a collected electron).\n *\n * The response is applied at each wavelength node. One broadband-average QE is never applied to the\n * total power. Outputs are rates per second; integrating over the exposure time happens later.\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 type {\n EvidenceProvenance\n} from \"../core/evidence-provenance.js\";\nimport { stringifyCanonicalJson } from \"../core/canonical-json.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport {\n calculatePhotonEnergyFromWavelength,\n type AirRefractiveIndexConditionPolicy,\n type AirRefractiveIndexReferenceConditions,\n type SourcedAirPhaseRefractiveIndex\n} from \"./photon-energy.js\";\nimport type {\n SensorResponseApplicationCompatibilityAssessment\n} from \"./response-application-compatibility.js\";\nimport type {\n SensorResponseOperatingRangeAssessment\n} from \"./response-operating-range.js\";\nimport {\n parseSensorColorSamplingProfile,\n type SensorColorSamplingProfile\n} from \"./color-sampling.js\";\nimport {\n createSensorSpectralResponseChannelBinding,\n requireCurrentSensorSpectralResponseChannelBinding,\n parseSensorSpectralResponseProfile,\n resolveParsedSensorSpectralResponseAtWavelength,\n type ResolvedSensorSpectralResponseValue,\n type SensorSpectralResponseProfile\n} from \"./spectral-response.js\";\nimport type {\n SensorSpatioSpectralIrradianceReduction,\n SensorSpatioSpectralWavelengthReduction\n} from \"./spatio-spectral-reduction.js\";\nimport { requireInputObject, requireInputKeys, requireInputNonNegativeFinite as requireNonNegativeFinite, requireInputPositiveFinite as requirePositiveFinite, requireInputNonNegativeSafeInteger as requireSafeNonNegativeInteger } from \"../core/record-validation.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { addCompensated, approximatelyEqual } from \"../core/shared-helpers.js\";\n\n\nexport interface SensorEqeAirRefractiveIndexSample {\n spectralSampleIndex: number;\n refractiveIndex:\n SourcedAirPhaseRefractiveIndex;\n}\n\nexport interface SensorEqeAirPhotonEnergyContext {\n samples:\n readonly SensorEqeAirRefractiveIndexSample[];\n operatingConditions?:\n AirRefractiveIndexReferenceConditions;\n conditionPolicy:\n AirRefractiveIndexConditionPolicy;\n}\n\nexport interface CalculateSensorEqeElectronRateInput {\n reduction:\n SensorSpatioSpectralIrradianceReduction;\n compatibility:\n SensorResponseApplicationCompatibilityAssessment;\n operatingRange:\n SensorResponseOperatingRangeAssessment;\n colorSamplingProfile:\n SensorColorSamplingProfile;\n spectralResponseProfile:\n SensorSpectralResponseProfile;\n airPhotonEnergyContext?:\n SensorEqeAirPhotonEnergyContext;\n}\n\nexport interface SensorEqeWavelengthRateContribution {\n spectralSampleIndex: number;\n wavelengthNanometers: number;\n wavelengthMeasureNanometers: number;\n geometricApertureIncidentSpectralFluxWattsPerNanometer:\n number;\n radiantPowerContributionWatts: number;\n vacuumWavelengthNanometers: number;\n photonEnergyJoules: number;\n wavelengthConversion:\n | \"vacuum-identity\"\n | \"air-to-vacuum-via-phase-refractive-index\";\n phaseRefractiveIndexUsed?: number;\n airConditionCompatibility?:\n | \"exact-match\"\n | \"assumed-compatible\";\n airRefractiveIndexEvidence?:\n readonly EvidenceProvenance[];\n incidentPhotonRatePerSecond: number;\n effectiveExternalQuantumEfficiency: number;\n responseComposition:\n | \"direct-effective-response\"\n | \"channel-filter-transmittance-times-detector-eqe\";\n channelFilterTransmittance?: number;\n detectorExternalQuantumEfficiency?: number;\n responseInterpolationUsed: boolean;\n expectedGeneratedElectronRatePerSecond:\n number;\n}\n\nexport interface SensorEqeElectronRate {\n colorSamplingProfileId: string;\n responseProfileId: string;\n responseApplicationProfileId: string;\n operatingRangeProfileId: string;\n /** The exact source colour site. Results produced by the engine fill this in. */\n site?: {\n x: number;\n y: number;\n };\n channelId: string;\n sourceResponseKind:\n | \"effective-external-quantum-efficiency\"\n | \"separable-channel-filter-and-detector-eqe\";\n responseScope:\n SensorSpatioSpectralIrradianceReduction[\"responseScope\"];\n responseReferencePlane:\n SensorResponseApplicationCompatibilityAssessment[\"requiredSourcePlane\"];\n wavelengthBasis:\n SensorSpatioSpectralIrradianceReduction[\"wavelengthBasis\"];\n wavelengthRangeNanometers:\n SensorSpatioSpectralIrradianceReduction[\"wavelengthRangeNanometers\"];\n spectralNodeCount: number;\n perWavelength:\n readonly SensorEqeWavelengthRateContribution[];\n incidentPhotonRatePerSecond: number;\n expectedGeneratedElectronRatePerSecond:\n number;\n summationMethod:\n \"kahan-compensated\";\n responseApplicationPerformed: true;\n quantumEfficiencyApplied: true;\n spectralResponsivityApplied: false;\n channelFilterTransmissionApplied:\n boolean;\n photonRateCalculated: true;\n electronRateCalculated: true;\n photonCountCalculated: false;\n electronCountCalculated: false;\n currentCalculated: false;\n temporalIntegrationApplied: false;\n exposureDurationApplied: false;\n saturationAssessed: false;\n shotNoiseApplied: false;\n readNoiseApplied: false;\n adcQuantizationApplied: false;\n rawCodeValueProduced: false;\n responseUncertaintyPropagated: false;\n photonEnergyUncertaintyPropagated: false;\n quadratureConvergenceErrorEstimated:\n false;\n}\n\ninterface CompensatedSum {\n sum: number;\n correction: number;\n}\n\n\n/**\n * Identity string for parsed plain data that ignores key order, so records with\n * the same content compare equal whatever order their properties were added in.\n */\nfunction canonicalJson(\n value: unknown\n): string {\n if (value === undefined) return \"undefined\";\n return stringifyCanonicalJson(value, {\n undefinedObjectProperties: \"omit\",\n nonFiniteNumberMessage:\n \"EQE electron-rate identity data must contain finite numbers.\",\n unsupportedValueMessage:\n \"EQE electron-rate identity data must be plain JSON data.\"\n });\n}\n\nfunction requireEquivalent(\n actual: unknown,\n expected: unknown,\n message: string\n): void {\n if (\n canonicalJson(actual) !==\n canonicalJson(expected)\n ) {\n throw new InvalidScientificInputError(\n message\n );\n }\n}\n\nfunction validateAuthorizationChain(\n reduction:\n SensorSpatioSpectralIrradianceReduction,\n compatibility:\n SensorResponseApplicationCompatibilityAssessment,\n operatingRange:\n SensorResponseOperatingRangeAssessment\n): void {\n requireInputObject(reduction, \"reduction input\");\n if (\n reduction.outputMeaning !==\n \"pre-response-spatio-spectral-radiometric-reduction\" ||\n reduction\n .spectralResponseApplicationPerformed !==\n false ||\n reduction.quantumEfficiencyApplied !==\n false ||\n reduction.temporalIntegrationApplied !==\n false\n ) {\n throw new InvalidScientificInputError(\n \"reduction must remain pre-response and pre-temporal before EQE rate conversion.\"\n );\n }\n\n if (\n !compatibility\n .structuralCompatibilityEstablished ||\n compatibility.compatibilityStatus ===\n \"blocked\" ||\n compatibility.blockers.length !== 0 ||\n !compatibility.sourcePlaneMatched ||\n compatibility.requiredSignalPath !==\n \"photon-rate-to-electrons\"\n ) {\n throw new InvalidScientificInputError(\n \"compatibility must establish a non-blocked photon-rate-to-electrons response path.\"\n );\n }\n\n if (\n compatibility.spectralResponseProfileId !==\n reduction.responseProfileId ||\n compatibility.colorSamplingProfileId !==\n reduction.colorSamplingProfileId ||\n compatibility.channelId !==\n reduction.channelId ||\n compatibility.responseScope !==\n reduction.responseScope\n ) {\n throw new InvalidScientificInputError(\n \"compatibility identities must exactly match the reduction.\"\n );\n }\n\n if (\n !operatingRange\n .operatingRangeCompatibilityAssessed ||\n !operatingRange\n .instantaneousResponseRangeCompatible ||\n !operatingRange\n .responseRateConversionAuthorized ||\n operatingRange.status === \"blocked\" ||\n operatingRange.blockers.length !== 0 ||\n operatingRange.requiredSignalPath !==\n \"photon-rate-to-electrons\" ||\n operatingRange\n .responseApplicationPerformed !==\n false ||\n operatingRange\n .photonConversionPerformed !== false ||\n operatingRange\n .electronConversionPerformed !== false ||\n operatingRange\n .temporalIntegrationAuthorized !== false\n ) {\n throw new InvalidScientificInputError(\n \"operatingRange must authorize a clean photon-rate-to-electrons conversion path.\"\n );\n }\n\n if (\n !operatingRange\n .spectralNodeRangeCompatibilityAssessed ||\n operatingRange\n .spectralInputModel.kind !==\n \"per-spectral-bin\"\n ) {\n throw new InvalidScientificInputError(\n \"operatingRange must establish per-spectral-bin input-range applicability before wavelength-dependent EQE conversion.\"\n );\n }\n\n if (\n operatingRange\n .spatialLinearityModel.kind !==\n \"linear-superposition-over-geometric-aperture\"\n ) {\n throw new InvalidScientificInputError(\n \"operatingRange must establish linear superposition over the geometric aperture.\"\n );\n }\n\n if (\n operatingRange.spectralResponseProfileId !==\n reduction.responseProfileId ||\n operatingRange.colorSamplingProfileId !==\n reduction.colorSamplingProfileId ||\n operatingRange.channelId !==\n reduction.channelId ||\n operatingRange.responseApplicationProfileId !==\n compatibility.applicationProfileId\n ) {\n throw new InvalidScientificInputError(\n \"operatingRange identities must exactly match the reduction and compatibility assessment.\"\n );\n }\n}\n\nfunction validateReductionSpectralNodes(\n reduction:\n SensorSpatioSpectralIrradianceReduction\n): readonly SensorSpatioSpectralWavelengthReduction[] {\n if (\n !Array.isArray(\n reduction.perWavelength\n ) ||\n !Number.isSafeInteger(\n reduction.spectralNodeCount\n ) ||\n reduction.spectralNodeCount <= 0 ||\n reduction.perWavelength.length !==\n reduction.spectralNodeCount\n ) {\n throw new InvalidScientificInputError(\n \"reduction.perWavelength must contain exactly spectralNodeCount entries.\"\n );\n }\n\n let previousWavelength =\n Number.NEGATIVE_INFINITY;\n let normalizedWeightSum = 0;\n const integratedPower:\n CompensatedSum = {\n sum: 0,\n correction: 0\n };\n\n reduction.perWavelength.forEach(\n (entry, arrayIndex) => {\n const sampleIndex =\n requireSafeNonNegativeInteger(\n entry.spectralSampleIndex,\n \"reduction.perWavelength[\" +\n arrayIndex +\n \"].spectralSampleIndex\"\n );\n if (sampleIndex !== arrayIndex) {\n throw new InvalidScientificInputError(\n \"reduction spectralSampleIndex values must form the exact contiguous range 0..spectralNodeCount-1.\"\n );\n }\n\n const wavelength =\n requirePositiveFinite(\n entry.wavelengthNanometers,\n \"reduction.perWavelength[\" +\n arrayIndex +\n \"].wavelengthNanometers\"\n );\n if (\n wavelength <=\n previousWavelength\n ) {\n throw new InvalidScientificInputError(\n \"reduction per-wavelength samples must be strictly increasing in wavelength.\"\n );\n }\n previousWavelength =\n wavelength;\n\n const measure =\n requirePositiveFinite(\n entry.wavelengthMeasureNanometers,\n \"reduction.perWavelength[\" +\n arrayIndex +\n \"].wavelengthMeasureNanometers\"\n );\n const normalizedWeight =\n requirePositiveFinite(\n entry.normalizedWavelengthWeight,\n \"reduction.perWavelength[\" +\n arrayIndex +\n \"].normalizedWavelengthWeight\"\n );\n const fluxDensity =\n requireNonNegativeFinite(\n entry\n .geometricApertureIncidentSpectralFluxWattsPerNanometer,\n \"reduction.perWavelength[\" +\n arrayIndex +\n \"].geometricApertureIncidentSpectralFluxWattsPerNanometer\"\n );\n const contribution =\n requireNonNegativeFinite(\n entry\n .wavelengthIntegratedGeometricApertureIncidentFluxContributionWatts,\n \"reduction.perWavelength[\" +\n arrayIndex +\n \"].wavelengthIntegratedGeometricApertureIncidentFluxContributionWatts\"\n );\n const expectedContribution =\n fluxDensity * measure;\n if (\n !Number.isFinite(\n expectedContribution\n ) ||\n !approximatelyEqual(\n contribution,\n expectedContribution\n )\n ) {\n throw new InvalidScientificInputError(\n \"reduction per-wavelength radiant-power contribution must equal spectral flux density × d-lambda.\"\n );\n }\n\n normalizedWeightSum +=\n normalizedWeight;\n if (\n !Number.isFinite(\n normalizedWeightSum\n )\n ) {\n throw new InvalidScientificInputError(\n \"reduction normalized wavelength weights must remain finite.\"\n );\n }\n addCompensated(\n integratedPower,\n contribution\n );\n }\n );\n\n if (\n !approximatelyEqual(\n normalizedWeightSum,\n 1\n )\n ) {\n throw new InvalidScientificInputError(\n \"reduction normalized wavelength weights must sum to 1.\"\n );\n }\n\n const totalPower =\n requireNonNegativeFinite(\n reduction\n .wavelengthIntegratedGeometricApertureIncidentFluxWatts,\n \"reduction.wavelengthIntegratedGeometricApertureIncidentFluxWatts\"\n );\n if (\n !approximatelyEqual(\n integratedPower.sum,\n totalPower\n )\n ) {\n throw new InvalidScientificInputError(\n \"reduction total geometric-aperture radiant power must match the sum of per-wavelength contributions.\"\n );\n }\n\n return reduction.perWavelength;\n}\n\nfunction validateOperatingRangeNodeBinding(\n reduction:\n SensorSpatioSpectralIrradianceReduction,\n nodes:\n readonly SensorSpatioSpectralWavelengthReduction[],\n operatingRange:\n SensorResponseOperatingRangeAssessment\n): void {\n const evaluated =\n operatingRange\n .evaluatedSpectralNodeInputs;\n\n if (\n evaluated === undefined ||\n evaluated.length !== nodes.length\n ) {\n throw new InvalidScientificInputError(\n \"operatingRange must carry one evaluated spectral-node input for every reduction wavelength node.\"\n );\n }\n\n if (\n operatingRange\n .evaluatedWavelengthRangeNanometers\n .minimum !==\n reduction.wavelengthRangeNanometers\n .minimum ||\n operatingRange\n .evaluatedWavelengthRangeNanometers\n .maximum !==\n reduction.wavelengthRangeNanometers\n .maximum\n ) {\n throw new InvalidScientificInputError(\n \"operatingRange wavelength range must exactly match the reduction.\"\n );\n }\n\n evaluated.forEach(\n (entry, index) => {\n const node = nodes[index]!;\n if (\n entry.spectralSampleIndex !==\n node.spectralSampleIndex ||\n entry.wavelengthNanometers !==\n node.wavelengthNanometers ||\n entry.wavelengthMeasureNanometers !==\n node.wavelengthMeasureNanometers\n ) {\n throw new InvalidScientificInputError(\n \"operatingRange spectral-node identities must exactly match the reduction.\"\n );\n }\n\n const expected =\n operatingRange.inputRange.kind ===\n \"wavelength-integrated-geometric-aperture-radiant-power\"\n ? node\n .wavelengthIntegratedGeometricApertureIncidentFluxContributionWatts\n : node\n .wavelengthIntegratedSpatialAverageContributionWattsPerSquareMeter;\n\n if (\n entry.kind !==\n operatingRange.inputRange.kind ||\n entry.unit !==\n operatingRange.inputRange.unit ||\n !approximatelyEqual(\n entry.value,\n expected\n )\n ) {\n throw new InvalidScientificInputError(\n \"operatingRange spectral-node values must match the reduction values in the calibrated input domain.\"\n );\n }\n }\n );\n}\n\nfunction parseAirContext(\n reduction:\n SensorSpatioSpectralIrradianceReduction,\n nodes:\n readonly SensorSpatioSpectralWavelengthReduction[],\n context:\n SensorEqeAirPhotonEnergyContext |\n undefined\n): ReadonlyMap<\n number,\n SourcedAirPhaseRefractiveIndex\n> {\n if (\n reduction.wavelengthBasis ===\n \"vacuum\"\n ) {\n if (context !== undefined) {\n throw new InvalidScientificInputError(\n \"airPhotonEnergyContext must be omitted for vacuum-basis EQE conversion.\"\n );\n }\n return new Map();\n }\n\n if (\n reduction.wavelengthBasis !== \"air\"\n ) {\n throw new InvalidScientificInputError(\n \"EQE electron-rate conversion requires a resolved air or vacuum wavelength basis.\"\n );\n }\n\n if (\n context === undefined ||\n !Array.isArray(context.samples) ||\n context.samples.length !==\n nodes.length\n ) {\n throw new InvalidScientificInputError(\n \"Air-basis EQE conversion requires exactly one refractive-index sample for every spectral node.\"\n );\n }\n\n const byIndex =\n new Map<\n number,\n SourcedAirPhaseRefractiveIndex\n >();\n\n context.samples.forEach(\n (entry, arrayIndex) => {\n if (\n typeof entry !== \"object\" ||\n entry === null\n ) {\n throw new InvalidConfigurationError(\n \"airPhotonEnergyContext.samples[\" +\n arrayIndex +\n \"] must be an object.\"\n );\n }\n\n const spectralSampleIndex =\n requireSafeNonNegativeInteger(\n entry.spectralSampleIndex,\n \"airPhotonEnergyContext.samples[\" +\n arrayIndex +\n \"].spectralSampleIndex\"\n );\n if (\n spectralSampleIndex >=\n nodes.length ||\n byIndex.has(\n spectralSampleIndex\n )\n ) {\n throw new InvalidScientificInputError(\n \"airPhotonEnergyContext must contain unique in-range spectral sample identities.\"\n );\n }\n byIndex.set(\n spectralSampleIndex,\n entry.refractiveIndex\n );\n }\n );\n\n return byIndex;\n}\n\nfunction responseEvidenceMatches(\n actual:\n readonly EvidenceProvenance[],\n expected:\n readonly EvidenceProvenance[]\n): boolean {\n return (\n canonicalJson(actual) ===\n canonicalJson(expected)\n );\n}\n\nfunction validateResponseBinding(\n resolved:\n ReturnType<\n typeof resolveParsedSensorSpectralResponseAtWavelength\n >[\"value\"],\n reduction:\n SensorSpatioSpectralIrradianceReduction\n): void {\n if (\n resolved.profileId !==\n reduction.responseProfileId ||\n resolved.colorSamplingProfileId !==\n reduction.colorSamplingProfileId ||\n resolved.channelId !==\n reduction.channelId ||\n resolved.sourceResponseKind !==\n reduction.sourceResponseKind ||\n resolved.responseScope !==\n reduction.responseScope ||\n resolved.scientificStatus !==\n reduction.responseScientificStatus\n ) {\n throw new InvalidScientificInputError(\n \"Resolved spectral response metadata must exactly match the response used to construct the reduction.\"\n );\n }\n\n requireEquivalent(\n resolved.uncertainty,\n reduction.responseUncertainty,\n \"Resolved spectral response uncertainty must match the reduction.\"\n );\n\n const expectedEvidence =\n reduction.componentEvidence.spectral;\n if (\n !responseEvidenceMatches(\n resolved.componentEvidence.profile,\n expectedEvidence.profile\n ) ||\n !responseEvidenceMatches(\n resolved.componentEvidence.channel,\n expectedEvidence.channel\n ) ||\n canonicalJson(\n resolved.componentEvidence.curves\n ) !==\n canonicalJson(\n expectedEvidence.curves\n )\n ) {\n throw new InvalidScientificInputError(\n \"Resolved spectral response evidence must match the calibration evidence carried by the reduction.\"\n );\n }\n}\n\nfunction validateExactResponseDataBinding(\n responseProfile:\n SensorSpectralResponseProfile,\n reduction:\n SensorSpatioSpectralIrradianceReduction\n): void {\n const response =\n responseProfile.channels.find(\n (entry) =>\n entry.channelId ===\n reduction.channelId\n );\n if (reduction.responseChannelBinding !== undefined) {\n requireCurrentSensorSpectralResponseChannelBinding(\n reduction.responseChannelBinding\n );\n }\n if (\n response === undefined ||\n reduction.responseChannelBinding ===\n undefined ||\n canonicalJson(\n createSensorSpectralResponseChannelBinding(\n response\n )\n ) !==\n canonicalJson(\n reduction.responseChannelBinding\n )\n ) {\n throw new InvalidScientificInputError(\n \"Supplied spectral response channel data must exactly match the response-channel binding carried by the reduction.\"\n );\n }\n}\n\nfunction requireEqeResponse(\n response:\n ResolvedSensorSpectralResponseValue\n): Extract<\n ResolvedSensorSpectralResponseValue,\n {\n kind:\n \"effective-external-quantum-efficiency\";\n }\n> {\n if (\n response.kind !==\n \"effective-external-quantum-efficiency\"\n ) {\n throw new InvalidScientificInputError(\n \"EQE electron-rate conversion cannot consume A/W spectral responsivity.\"\n );\n }\n return response;\n}\n\n/**\n * Converts the spectral radiant power collected by the site's geometric aperture\n * (before the sensor response) into an incident photon rate and an expected\n * generated electron rate.\n *\n * The response is applied at each wavelength node. One broadband-average QE is\n * never applied to the total power.\n *\n * Integration over exposure time happens later, so all outputs are rates per\n * second, not photon or electron counts.\n */\nexport function calculateSensorEqeElectronRate(\n input:\n CalculateSensorEqeElectronRateInput\n): CalculationResult<SensorEqeElectronRate> {\n requireInputKeys(input, \"calculateSensorEqeElectronRate\", [\"reduction\", \"compatibility\", \"operatingRange\", \"colorSamplingProfile\", \"spectralResponseProfile\", \"airPhotonEnergyContext\"]);\n validateAuthorizationChain(\n input.reduction,\n input.compatibility,\n input.operatingRange\n );\n\n if (\n input.reduction\n .sourceResponseKind ===\n \"effective-spectral-responsivity\"\n ) {\n throw new InvalidScientificInputError(\n \"EQE electron-rate conversion requires an EQE or filter×detector-EQE response, not A/W responsivity.\"\n );\n }\n\n const colorProfile =\n parseSensorColorSamplingProfile(\n input.colorSamplingProfile\n );\n const responseProfile =\n parseSensorSpectralResponseProfile(\n input.spectralResponseProfile\n );\n\n if (\n colorProfile.profileId !==\n input.reduction\n .colorSamplingProfileId ||\n responseProfile.profileId !==\n input.reduction\n .responseProfileId ||\n responseProfile\n .colorSamplingProfileId !==\n colorProfile.profileId\n ) {\n throw new InvalidScientificInputError(\n \"Supplied color/spectral response profiles must exactly match the reduction identities.\"\n );\n }\n\n validateExactResponseDataBinding(\n responseProfile,\n input.reduction\n );\n\n const nodes =\n validateReductionSpectralNodes(\n input.reduction\n );\n validateOperatingRangeNodeBinding(\n input.reduction,\n nodes,\n input.operatingRange\n );\n\n const airIndices =\n parseAirContext(\n input.reduction,\n nodes,\n input.airPhotonEnergyContext\n );\n\n const photonRateSum:\n CompensatedSum = {\n sum: 0,\n correction: 0\n };\n const electronRateSum:\n CompensatedSum = {\n sum: 0,\n correction: 0\n };\n const perWavelength:\n SensorEqeWavelengthRateContribution[] =\n [];\n\n let responseBindingValidated =\n false;\n let channelFilterTransmissionApplied =\n false;\n\n for (const node of nodes) {\n const resolved =\n resolveParsedSensorSpectralResponseAtWavelength({\n colorSamplingProfile:\n colorProfile,\n spectralResponseProfile:\n responseProfile,\n channelId:\n input.reduction.channelId,\n wavelengthNanometers:\n node.wavelengthNanometers,\n wavelengthBasis:\n input.reduction\n .wavelengthBasis\n });\n\n if (!responseBindingValidated) {\n validateResponseBinding(\n resolved.value,\n input.reduction\n );\n responseBindingValidated =\n true;\n }\n\n const eqe =\n requireEqeResponse(\n resolved.value.response\n );\n\n const airIndex =\n airIndices.get(\n node.spectralSampleIndex\n );\n const photonEnergy =\n calculatePhotonEnergyFromWavelength({\n wavelengthNanometers:\n node.wavelengthNanometers,\n wavelengthBasis:\n input.reduction\n .wavelengthBasis,\n ...(input.reduction\n .wavelengthBasis === \"air\"\n ? {\n airPhaseRefractiveIndex:\n airIndex!,\n ...(input.airPhotonEnergyContext!\n .operatingConditions ===\n undefined\n ? {}\n : {\n airOperatingConditions:\n input\n .airPhotonEnergyContext!\n .operatingConditions\n }),\n airConditionPolicy:\n input.airPhotonEnergyContext!\n .conditionPolicy\n }\n : {})\n }).value;\n\n const radiantPowerContributionWatts =\n node\n .geometricApertureIncidentSpectralFluxWattsPerNanometer *\n node.wavelengthMeasureNanometers;\n const incidentPhotonRatePerSecond =\n radiantPowerContributionWatts /\n photonEnergy.photonEnergyJoules;\n const expectedGeneratedElectronRatePerSecond =\n incidentPhotonRatePerSecond *\n eqe.externalQuantumEfficiency;\n\n if (\n !Number.isFinite(\n radiantPowerContributionWatts\n ) ||\n !Number.isFinite(\n incidentPhotonRatePerSecond\n ) ||\n !Number.isFinite(\n expectedGeneratedElectronRatePerSecond\n )\n ) {\n throw new InvalidScientificInputError(\n \"EQE wavelength-node rate calculation must remain finite.\"\n );\n }\n\n addCompensated(\n photonRateSum,\n incidentPhotonRatePerSecond\n );\n addCompensated(\n electronRateSum,\n expectedGeneratedElectronRatePerSecond\n );\n\n if (\n eqe.composition ===\n \"channel-filter-transmittance-times-detector-eqe\"\n ) {\n channelFilterTransmissionApplied =\n true;\n }\n\n perWavelength.push({\n spectralSampleIndex:\n node.spectralSampleIndex,\n wavelengthNanometers:\n node.wavelengthNanometers,\n wavelengthMeasureNanometers:\n node.wavelengthMeasureNanometers,\n geometricApertureIncidentSpectralFluxWattsPerNanometer:\n node\n .geometricApertureIncidentSpectralFluxWattsPerNanometer,\n radiantPowerContributionWatts,\n vacuumWavelengthNanometers:\n photonEnergy\n .vacuumWavelengthNanometers,\n photonEnergyJoules:\n photonEnergy\n .photonEnergyJoules,\n wavelengthConversion:\n photonEnergy\n .wavelengthConversion,\n ...(photonEnergy\n .phaseRefractiveIndexUsed ===\n undefined\n ? {}\n : {\n phaseRefractiveIndexUsed:\n photonEnergy\n .phaseRefractiveIndexUsed\n }),\n ...(photonEnergy\n .airConditionCompatibility ===\n undefined\n ? {}\n : {\n airConditionCompatibility:\n photonEnergy\n .airConditionCompatibility\n }),\n ...(photonEnergy\n .airRefractiveIndexEvidence ===\n undefined\n ? {}\n : {\n airRefractiveIndexEvidence:\n photonEnergy\n .airRefractiveIndexEvidence\n }),\n incidentPhotonRatePerSecond,\n effectiveExternalQuantumEfficiency:\n eqe.externalQuantumEfficiency,\n responseComposition:\n eqe.composition,\n ...(eqe.channelFilterTransmittance ===\n undefined\n ? {}\n : {\n channelFilterTransmittance:\n eqe.channelFilterTransmittance\n }),\n ...(eqe\n .detectorExternalQuantumEfficiency ===\n undefined\n ? {}\n : {\n detectorExternalQuantumEfficiency:\n eqe\n .detectorExternalQuantumEfficiency\n }),\n responseInterpolationUsed:\n resolved.value\n .interpolationUsed,\n expectedGeneratedElectronRatePerSecond\n });\n }\n\n if (\n !Number.isFinite(\n photonRateSum.sum\n ) ||\n !Number.isFinite(\n electronRateSum.sum\n )\n ) {\n throw new InvalidScientificInputError(\n \"Total EQE photon/electron rates must remain finite.\"\n );\n }\n\n return approximationResult(\n {\n colorSamplingProfileId:\n input.reduction\n .colorSamplingProfileId,\n responseProfileId:\n input.reduction\n .responseProfileId,\n responseApplicationProfileId:\n input.compatibility\n .applicationProfileId,\n operatingRangeProfileId:\n input.operatingRange\n .operatingRangeProfileId,\n site: {\n ...input.reduction.site\n },\n channelId:\n input.reduction.channelId,\n sourceResponseKind:\n input.reduction\n .sourceResponseKind,\n responseScope:\n input.reduction.responseScope,\n responseReferencePlane:\n input.compatibility\n .requiredSourcePlane,\n wavelengthBasis:\n input.reduction\n .wavelengthBasis,\n wavelengthRangeNanometers: {\n ...input.reduction\n .wavelengthRangeNanometers\n },\n spectralNodeCount:\n nodes.length,\n perWavelength,\n incidentPhotonRatePerSecond:\n photonRateSum.sum,\n expectedGeneratedElectronRatePerSecond:\n electronRateSum.sum,\n summationMethod:\n \"kahan-compensated\",\n responseApplicationPerformed:\n true,\n quantumEfficiencyApplied:\n true,\n spectralResponsivityApplied:\n false,\n channelFilterTransmissionApplied,\n photonRateCalculated: true,\n electronRateCalculated: true,\n photonCountCalculated: false,\n electronCountCalculated: false,\n currentCalculated: false,\n temporalIntegrationApplied:\n false,\n exposureDurationApplied: false,\n saturationAssessed: false,\n shotNoiseApplied: false,\n readNoiseApplied: false,\n adcQuantizationApplied:\n false,\n rawCodeValueProduced: false,\n responseUncertaintyPropagated:\n false,\n photonEnergyUncertaintyPropagated:\n false,\n quadratureConvergenceErrorEstimated:\n false\n },\n \"sensor-eqe-electron-rate\",\n \"1.0.0\",\n [\n \"Response is evaluated independently at every wavelength quadrature node; broadband radiant power is never multiplied by an average QE.\",\n \"Each node converts radiant power to photon rate using photon energy hν, then applies effective external QE to obtain expected generated-electron rate.\",\n \"Direct effective EQE is preserved as direct response; explicitly separable channel-filter transmittance × detector EQE is multiplied only through the existing spectral-response resolver.\",\n \"The response calibration metadata/evidence must match the spectral evidence carried by the pre-response reduction; matching profile IDs alone is insufficient.\",\n \"The structural compatibility and operating-range assessments must both be non-blocked and authorize the photon-rate→electron path.\",\n \"Operating-range authorization must be per spectral bin, with spectral-node identities and calibrated-domain values bound back to the exact reduction used for conversion.\",\n \"Operating-range authorization must establish linear superposition over the geometric aperture so spatial reduction may precede response application without hiding sub-aperture nonlinear behavior.\",\n \"Air-basis spectral nodes require one exact-wavelength sourced phase refractive index per spectral sample plus the photon-energy atmosphere compatibility policy.\",\n \"Kahan compensated summation is used across wavelength nodes to reduce loss of small contributions when rates span large dynamic ranges.\",\n \"Outputs are instantaneous expected rates only. Exposure duration, photon/electron counts, full-well saturation, shot/read noise, ADC/RAW conversion and reconstruction remain downstream.\",\n \"Response/refractive-index uncertainties and quadrature convergence error are not propagated into a combined output uncertainty by this version.\"\n ]\n );\n}\n"]}
|
|
@@ -106,6 +106,8 @@ export declare function parseSecondsFact(value: unknown, path: string): SourcedC
|
|
|
106
106
|
/** Parses one exposure boundary schedule (opening or closing). Not a root export. */
|
|
107
107
|
export declare function parseBoundarySchedule(value: unknown, path: string): ExposureBoundarySchedule;
|
|
108
108
|
export declare function validateNativeRaster(raster: NativeImageRaster): void;
|
|
109
|
+
export declare function validateActiveCaptureRect(rect: RasterRect, raster: NativeImageRaster): void;
|
|
110
|
+
export declare function validateShutterMechanism(value: unknown): asserts value is CaptureShutterMechanism;
|
|
109
111
|
export declare function scanVector(direction: NativeSensorReadoutScanDirection): RasterVector;
|
|
110
112
|
export declare function normalizedScanPosition(point: RasterPoint, rect: RasterRect, direction: NativeSensorReadoutScanDirection): number;
|
|
111
113
|
/**
|
|
@@ -112,7 +112,7 @@ export function validateNativeRaster(raster) {
|
|
|
112
112
|
requirePositiveInteger("nativeRaster.pixelWidth", raster.pixelWidth);
|
|
113
113
|
requirePositiveInteger("nativeRaster.pixelHeight", raster.pixelHeight);
|
|
114
114
|
}
|
|
115
|
-
function validateActiveCaptureRect(rect, raster) {
|
|
115
|
+
export function validateActiveCaptureRect(rect, raster) {
|
|
116
116
|
requireNonNegativeInteger("activeCaptureRect.x", rect.x);
|
|
117
117
|
requireNonNegativeInteger("activeCaptureRect.y", rect.y);
|
|
118
118
|
requirePositiveInteger("activeCaptureRect.width", rect.width);
|
|
@@ -122,7 +122,7 @@ function validateActiveCaptureRect(rect, raster) {
|
|
|
122
122
|
throw new InvalidScientificInputError("activeCaptureRect must fit entirely within nativeRaster.");
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
|
-
function validateShutterMechanism(value) {
|
|
125
|
+
export function validateShutterMechanism(value) {
|
|
126
126
|
if (typeof value !== "string" ||
|
|
127
127
|
!SHUTTER_MECHANISMS.has(value)) {
|
|
128
128
|
throw new InvalidConfigurationError("shutterMechanism is invalid.");
|