@photivra/engine 2.9.0 → 2.11.1

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.
Files changed (147) hide show
  1. package/CHANGELOG.md +223 -1
  2. package/CITATION.cff +1 -1
  3. package/NODE_SUPPORT.md +21 -3
  4. package/README.md +6 -6
  5. package/SECURITY.md +1 -1
  6. package/THIRD_PARTY.md +2 -0
  7. package/dist/capture/native-environment-lean.js +89 -5
  8. package/dist/capture/native-environment-lean.js.map +1 -1
  9. package/dist/capture/native-environment-tabulated.d.ts +37 -0
  10. package/dist/capture/native-environment-tabulated.js +42 -6
  11. package/dist/capture/native-environment-tabulated.js.map +1 -1
  12. package/dist/core/portable-math.d.ts +19 -0
  13. package/dist/core/portable-math.js +82 -0
  14. package/dist/core/portable-math.js.map +1 -1
  15. package/dist/core/version.d.ts +1 -1
  16. package/dist/core/version.js +1 -1
  17. package/dist/core/version.js.map +1 -1
  18. package/dist/facade/channel-response.d.ts +15 -0
  19. package/dist/facade/channel-response.js +33 -0
  20. package/dist/facade/channel-response.js.map +1 -0
  21. package/dist/facade/chart-material-layers.d.ts +115 -0
  22. package/dist/facade/chart-material-layers.js +224 -0
  23. package/dist/facade/chart-material-layers.js.map +1 -0
  24. package/dist/facade/chart-radiance-batch.d.ts +8 -2
  25. package/dist/facade/chart-radiance-batch.js +11 -4
  26. package/dist/facade/chart-radiance-batch.js.map +1 -1
  27. package/dist/facade/chart-scenes.d.ts +134 -0
  28. package/dist/facade/chart-scenes.js +32 -22
  29. package/dist/facade/chart-scenes.js.map +1 -1
  30. package/dist/facade/diffraction-provider.d.ts +8 -2
  31. package/dist/facade/diffraction-provider.js +120 -16
  32. package/dist/facade/diffraction-provider.js.map +1 -1
  33. package/dist/facade/display-view-gpu-session.d.ts +18 -0
  34. package/dist/facade/display-view-gpu-session.js +280 -0
  35. package/dist/facade/display-view-gpu-session.js.map +1 -0
  36. package/dist/facade/display-view-gpu.d.ts +146 -0
  37. package/dist/facade/display-view-gpu.js +59 -0
  38. package/dist/facade/display-view-gpu.js.map +1 -0
  39. package/dist/facade/display-view.d.ts +123 -1
  40. package/dist/facade/display-view.js +478 -116
  41. package/dist/facade/display-view.js.map +1 -1
  42. package/dist/facade/expected-signal-setup.d.ts +5 -0
  43. package/dist/facade/expected-signal-setup.js +18 -1
  44. package/dist/facade/expected-signal-setup.js.map +1 -1
  45. package/dist/facade/expected-signal.d.ts +80 -6
  46. package/dist/facade/expected-signal.js +200 -24
  47. package/dist/facade/expected-signal.js.map +1 -1
  48. package/dist/facade/scene-radiance.d.ts +4 -2
  49. package/dist/facade/scene-radiance.js +7 -4
  50. package/dist/facade/scene-radiance.js.map +1 -1
  51. package/dist/facade/simulate-capture.d.ts +81 -7
  52. package/dist/facade/simulate-capture.js +276 -53
  53. package/dist/facade/simulate-capture.js.map +1 -1
  54. package/dist/facade/simulate-display-view.d.ts +54 -3
  55. package/dist/facade/simulate-display-view.js +120 -27
  56. package/dist/facade/simulate-display-view.js.map +1 -1
  57. package/dist/gpu/chart-composite-gpu.d.ts +119 -0
  58. package/dist/gpu/chart-composite-gpu.js +572 -0
  59. package/dist/gpu/chart-composite-gpu.js.map +1 -0
  60. package/dist/gpu/chart-radiance-gpu.d.ts +6 -6
  61. package/dist/gpu/chart-radiance-gpu.js +9 -11
  62. package/dist/gpu/chart-radiance-gpu.js.map +1 -1
  63. package/dist/gpu/display-view-chart-material-module.d.ts +10 -0
  64. package/dist/gpu/display-view-chart-material-module.js +58 -0
  65. package/dist/gpu/display-view-chart-material-module.js.map +1 -0
  66. package/dist/gpu/display-view-composite-material-module.d.ts +68 -0
  67. package/dist/gpu/display-view-composite-material-module.js +248 -0
  68. package/dist/gpu/display-view-composite-material-module.js.map +1 -0
  69. package/dist/gpu/display-view-quality-gpu.d.ts +185 -0
  70. package/dist/gpu/display-view-quality-gpu.js +1000 -0
  71. package/dist/gpu/display-view-quality-gpu.js.map +1 -0
  72. package/dist/gpu/display-view-speed-gpu.d.ts +156 -0
  73. package/dist/gpu/display-view-speed-gpu.js +524 -0
  74. package/dist/gpu/display-view-speed-gpu.js.map +1 -0
  75. package/dist/gpu/image-kernel-accelerator.d.ts +23 -4
  76. package/dist/gpu/image-kernel-accelerator.js +58 -10
  77. package/dist/gpu/image-kernel-accelerator.js.map +1 -1
  78. package/dist/gpu/image-kernel-execution.d.ts +60 -3
  79. package/dist/gpu/image-kernel-execution.js +2 -2
  80. package/dist/gpu/image-kernel-execution.js.map +1 -1
  81. package/dist/gpu/sensor-stage-gpu.d.ts +11 -0
  82. package/dist/gpu/sensor-stage-gpu.js +42 -13
  83. package/dist/gpu/sensor-stage-gpu.js.map +1 -1
  84. package/dist/gpu/webgpu-runtime.d.ts +4 -2
  85. package/dist/gpu/webgpu-runtime.js +13 -4
  86. package/dist/gpu/webgpu-runtime.js.map +1 -1
  87. package/dist/gpu/webgpu-switch.d.ts +4 -0
  88. package/dist/gpu/webgpu-switch.js +29 -0
  89. package/dist/gpu/webgpu-switch.js.map +1 -0
  90. package/dist/gpu/wgsl-chart-composite-kernel.d.ts +52 -0
  91. package/dist/gpu/wgsl-chart-composite-kernel.js +515 -0
  92. package/dist/gpu/wgsl-chart-composite-kernel.js.map +1 -0
  93. package/dist/gpu/wgsl-chart-kernel.d.ts +11 -9
  94. package/dist/gpu/wgsl-chart-kernel.js +33 -27
  95. package/dist/gpu/wgsl-chart-kernel.js.map +1 -1
  96. package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +95 -0
  97. package/dist/gpu/wgsl-display-view-quality-kernel.js +769 -0
  98. package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -0
  99. package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +75 -0
  100. package/dist/gpu/wgsl-display-view-speed-kernel.js +386 -0
  101. package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -0
  102. package/dist/gpu/wgsl-sensor-kernel.d.ts +28 -9
  103. package/dist/gpu/wgsl-sensor-kernel.js +94 -50
  104. package/dist/gpu/wgsl-sensor-kernel.js.map +1 -1
  105. package/dist/index.d.ts +6 -3
  106. package/dist/index.js +4 -2
  107. package/dist/index.js.map +1 -1
  108. package/dist/optics/defocus-disc-kernel.d.ts +61 -0
  109. package/dist/optics/defocus-disc-kernel.js +191 -0
  110. package/dist/optics/defocus-disc-kernel.js.map +1 -0
  111. package/dist/optics/irradiance-convolution.d.ts +11 -0
  112. package/dist/optics/irradiance-convolution.js +64 -24
  113. package/dist/optics/irradiance-convolution.js.map +1 -1
  114. package/dist/optics/scene-to-sensor-irradiance.d.ts +13 -0
  115. package/dist/optics/scene-to-sensor-irradiance.js +58 -24
  116. package/dist/optics/scene-to-sensor-irradiance.js.map +1 -1
  117. package/dist/optics/sensor-aperture-rays.js +2 -1
  118. package/dist/optics/sensor-aperture-rays.js.map +1 -1
  119. package/dist/optics/sensor-environment-query.js +4 -3
  120. package/dist/optics/sensor-environment-query.js.map +1 -1
  121. package/dist/optics/site-diffraction-kernel.d.ts +16 -0
  122. package/dist/optics/site-diffraction-kernel.js +60 -8
  123. package/dist/optics/site-diffraction-kernel.js.map +1 -1
  124. package/dist/processing/demosaic.js +114 -37
  125. package/dist/processing/demosaic.js.map +1 -1
  126. package/dist/schema/scene-radiance.d.ts +8 -0
  127. package/dist/schema/scene-radiance.js +28 -17
  128. package/dist/schema/scene-radiance.js.map +1 -1
  129. package/dist/sensor/environment-photo-signal.d.ts +21 -0
  130. package/dist/sensor/environment-photo-signal.js +68 -2
  131. package/dist/sensor/environment-photo-signal.js.map +1 -1
  132. package/docs/API_REFERENCE.md +60 -1
  133. package/docs/QUICK_START.md +120 -11
  134. package/docs/USAGE.md +82 -2
  135. package/docs/api/core-version.md +1 -1
  136. package/docs/api/exports.json +157 -2
  137. package/docs/api/facade-chart-material-layers.md +137 -0
  138. package/docs/api/facade-chart-radiance-batch.md +7 -2
  139. package/docs/api/facade-display-view-gpu.md +94 -0
  140. package/docs/api/facade-display-view.md +50 -0
  141. package/docs/api/facade-scene-radiance.md +3 -2
  142. package/docs/api/facade-simulate-capture.md +81 -12
  143. package/docs/api/facade-simulate-display-view.md +39 -2
  144. package/docs/api/gpu-image-kernel-accelerator.md +10 -3
  145. package/docs/api/gpu-image-kernel-execution.md +76 -4
  146. package/docs/api/gpu-webgpu-switch.md +23 -0
  147. package/package.json +3 -1
@@ -0,0 +1,191 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Internal (not exported from the package): the geometric defocus disc of a flat scene, sampled on the square site
4
+ * lattice, for `lens.defocus: { model: "geometric-disc" }` (#492).
5
+ *
6
+ * Model. For a scene plane at the subject distance d and a thin lens focused at s, an ideal uniform circular pupil
7
+ * spreads every in-focus image point into a uniform disc of diameter c = f²·|s − d| / (N·d·(s − f)) on the sensor
8
+ * (`calculateDefocusCircle`, thin-lens model 1.1.0; geometric optics, no diffraction or aberrations). The defocused
9
+ * image of the plane is therefore the in-focus image convolved with that disc: one fixed, wavelength-independent
10
+ * kernel. It is an approximation (geometric optics, ideal uniform circular pupil, no vignetting of the pupil).
11
+ *
12
+ * Sampling. The disc of diameter c/p sites (p the site pitch) is centred on a site centre. The weight of the site at
13
+ * offset (i, j) is the exact area of the disc inside that site's square cell [i − ½, i + ½] × [j − ½, j + ½], divided
14
+ * by the area of the disc. The area of a disc inside a rectangle is a signed sum over the rectangle's four corners of
15
+ * the area Q(x, y) of the disc inside the quadrant [0, x] × [0, y] (inclusion–exclusion), and for x, y ≥ 0 with
16
+ * x, y clamped to the radius r
17
+ * Q = x·y when x² + y² ≤ r²,
18
+ * Q = ½·(x_a·y + x·y_a + r²·(arcsin(x/r) − arcsin(x_a/r))) otherwise,
19
+ * with x_a = √(r² − y²) and y_a = √(r² − x²) the arc's crossings of the quadrant's edges: the rectangle left of the
20
+ * crossing plus the area under the arc, ∫ √(r² − t²) dt = ½·(t·√(r² − t²) + r²·arcsin(t/r)) (elementary geometry
21
+ * and calculus; NIST DLMF 4.24.1 for the arcsine, free). The arcsine difference is the angle between the two arc
22
+ * points, evaluated as arcsin((x·y − x_a·y_a)/r²) (the sine of a difference). Q is extended to negative x or y as an
23
+ * odd function.
24
+ *
25
+ * Determinism. Only correctly rounded operations and `portableAsin` are used, so the weights are the same bits in
26
+ * every JavaScript engine. One octant (0 ≤ j ≤ i) is computed and mirrored, so the kernel has the exact 8-fold
27
+ * symmetry of the disc and the square lattice. The weights are divided by their float64 sum, then the centre weight
28
+ * takes up the remaining rounding, so their exact sum is 1 within half a unit in the last place of the centre weight.
29
+ *
30
+ * Limits. The window is (2R + 1)² sites with R = ⌈r − ½⌉ (0 when r ≤ ½: a disc inside one cell is the identity, a
31
+ * valid kernel). R is capped at {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES}; past it the call fails, naming the
32
+ * cap and the radius asked for, instead of clipping the disc.
33
+ */
34
+ import { portableAsin } from "../core/portable-math.js";
35
+ import { InvalidScientificInputError } from "../core/validation.js";
36
+ import { calculateDefocusCircle } from "./depth-of-field.js";
37
+ /**
38
+ * Largest kernel radius in sites, R ≤ 64 (a 129 × 129 window, 16 641 taps per site; the diffraction kernels stop at 32).
39
+ * Set from the cost measured in the capture (#492, step 2a; docs/QUICK_START.md, "Optics"): the capture's time grows with
40
+ * R² at every site the disc sees an edge from, and a 0.7 MP Siemens star took 0.37 s in focus, 2.9 s at R = 32, 4.7 s at
41
+ * R = 41 and 11.6 s at R = 64 (Node, one thread). 64 keeps the issue's 1.4 µm example (50 mm f/2.8, subject 2 m, focus
42
+ * 1.6 m: R = 41) with room for larger blurs, at a cost the caller chooses and the docs state; past it the call fails.
43
+ */
44
+ export const DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES = 64;
45
+ /** Kernel radius in sites for a disc of `diameterSites`: the cells [i − ½, i + ½] the disc reaches with positive area. */
46
+ export function defocusDiscKernelRadius(diameterSites) {
47
+ requireDiameter(diameterSites);
48
+ const r = diameterSites / 2;
49
+ return r <= 0.5 ? 0 : Math.ceil(r - 0.5);
50
+ }
51
+ /**
52
+ * {@link defocusDiscKernelRadius}, failing with `InvalidScientificInputError` (naming the cap and the radius asked for)
53
+ * when it exceeds {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES}. The disc is never clipped to fit.
54
+ */
55
+ export function requireDefocusDiscKernelRadius(diameterSites) {
56
+ const radius = defocusDiscKernelRadius(diameterSites);
57
+ if (radius > DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES)
58
+ throw new InvalidScientificInputError("The defocus disc of " + diameterSites + " sites needs a kernel radius of " + radius +
59
+ " sites; the limit is " + DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES + " sites (DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES).");
60
+ return radius;
61
+ }
62
+ function requireDiameter(diameterSites) {
63
+ if (typeof diameterSites !== "number" || !Number.isFinite(diameterSites) || diameterSites < 0)
64
+ throw new InvalidScientificInputError("The defocus disc diameter must be a finite number of sites, zero or more.");
65
+ }
66
+ /** Area of the disc of radius r (centred at the origin) inside [0, x] × [0, y], for x, y ≥ 0. */
67
+ function quadrantArea(x, y, r) {
68
+ if (x > r)
69
+ x = r;
70
+ if (y > r)
71
+ y = r;
72
+ if (x * x + y * y <= r * r)
73
+ return x * y;
74
+ const xa = Math.sqrt((r - y) * (r + y)), ya = Math.sqrt((r - x) * (r + x));
75
+ // arcsin(x/r) − arcsin(x_a/r) is the angle between the arc points (x_a, y) and (x, y_a), whose sine is (x·y − x_a·y_a)/r².
76
+ return 0.5 * (xa * y + x * ya + r * r * portableAsin(Math.min(1, (x * y - xa * ya) / (r * r))));
77
+ }
78
+ /** {@link quadrantArea} extended as an odd function of x and of y. */
79
+ function signedQuadrantArea(x, y, r) {
80
+ const q = quadrantArea(x < 0 ? -x : x, y < 0 ? -y : y, r);
81
+ return (x < 0) !== (y < 0) ? -q : q;
82
+ }
83
+ /**
84
+ * Area of the disc inside the cell [i − ½, i + ½] × [j − ½, j + ½], for i ≥ j ≥ 0. Exactly 0 when the cell's point nearest
85
+ * the centre lies on or outside the circle. Otherwise the four corner terms are up to r² in size, so the difference
86
+ * carries an absolute rounding error of a few r²·2^-53 (below 10^-11 at the radius cap, under 10^-15 of the disc's area);
87
+ * a cell that barely touches the disc can come out a rounding error below zero and is set to 0.
88
+ */
89
+ function cellArea(i, j, r) {
90
+ const x0 = i - 0.5, x1 = i + 0.5, y0 = j - 0.5, y1 = j + 0.5;
91
+ const nx = i === 0 ? 0 : x0, ny = j === 0 ? 0 : y0;
92
+ if (nx * nx + ny * ny >= r * r)
93
+ return 0;
94
+ const area = (signedQuadrantArea(x1, y1, r) - signedQuadrantArea(x0, y1, r)) - (signedQuadrantArea(x1, y0, r) - signedQuadrantArea(x0, y0, r));
95
+ return area > 0 ? area : 0;
96
+ }
97
+ /**
98
+ * The defocus disc of `diameterSites` (c / pitch) sampled on the site lattice, or null for a zero diameter (in focus:
99
+ * callers apply no kernel). Fails with `InvalidScientificInputError` for a negative or non-finite diameter, and when
100
+ * the kernel radius exceeds {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES}.
101
+ */
102
+ export function createDefocusDiscSiteKernel(diameterSites) {
103
+ const radius = requireDefocusDiscKernelRadius(diameterSites);
104
+ if (diameterSites === 0)
105
+ return null;
106
+ const r = diameterSites / 2, width = 2 * radius + 1, weights = new Float64Array(width * width);
107
+ // One octant, mirrored into the eight symmetric cells.
108
+ for (let i = 0; i <= radius; i++)
109
+ for (let j = 0; j <= i; j++) {
110
+ const a = cellArea(i, j, r);
111
+ for (const [u, v] of [[i, j], [j, i]])
112
+ for (const su of u === 0 ? [1] : [1, -1])
113
+ for (const sv of v === 0 ? [1] : [1, -1])
114
+ weights[(sv * v + radius) * width + su * u + radius] = a;
115
+ }
116
+ normalize(weights, (width * width - 1) / 2);
117
+ return { diameterSites, radius, width, weights };
118
+ }
119
+ /**
120
+ * Divides by the float64 sum, then sets the centre weight to 1 minus the exact sum of the others (accumulated as a
121
+ * double-double with Knuth's TwoSum), rounded once. The exact real sum of the weights is then 1 within half a unit in
122
+ * the last place of the centre weight. (No float64 summation order can promise exactly 1: a one-ulp change of one weight
123
+ * can move a rounded sum from just below 1 to just above it.)
124
+ */
125
+ function normalize(weights, centre) {
126
+ let total = 0;
127
+ for (let k = 0; k < weights.length; k++)
128
+ total += weights[k];
129
+ for (let k = 0; k < weights.length; k++)
130
+ weights[k] /= total;
131
+ let hi = 0, lo = 0;
132
+ for (let k = 0; k < weights.length; k++) {
133
+ if (k === centre)
134
+ continue;
135
+ const w = weights[k], s = hi + w, b = s - hi;
136
+ lo += (hi - (s - b)) + (w - b);
137
+ hi = s;
138
+ }
139
+ // 1 − (hi + lo): TwoSum of 1 and −hi, then the low parts.
140
+ const s = 1 - hi, b = s - 1, e = (1 - (s - b)) + (-hi - b);
141
+ weights[centre] = s + (e - lo);
142
+ }
143
+ /**
144
+ * Resolves the defocus disc for a thin lens focused at `focusDistanceM` imaging a plane at `subjectDistanceM`, on a
145
+ * lattice of `pitchMicrometers`. Exactly zero when the two distances are equal. It does not check the radius cap.
146
+ */
147
+ export function resolveDefocusDisc(input) {
148
+ const { pitchMicrometers, ...circle } = input;
149
+ if (!Number.isFinite(pitchMicrometers) || pitchMicrometers <= 0)
150
+ throw new InvalidScientificInputError("pitchMicrometers must be a positive finite number.");
151
+ const diameterMicrometers = calculateDefocusCircle(circle).value.diameterMm * 1000;
152
+ const diameterSites = diameterMicrometers / pitchMicrometers;
153
+ return { diameterMicrometers, diameterSites, kernelRadiusSites: defocusDiscKernelRadius(diameterSites) };
154
+ }
155
+ /**
156
+ * The full 2-D convolution of two square site kernels (row-major, radii `radiusA` and `radiusB`): a kernel of radius
157
+ * radiusA + radiusB with out(t) = Σ_s a(s)·b(t − s), each output summed over the taps of `a` in row-major order (zero taps
158
+ * of either kernel skipped, which adds exact zeros only). Used for lens.defocus with lens.diffraction (#492): each spectral
159
+ * node's diffraction kernel convolved with the defocus disc. That combination is an approximation: the defocused wave PSF
160
+ * of a circular pupil is not exactly the in-focus Airy pattern convolved with the geometric disc. Both inputs here are
161
+ * centred and non-negative, so the result is too; its sum is the product of the inputs' sums up to rounding.
162
+ */
163
+ export function convolveSiteKernels(a, radiusA, b, radiusB) {
164
+ const wa = 2 * radiusA + 1, wb = 2 * radiusB + 1, radius = radiusA + radiusB, width = 2 * radius + 1;
165
+ if (a.length !== wa * wa || b.length !== wb * wb)
166
+ throw new InvalidScientificInputError("Site kernels must be (2R + 1)² taps.");
167
+ const out = new Float64Array(width * width);
168
+ // Non-zero taps of b, as (row, column, weight).
169
+ const bRows = [], bColumns = [], bWeights = [];
170
+ for (let j = 0; j < wb; j++)
171
+ for (let i = 0; i < wb; i++) {
172
+ const w = b[j * wb + i];
173
+ if (w !== 0) {
174
+ bRows.push(j);
175
+ bColumns.push(i);
176
+ bWeights.push(w);
177
+ }
178
+ }
179
+ for (let sj = 0; sj < wa; sj++)
180
+ for (let si = 0; si < wa; si++) {
181
+ const w = a[sj * wa + si];
182
+ if (w === 0)
183
+ continue;
184
+ for (let k = 0; k < bWeights.length; k++) {
185
+ const index = (sj + bRows[k]) * width + si + bColumns[k];
186
+ out[index] = out[index] + w * bWeights[k];
187
+ }
188
+ }
189
+ return out;
190
+ }
191
+ //# sourceMappingURL=defocus-disc-kernel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defocus-disc-kernel.js","sourceRoot":"","sources":["../../src/optics/defocus-disc-kernel.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Internal (not exported from the package): the geometric defocus disc of a flat scene, sampled on the square site\n * lattice, for `lens.defocus: { model: \"geometric-disc\" }` (#492).\n *\n * Model. For a scene plane at the subject distance d and a thin lens focused at s, an ideal uniform circular pupil\n * spreads every in-focus image point into a uniform disc of diameter c = f²·|s − d| / (N·d·(s − f)) on the sensor\n * (`calculateDefocusCircle`, thin-lens model 1.1.0; geometric optics, no diffraction or aberrations). The defocused\n * image of the plane is therefore the in-focus image convolved with that disc: one fixed, wavelength-independent\n * kernel. It is an approximation (geometric optics, ideal uniform circular pupil, no vignetting of the pupil).\n *\n * Sampling. The disc of diameter c/p sites (p the site pitch) is centred on a site centre. The weight of the site at\n * offset (i, j) is the exact area of the disc inside that site's square cell [i − ½, i + ½] × [j − ½, j + ½], divided\n * by the area of the disc. The area of a disc inside a rectangle is a signed sum over the rectangle's four corners of\n * the area Q(x, y) of the disc inside the quadrant [0, x] × [0, y] (inclusion–exclusion), and for x, y ≥ 0 with\n * x, y clamped to the radius r\n * Q = x·y when x² + y² ≤ r²,\n * Q = ½·(x_a·y + x·y_a + r²·(arcsin(x/r) − arcsin(x_a/r))) otherwise,\n * with x_a = √(r² − y²) and y_a = √(r² − x²) the arc's crossings of the quadrant's edges: the rectangle left of the\n * crossing plus the area under the arc, ∫ √(r² − t²) dt = ½·(t·√(r² − t²) + r²·arcsin(t/r)) (elementary geometry\n * and calculus; NIST DLMF 4.24.1 for the arcsine, free). The arcsine difference is the angle between the two arc\n * points, evaluated as arcsin((x·y − x_a·y_a)/r²) (the sine of a difference). Q is extended to negative x or y as an\n * odd function.\n *\n * Determinism. Only correctly rounded operations and `portableAsin` are used, so the weights are the same bits in\n * every JavaScript engine. One octant (0 ≤ j ≤ i) is computed and mirrored, so the kernel has the exact 8-fold\n * symmetry of the disc and the square lattice. The weights are divided by their float64 sum, then the centre weight\n * takes up the remaining rounding, so their exact sum is 1 within half a unit in the last place of the centre weight.\n *\n * Limits. The window is (2R + 1)² sites with R = ⌈r − ½⌉ (0 when r ≤ ½: a disc inside one cell is the identity, a\n * valid kernel). R is capped at {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES}; past it the call fails, naming the\n * cap and the radius asked for, instead of clipping the disc.\n */\n\nimport { portableAsin } from \"../core/portable-math.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport { calculateDefocusCircle } from \"./depth-of-field.js\";\n\n/**\n * Largest kernel radius in sites, R ≤ 64 (a 129 × 129 window, 16 641 taps per site; the diffraction kernels stop at 32).\n * Set from the cost measured in the capture (#492, step 2a; docs/QUICK_START.md, \"Optics\"): the capture's time grows with\n * R² at every site the disc sees an edge from, and a 0.7 MP Siemens star took 0.37 s in focus, 2.9 s at R = 32, 4.7 s at\n * R = 41 and 11.6 s at R = 64 (Node, one thread). 64 keeps the issue's 1.4 µm example (50 mm f/2.8, subject 2 m, focus\n * 1.6 m: R = 41) with room for larger blurs, at a cost the caller chooses and the docs state; past it the call fails.\n */\nexport const DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES = 64;\n\n/** The disc sampled on the site lattice. */\nexport interface DefocusDiscSiteKernel {\n /** Disc diameter in sites (c / pitch). */\n readonly diameterSites: number;\n /** Kernel radius R in sites: the window is (2R + 1) × (2R + 1). */\n readonly radius: number;\n /** Window side, 2R + 1. */\n readonly width: number;\n /** Row-major weights, index (j + R)·width + (i + R) for the site offset (i, j); their row-major float64 sum is 1. */\n readonly weights: Float64Array;\n}\n\n/** Kernel radius in sites for a disc of `diameterSites`: the cells [i − ½, i + ½] the disc reaches with positive area. */\nexport function defocusDiscKernelRadius(diameterSites: number): number {\n requireDiameter(diameterSites);\n const r = diameterSites / 2;\n return r <= 0.5 ? 0 : Math.ceil(r - 0.5);\n}\n\n/**\n * {@link defocusDiscKernelRadius}, failing with `InvalidScientificInputError` (naming the cap and the radius asked for)\n * when it exceeds {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES}. The disc is never clipped to fit.\n */\nexport function requireDefocusDiscKernelRadius(diameterSites: number): number {\n const radius = defocusDiscKernelRadius(diameterSites);\n if (radius > DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES)\n throw new InvalidScientificInputError(\"The defocus disc of \" + diameterSites + \" sites needs a kernel radius of \" + radius +\n \" sites; the limit is \" + DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES + \" sites (DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES).\");\n return radius;\n}\n\nfunction requireDiameter(diameterSites: number): void {\n if (typeof diameterSites !== \"number\" || !Number.isFinite(diameterSites) || diameterSites < 0)\n throw new InvalidScientificInputError(\"The defocus disc diameter must be a finite number of sites, zero or more.\");\n}\n\n/** Area of the disc of radius r (centred at the origin) inside [0, x] × [0, y], for x, y ≥ 0. */\nfunction quadrantArea(x: number, y: number, r: number): number {\n if (x > r) x = r;\n if (y > r) y = r;\n if (x * x + y * y <= r * r) return x * y;\n const xa = Math.sqrt((r - y) * (r + y)), ya = Math.sqrt((r - x) * (r + x));\n // arcsin(x/r) − arcsin(x_a/r) is the angle between the arc points (x_a, y) and (x, y_a), whose sine is (x·y − x_a·y_a)/r².\n return 0.5 * (xa * y + x * ya + r * r * portableAsin(Math.min(1, (x * y - xa * ya) / (r * r))));\n}\n/** {@link quadrantArea} extended as an odd function of x and of y. */\nfunction signedQuadrantArea(x: number, y: number, r: number): number {\n const q = quadrantArea(x < 0 ? -x : x, y < 0 ? -y : y, r);\n return (x < 0) !== (y < 0) ? -q : q;\n}\n/**\n * Area of the disc inside the cell [i − ½, i + ½] × [j − ½, j + ½], for i ≥ j ≥ 0. Exactly 0 when the cell's point nearest\n * the centre lies on or outside the circle. Otherwise the four corner terms are up to r² in size, so the difference\n * carries an absolute rounding error of a few r²·2^-53 (below 10^-11 at the radius cap, under 10^-15 of the disc's area);\n * a cell that barely touches the disc can come out a rounding error below zero and is set to 0.\n */\nfunction cellArea(i: number, j: number, r: number): number {\n const x0 = i - 0.5, x1 = i + 0.5, y0 = j - 0.5, y1 = j + 0.5;\n const nx = i === 0 ? 0 : x0, ny = j === 0 ? 0 : y0;\n if (nx * nx + ny * ny >= r * r) return 0;\n const area = (signedQuadrantArea(x1, y1, r) - signedQuadrantArea(x0, y1, r)) - (signedQuadrantArea(x1, y0, r) - signedQuadrantArea(x0, y0, r));\n return area > 0 ? area : 0;\n}\n\n/**\n * The defocus disc of `diameterSites` (c / pitch) sampled on the site lattice, or null for a zero diameter (in focus:\n * callers apply no kernel). Fails with `InvalidScientificInputError` for a negative or non-finite diameter, and when\n * the kernel radius exceeds {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES}.\n */\nexport function createDefocusDiscSiteKernel(diameterSites: number): DefocusDiscSiteKernel | null {\n const radius = requireDefocusDiscKernelRadius(diameterSites);\n if (diameterSites === 0) return null;\n const r = diameterSites / 2, width = 2 * radius + 1, weights = new Float64Array(width * width);\n // One octant, mirrored into the eight symmetric cells.\n for (let i = 0; i <= radius; i++) for (let j = 0; j <= i; j++) {\n const a = cellArea(i, j, r);\n for (const [u, v] of [[i, j], [j, i]] as const) for (const su of u === 0 ? [1] : [1, -1]) for (const sv of v === 0 ? [1] : [1, -1])\n weights[(sv * v + radius) * width + su * u + radius] = a;\n }\n normalize(weights, (width * width - 1) / 2);\n return { diameterSites, radius, width, weights };\n}\n\n/**\n * Divides by the float64 sum, then sets the centre weight to 1 minus the exact sum of the others (accumulated as a\n * double-double with Knuth's TwoSum), rounded once. The exact real sum of the weights is then 1 within half a unit in\n * the last place of the centre weight. (No float64 summation order can promise exactly 1: a one-ulp change of one weight\n * can move a rounded sum from just below 1 to just above it.)\n */\nfunction normalize(weights: Float64Array, centre: number): void {\n let total = 0;\n for (let k = 0; k < weights.length; k++) total += weights[k]!;\n for (let k = 0; k < weights.length; k++) weights[k]! /= total;\n let hi = 0, lo = 0;\n for (let k = 0; k < weights.length; k++) {\n if (k === centre) continue;\n const w = weights[k]!, s = hi + w, b = s - hi;\n lo += (hi - (s - b)) + (w - b);\n hi = s;\n }\n // 1 − (hi + lo): TwoSum of 1 and −hi, then the low parts.\n const s = 1 - hi, b = s - 1, e = (1 - (s - b)) + (-hi - b);\n weights[centre] = s + (e - lo);\n}\n\n/** The geometric defocus disc of one request, in micrometres and in sites. */\nexport interface ResolvedDefocusDisc {\n /** Blur-circle diameter c at the sensor, µm (`calculateDefocusCircle`, thin-lens model 1.1.0). */\n readonly diameterMicrometers: number;\n /** c / pitch. */\n readonly diameterSites: number;\n /** Kernel radius R in sites (0 when c = 0). */\n readonly kernelRadiusSites: number;\n}\n\n/**\n * Resolves the defocus disc for a thin lens focused at `focusDistanceM` imaging a plane at `subjectDistanceM`, on a\n * lattice of `pitchMicrometers`. Exactly zero when the two distances are equal. It does not check the radius cap.\n */\nexport function resolveDefocusDisc(input: { focalLengthMm: number; aperture: number; focusDistanceM: number; subjectDistanceM: number;\n pitchMicrometers: number }): ResolvedDefocusDisc {\n const { pitchMicrometers, ...circle } = input;\n if (!Number.isFinite(pitchMicrometers) || pitchMicrometers <= 0) throw new InvalidScientificInputError(\"pitchMicrometers must be a positive finite number.\");\n const diameterMicrometers = calculateDefocusCircle(circle).value.diameterMm * 1000;\n const diameterSites = diameterMicrometers / pitchMicrometers;\n return { diameterMicrometers, diameterSites, kernelRadiusSites: defocusDiscKernelRadius(diameterSites) };\n}\n\n/**\n * The full 2-D convolution of two square site kernels (row-major, radii `radiusA` and `radiusB`): a kernel of radius\n * radiusA + radiusB with out(t) = Σ_s a(s)·b(t − s), each output summed over the taps of `a` in row-major order (zero taps\n * of either kernel skipped, which adds exact zeros only). Used for lens.defocus with lens.diffraction (#492): each spectral\n * node's diffraction kernel convolved with the defocus disc. That combination is an approximation: the defocused wave PSF\n * of a circular pupil is not exactly the in-focus Airy pattern convolved with the geometric disc. Both inputs here are\n * centred and non-negative, so the result is too; its sum is the product of the inputs' sums up to rounding.\n */\nexport function convolveSiteKernels(a: Float64Array, radiusA: number, b: Float64Array, radiusB: number): Float64Array {\n const wa = 2 * radiusA + 1, wb = 2 * radiusB + 1, radius = radiusA + radiusB, width = 2 * radius + 1;\n if (a.length !== wa * wa || b.length !== wb * wb) throw new InvalidScientificInputError(\"Site kernels must be (2R + 1)² taps.\");\n const out = new Float64Array(width * width);\n // Non-zero taps of b, as (row, column, weight).\n const bRows: number[] = [], bColumns: number[] = [], bWeights: number[] = [];\n for (let j = 0; j < wb; j++) for (let i = 0; i < wb; i++) { const w = b[j * wb + i]!; if (w !== 0) { bRows.push(j); bColumns.push(i); bWeights.push(w); } }\n for (let sj = 0; sj < wa; sj++) for (let si = 0; si < wa; si++) {\n const w = a[sj * wa + si]!;\n if (w === 0) continue;\n for (let k = 0; k < bWeights.length; k++) {\n const index = (sj + bRows[k]!) * width + si + bColumns[k]!;\n out[index] = out[index]! + w * bWeights[k]!;\n }\n }\n return out;\n}\n"]}
@@ -64,3 +64,14 @@ export declare function calculateIrradianceConvolution(input: IrradianceConvolut
64
64
  plan: IrradianceConvolutionPlan;
65
65
  values: Float64Array;
66
66
  };
67
+ /**
68
+ * Internal (#453; not exported from the package): runs several convolutions that share one geometry (width, height,
69
+ * kernel radius, patch size, boundary, pixel limit) with one validation of that geometry. The first call is
70
+ * `calculateIrradianceConvolution` itself, with every check. Each later call checks its own arrays exactly as the plan
71
+ * does (array types and lengths against the geometry, every sample, every kernel weight and kernel sum, with the same
72
+ * messages) and then runs the same gather (`convolveIrradianceRows`) with the first call's plan, whose geometry fields are
73
+ * the same numbers a full validation of this call would give. The values are therefore identical to one
74
+ * `calculateIrradianceConvolution` per call; only the repeated record and geometry checks are skipped.
75
+ * @internal
76
+ */
77
+ export declare function createIrradianceConvolutionRunner(geometry: Omit<IrradianceConvolutionInput, "samples" | "kernels">): (samples: Float32Array | Float64Array, kernels: Float32Array | Float64Array) => Float64Array;
@@ -49,6 +49,38 @@ function positiveInteger(value, maximum) {
49
49
  }
50
50
  return value;
51
51
  }
52
+ /** Checks every sample (finite, non-negative, within the declared magnitude) and returns the largest. */
53
+ function checkedMaximumSampleMagnitude(samples) {
54
+ let maximumSampleMagnitude = 0;
55
+ for (let i = 0; i < samples.length; i++) {
56
+ const v = samples[i];
57
+ // Negated comparison also rejects NaN.
58
+ if (!(v >= 0 && v <= IRRADIANCE_CONVOLUTION_LIMITS.maximumSampleMagnitude)) {
59
+ throw new InvalidConfigurationError("Irradiance samples must be finite, non-negative and within the declared magnitude.");
60
+ }
61
+ if (v > maximumSampleMagnitude)
62
+ maximumSampleMagnitude = v;
63
+ }
64
+ return maximumSampleMagnitude;
65
+ }
66
+ /** Checks every kernel weight (finite, in [0, 1]) and kernel sum (at most 1 plus slack) and returns the largest sum. */
67
+ function checkedMaximumKernelSum(kernels, patchCount, tapCount) {
68
+ let maximumKernelSum = 0;
69
+ for (let p = 0; p < patchCount; p++) {
70
+ let sum = 0;
71
+ for (let t = p * tapCount, end = t + tapCount; t < end; t++) {
72
+ const w = kernels[t];
73
+ if (!(w >= 0 && w <= 1))
74
+ throw new InvalidConfigurationError("PSF kernel weights must be finite and within [0, 1].");
75
+ sum += w;
76
+ }
77
+ if (sum > IRRADIANCE_CONVOLUTION_LIMITS.maximumKernelSum)
78
+ throw new InvalidConfigurationError("A PSF kernel may not create energy (sum exceeds 1).");
79
+ if (sum > maximumKernelSum)
80
+ maximumKernelSum = sum;
81
+ }
82
+ return maximumKernelSum;
83
+ }
52
84
  /** Validates the request and returns its plan. Every sample and weight is checked once. */
53
85
  export function calculateIrradianceConvolutionPlan(input) {
54
86
  const r = requireAllowlistedRecord(input, ["schemaVersion", "width", "height", "samples", "kernelRadius", "patchWidth",
@@ -76,30 +108,8 @@ export function calculateIrradianceConvolutionPlan(input) {
76
108
  !(kernels instanceof Float32Array || kernels instanceof Float64Array) || kernels.length !== patchCount * tapCount) {
77
109
  throw new InvalidConfigurationError("Irradiance convolution arrays do not match the declared geometry.");
78
110
  }
79
- let maximumSampleMagnitude = 0;
80
- for (let i = 0; i < samples.length; i++) {
81
- const v = samples[i];
82
- // Negated comparison also rejects NaN.
83
- if (!(v >= 0 && v <= L.maximumSampleMagnitude)) {
84
- throw new InvalidConfigurationError("Irradiance samples must be finite, non-negative and within the declared magnitude.");
85
- }
86
- if (v > maximumSampleMagnitude)
87
- maximumSampleMagnitude = v;
88
- }
89
- let maximumKernelSum = 0;
90
- for (let p = 0; p < patchCount; p++) {
91
- let sum = 0;
92
- for (let t = p * tapCount, end = t + tapCount; t < end; t++) {
93
- const w = kernels[t];
94
- if (!(w >= 0 && w <= 1))
95
- throw new InvalidConfigurationError("PSF kernel weights must be finite and within [0, 1].");
96
- sum += w;
97
- }
98
- if (sum > L.maximumKernelSum)
99
- throw new InvalidConfigurationError("A PSF kernel may not create energy (sum exceeds 1).");
100
- if (sum > maximumKernelSum)
101
- maximumKernelSum = sum;
102
- }
111
+ const maximumSampleMagnitude = checkedMaximumSampleMagnitude(samples);
112
+ const maximumKernelSum = checkedMaximumKernelSum(kernels, patchCount, tapCount);
103
113
  return Object.freeze({ width, height, kernelRadius: radius, kernelSize, tapCount, patchWidth, patchHeight, patchColumns, patchRows,
104
114
  maximumKernelSum, maximumSampleMagnitude });
105
115
  }
@@ -139,4 +149,34 @@ export function calculateIrradianceConvolution(input) {
139
149
  convolveIrradianceRows(plan, input.samples, input.kernels, 0, plan.height, values);
140
150
  return { plan, values };
141
151
  }
152
+ /**
153
+ * Internal (#453; not exported from the package): runs several convolutions that share one geometry (width, height,
154
+ * kernel radius, patch size, boundary, pixel limit) with one validation of that geometry. The first call is
155
+ * `calculateIrradianceConvolution` itself, with every check. Each later call checks its own arrays exactly as the plan
156
+ * does (array types and lengths against the geometry, every sample, every kernel weight and kernel sum, with the same
157
+ * messages) and then runs the same gather (`convolveIrradianceRows`) with the first call's plan, whose geometry fields are
158
+ * the same numbers a full validation of this call would give. The values are therefore identical to one
159
+ * `calculateIrradianceConvolution` per call; only the repeated record and geometry checks are skipped.
160
+ * @internal
161
+ */
162
+ export function createIrradianceConvolutionRunner(geometry) {
163
+ let plan;
164
+ return (samples, kernels) => {
165
+ if (plan === undefined) {
166
+ const first = calculateIrradianceConvolution({ ...geometry, samples, kernels });
167
+ plan = first.plan;
168
+ return first.values;
169
+ }
170
+ const patchCount = plan.patchColumns * plan.patchRows;
171
+ if (!(samples instanceof Float32Array || samples instanceof Float64Array) || samples.length !== plan.width * plan.height ||
172
+ !(kernels instanceof Float32Array || kernels instanceof Float64Array) || kernels.length !== patchCount * plan.tapCount) {
173
+ throw new InvalidConfigurationError("Irradiance convolution arrays do not match the declared geometry.");
174
+ }
175
+ checkedMaximumSampleMagnitude(samples);
176
+ checkedMaximumKernelSum(kernels, patchCount, plan.tapCount);
177
+ const values = new Float64Array(plan.width * plan.height);
178
+ convolveIrradianceRows(plan, samples, kernels, 0, plan.height, values);
179
+ return values;
180
+ };
181
+ }
142
182
  //# sourceMappingURL=irradiance-convolution.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"irradiance-convolution.js","sourceRoot":"","sources":["../../src/optics/irradiance-convolution.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Blurs a sampled image-plane quantity with caller-supplied PSF (point-spread function) kernels, one\n * kernel per patch of the image. The quantity must be linear and additive (for example irradiance).\n * Each patch is treated as shift-invariant (\"isoplanatic\"): the same PSF applies everywhere inside it.\n * The PSFs are incoherent intensity PSFs.\n *\n * This is the CPU float64 reference for the optics-as-convolution step (#277). The `webgpu-f32`\n * path runs the same equation; see docs/WEBGPU.md.\n *\n * Equation (indexed by source site; an energy-conserving scatter written as a gather):\n * out(x, y) = Σ_{v=-r..r} Σ_{u=-r..r} in(x − u, y − v) · K_{p(x − u, y − v)}(u, v)\n * where `p(s)` is the patch containing the **source** site and `K_p(u, v)` is the fraction of the source\n * site's quantity that lands at displacement (+u right, +v down), in grid sites.\n *\n * Incoherent imaging is a linear sum of intensity PSFs: the output intensity is the input intensity\n * convolved with the intensity PSF (MIT OpenCourseWare 2.71 Optics, Spring 2009, lecture 22, free).\n * A PSF that varies across the field is approximated as constant inside each patch, as in methods\n * that divide the image into isoplanatic subsections (van de Ketterij et al., arXiv:2011.01738,\n * sec. 1, free). Also see: Goodman, Introduction to Fourier Optics, 4th ed., 2017, §7.1. Camera\n * simulators such as ISETCam use the same piecewise shift-invariant method\n * (Farrell et al., Proc. SPIE 8299, 2012). The caller chooses the patch size and kernels, and so\n * owns the patch approximation.\n *\n * Edges: the grid holds every source. Sources outside the grid are not represented. Anything\n * scattered outside the grid is lost; compare `inputSum` with `outputSum` to see how much. There is no\n * padding, reflection or renormalization.\n */\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { requireAllowlistedRecord } from \"../core/record-validation.js\";\nimport { FULL_RESOLUTION_CEILING_PIXELS, resolveRaisedLimit } from \"../core/pixel-limits.js\";\n\nexport const IRRADIANCE_CONVOLUTION_SCHEMA_VERSION = \"0.1.0\" as const;\n/** `maximumPixels` is the default grid limit; a request may raise it with its own `maximumPixels`, up to `ceilingPixels`. */\nexport const IRRADIANCE_CONVOLUTION_LIMITS = Object.freeze({\n maximumPixels: 24_000_000,\n ceilingPixels: FULL_RESOLUTION_CEILING_PIXELS,\n maximumDimension: 16_384,\n maximumKernelRadius: 32,\n maximumPatchCount: 4_096,\n maximumKernelWeights: 4_194_304,\n /** A kernel may lose energy (vignetting, transmission) but may not create it, beyond rounding slack. */\n maximumKernelSum: 1 + 2 ** -20,\n /** Inputs larger than this are rejected so that float32 execution cannot overflow. */\n maximumSampleMagnitude: 2 ** 100\n});\n\nexport interface IrradianceConvolutionInput {\n schemaVersion: typeof IRRADIANCE_CONVOLUTION_SCHEMA_VERSION;\n width: number;\n height: number;\n /** Row-major samples. Finite, non-negative, linear and additive (for example irradiance, or expected photons per site). */\n samples: Float32Array | Float64Array;\n /** Kernel half-width in grid sites. Each kernel is (2r+1)×(2r+1). */\n kernelRadius: number;\n /** Patch size in grid sites. The last row and column of patches may be partial. */\n patchWidth: number;\n patchHeight: number;\n /**\n * Finite non-negative weights, `[patchRow][patchColumn][v + r][u + r]`, one kernel per patch in\n * row-major patch order. Each kernel sums to at most 1 (+2^-20 rounding slack).\n */\n kernels: Float32Array | Float64Array;\n boundary: \"zero-outside-grid\";\n /**\n * Optional opt-in grid limit for large images: an integer from 1 to `IRRADIANCE_CONVOLUTION_LIMITS.ceilingPixels`.\n * Default `IRRADIANCE_CONVOLUTION_LIMITS.maximumPixels` (24 000 000). It does not change the result.\n */\n maximumPixels?: number;\n}\n\n/** Validated, immutable geometry of a convolution request. Sample arrays are borrowed, not copied. */\nexport interface IrradianceConvolutionPlan {\n width: number;\n height: number;\n kernelRadius: number;\n kernelSize: number;\n tapCount: number;\n patchWidth: number;\n patchHeight: number;\n patchColumns: number;\n patchRows: number;\n /** Largest kernel sum and largest input magnitude. The float32 error bound uses them. */\n maximumKernelSum: number;\n maximumSampleMagnitude: number;\n}\n\nfunction positiveInteger(value: unknown, maximum: number): number {\n if (typeof value !== \"number\" || !Number.isSafeInteger(value) || value <= 0 || value > maximum) {\n throw new InvalidConfigurationError(\"Invalid irradiance convolution geometry.\");\n }\n return value;\n}\n\n/** Validates the request and returns its plan. Every sample and weight is checked once. */\nexport function calculateIrradianceConvolutionPlan(input: IrradianceConvolutionInput): IrradianceConvolutionPlan {\n const r = requireAllowlistedRecord(input, [\"schemaVersion\", \"width\", \"height\", \"samples\", \"kernelRadius\", \"patchWidth\",\n \"patchHeight\", \"kernels\", \"boundary\", \"maximumPixels\"], \"Invalid irradiance convolution input.\");\n const L = IRRADIANCE_CONVOLUTION_LIMITS;\n if (r.schemaVersion !== IRRADIANCE_CONVOLUTION_SCHEMA_VERSION || r.boundary !== \"zero-outside-grid\") {\n throw new InvalidConfigurationError(\"Unsupported irradiance convolution schema or boundary.\");\n }\n const width = positiveInteger(r.width, L.maximumDimension), height = positiveInteger(r.height, L.maximumDimension);\n const maximumPixels = resolveRaisedLimit(r.maximumPixels, L.maximumPixels, L.ceilingPixels,\n \"Irradiance convolution maximumPixels must be an integer from 1 to \" + L.ceilingPixels + \".\");\n if (width * height > maximumPixels) throw new InvalidConfigurationError(\"Irradiance convolution grid exceeds the pixel limit.\");\n const radius = r.kernelRadius;\n if (typeof radius !== \"number\" || !Number.isSafeInteger(radius) || radius < 0 || radius > L.maximumKernelRadius) {\n throw new InvalidConfigurationError(\"Invalid irradiance convolution kernel radius.\");\n }\n const patchWidth = positiveInteger(r.patchWidth, width), patchHeight = positiveInteger(r.patchHeight, height);\n const patchColumns = Math.ceil(width / patchWidth), patchRows = Math.ceil(height / patchHeight);\n const kernelSize = 2 * radius + 1, tapCount = kernelSize * kernelSize, patchCount = patchColumns * patchRows;\n if (patchCount > L.maximumPatchCount || patchCount * tapCount > L.maximumKernelWeights) {\n throw new InvalidConfigurationError(\"Irradiance convolution kernels exceed the declared limits.\");\n }\n const samples = r.samples, kernels = r.kernels;\n if (!(samples instanceof Float32Array || samples instanceof Float64Array) || samples.length !== width * height ||\n !(kernels instanceof Float32Array || kernels instanceof Float64Array) || kernels.length !== patchCount * tapCount) {\n throw new InvalidConfigurationError(\"Irradiance convolution arrays do not match the declared geometry.\");\n }\n let maximumSampleMagnitude = 0;\n for (let i = 0; i < samples.length; i++) {\n const v = samples[i]!;\n // Negated comparison also rejects NaN.\n if (!(v >= 0 && v <= L.maximumSampleMagnitude)) {\n throw new InvalidConfigurationError(\"Irradiance samples must be finite, non-negative and within the declared magnitude.\");\n }\n if (v > maximumSampleMagnitude) maximumSampleMagnitude = v;\n }\n let maximumKernelSum = 0;\n for (let p = 0; p < patchCount; p++) {\n let sum = 0;\n for (let t = p * tapCount, end = t + tapCount; t < end; t++) {\n const w = kernels[t]!;\n if (!(w >= 0 && w <= 1)) throw new InvalidConfigurationError(\"PSF kernel weights must be finite and within [0, 1].\");\n sum += w;\n }\n if (sum > L.maximumKernelSum) throw new InvalidConfigurationError(\"A PSF kernel may not create energy (sum exceeds 1).\");\n if (sum > maximumKernelSum) maximumKernelSum = sum;\n }\n return Object.freeze({ width, height, kernelRadius: radius, kernelSize, tapCount, patchWidth, patchHeight, patchColumns, patchRows,\n maximumKernelSum, maximumSampleMagnitude });\n}\n\n/**\n * Reference gather for output rows `[y0, y1)`, written into `output` at `(y - y0) * width + x`.\n * The summation order (v outer, u inner, both ascending) is part of the contract shared with the\n * WGSL (WebGPU shader) kernel.\n * @internal\n */\nexport function convolveIrradianceRows(plan: IrradianceConvolutionPlan, samples: Float32Array | Float64Array,\n kernels: Float32Array | Float64Array, y0: number, y1: number, output: Float64Array): void {\n const { width, height, kernelRadius: r, kernelSize, tapCount, patchWidth, patchHeight, patchColumns, patchRows } = plan;\n for (let y = y0; y < y1; y++) {\n for (let x = 0; x < width; x++) {\n let sum = 0;\n for (let v = -r; v <= r; v++) {\n const sy = y - v;\n if (sy < 0 || sy >= height) continue;\n const patchRowBase = Math.min(Math.floor(sy / patchHeight), patchRows - 1) * patchColumns;\n const kernelRow = (v + r) * kernelSize + r, sampleRow = sy * width;\n for (let u = -r; u <= r; u++) {\n const sx = x - u;\n if (sx < 0 || sx >= width) continue;\n const patch = patchRowBase + Math.min(Math.floor(sx / patchWidth), patchColumns - 1);\n sum += samples[sampleRow + sx]! * kernels[patch * tapCount + kernelRow + u]!;\n }\n }\n output[(y - y0) * width + x] = sum;\n }\n }\n}\n\n/** Synchronous float64 reference for bounded grids. Use the image-kernel accelerator for large grids. */\nexport function calculateIrradianceConvolution(input: IrradianceConvolutionInput): { plan: IrradianceConvolutionPlan; values: Float64Array } {\n const plan = calculateIrradianceConvolutionPlan(input);\n const values = new Float64Array(plan.width * plan.height);\n convolveIrradianceRows(plan, input.samples, input.kernels, 0, plan.height, values);\n return { plan, values };\n}\n"]}
1
+ {"version":3,"file":"irradiance-convolution.js","sourceRoot":"","sources":["../../src/optics/irradiance-convolution.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Blurs a sampled image-plane quantity with caller-supplied PSF (point-spread function) kernels, one\n * kernel per patch of the image. The quantity must be linear and additive (for example irradiance).\n * Each patch is treated as shift-invariant (\"isoplanatic\"): the same PSF applies everywhere inside it.\n * The PSFs are incoherent intensity PSFs.\n *\n * This is the CPU float64 reference for the optics-as-convolution step (#277). The `webgpu-f32`\n * path runs the same equation; see docs/WEBGPU.md.\n *\n * Equation (indexed by source site; an energy-conserving scatter written as a gather):\n * out(x, y) = Σ_{v=-r..r} Σ_{u=-r..r} in(x − u, y − v) · K_{p(x − u, y − v)}(u, v)\n * where `p(s)` is the patch containing the **source** site and `K_p(u, v)` is the fraction of the source\n * site's quantity that lands at displacement (+u right, +v down), in grid sites.\n *\n * Incoherent imaging is a linear sum of intensity PSFs: the output intensity is the input intensity\n * convolved with the intensity PSF (MIT OpenCourseWare 2.71 Optics, Spring 2009, lecture 22, free).\n * A PSF that varies across the field is approximated as constant inside each patch, as in methods\n * that divide the image into isoplanatic subsections (van de Ketterij et al., arXiv:2011.01738,\n * sec. 1, free). Also see: Goodman, Introduction to Fourier Optics, 4th ed., 2017, §7.1. Camera\n * simulators such as ISETCam use the same piecewise shift-invariant method\n * (Farrell et al., Proc. SPIE 8299, 2012). The caller chooses the patch size and kernels, and so\n * owns the patch approximation.\n *\n * Edges: the grid holds every source. Sources outside the grid are not represented. Anything\n * scattered outside the grid is lost; compare `inputSum` with `outputSum` to see how much. There is no\n * padding, reflection or renormalization.\n */\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { requireAllowlistedRecord } from \"../core/record-validation.js\";\nimport { FULL_RESOLUTION_CEILING_PIXELS, resolveRaisedLimit } from \"../core/pixel-limits.js\";\n\nexport const IRRADIANCE_CONVOLUTION_SCHEMA_VERSION = \"0.1.0\" as const;\n/** `maximumPixels` is the default grid limit; a request may raise it with its own `maximumPixels`, up to `ceilingPixels`. */\nexport const IRRADIANCE_CONVOLUTION_LIMITS = Object.freeze({\n maximumPixels: 24_000_000,\n ceilingPixels: FULL_RESOLUTION_CEILING_PIXELS,\n maximumDimension: 16_384,\n maximumKernelRadius: 32,\n maximumPatchCount: 4_096,\n maximumKernelWeights: 4_194_304,\n /** A kernel may lose energy (vignetting, transmission) but may not create it, beyond rounding slack. */\n maximumKernelSum: 1 + 2 ** -20,\n /** Inputs larger than this are rejected so that float32 execution cannot overflow. */\n maximumSampleMagnitude: 2 ** 100\n});\n\nexport interface IrradianceConvolutionInput {\n schemaVersion: typeof IRRADIANCE_CONVOLUTION_SCHEMA_VERSION;\n width: number;\n height: number;\n /** Row-major samples. Finite, non-negative, linear and additive (for example irradiance, or expected photons per site). */\n samples: Float32Array | Float64Array;\n /** Kernel half-width in grid sites. Each kernel is (2r+1)×(2r+1). */\n kernelRadius: number;\n /** Patch size in grid sites. The last row and column of patches may be partial. */\n patchWidth: number;\n patchHeight: number;\n /**\n * Finite non-negative weights, `[patchRow][patchColumn][v + r][u + r]`, one kernel per patch in\n * row-major patch order. Each kernel sums to at most 1 (+2^-20 rounding slack).\n */\n kernels: Float32Array | Float64Array;\n boundary: \"zero-outside-grid\";\n /**\n * Optional opt-in grid limit for large images: an integer from 1 to `IRRADIANCE_CONVOLUTION_LIMITS.ceilingPixels`.\n * Default `IRRADIANCE_CONVOLUTION_LIMITS.maximumPixels` (24 000 000). It does not change the result.\n */\n maximumPixels?: number;\n}\n\n/** Validated, immutable geometry of a convolution request. Sample arrays are borrowed, not copied. */\nexport interface IrradianceConvolutionPlan {\n width: number;\n height: number;\n kernelRadius: number;\n kernelSize: number;\n tapCount: number;\n patchWidth: number;\n patchHeight: number;\n patchColumns: number;\n patchRows: number;\n /** Largest kernel sum and largest input magnitude. The float32 error bound uses them. */\n maximumKernelSum: number;\n maximumSampleMagnitude: number;\n}\n\nfunction positiveInteger(value: unknown, maximum: number): number {\n if (typeof value !== \"number\" || !Number.isSafeInteger(value) || value <= 0 || value > maximum) {\n throw new InvalidConfigurationError(\"Invalid irradiance convolution geometry.\");\n }\n return value;\n}\n\n/** Checks every sample (finite, non-negative, within the declared magnitude) and returns the largest. */\nfunction checkedMaximumSampleMagnitude(samples: Float32Array | Float64Array): number {\n let maximumSampleMagnitude = 0;\n for (let i = 0; i < samples.length; i++) {\n const v = samples[i]!;\n // Negated comparison also rejects NaN.\n if (!(v >= 0 && v <= IRRADIANCE_CONVOLUTION_LIMITS.maximumSampleMagnitude)) {\n throw new InvalidConfigurationError(\"Irradiance samples must be finite, non-negative and within the declared magnitude.\");\n }\n if (v > maximumSampleMagnitude) maximumSampleMagnitude = v;\n }\n return maximumSampleMagnitude;\n}\n\n/** Checks every kernel weight (finite, in [0, 1]) and kernel sum (at most 1 plus slack) and returns the largest sum. */\nfunction checkedMaximumKernelSum(kernels: Float32Array | Float64Array, patchCount: number, tapCount: number): number {\n let maximumKernelSum = 0;\n for (let p = 0; p < patchCount; p++) {\n let sum = 0;\n for (let t = p * tapCount, end = t + tapCount; t < end; t++) {\n const w = kernels[t]!;\n if (!(w >= 0 && w <= 1)) throw new InvalidConfigurationError(\"PSF kernel weights must be finite and within [0, 1].\");\n sum += w;\n }\n if (sum > IRRADIANCE_CONVOLUTION_LIMITS.maximumKernelSum) throw new InvalidConfigurationError(\"A PSF kernel may not create energy (sum exceeds 1).\");\n if (sum > maximumKernelSum) maximumKernelSum = sum;\n }\n return maximumKernelSum;\n}\n\n/** Validates the request and returns its plan. Every sample and weight is checked once. */\nexport function calculateIrradianceConvolutionPlan(input: IrradianceConvolutionInput): IrradianceConvolutionPlan {\n const r = requireAllowlistedRecord(input, [\"schemaVersion\", \"width\", \"height\", \"samples\", \"kernelRadius\", \"patchWidth\",\n \"patchHeight\", \"kernels\", \"boundary\", \"maximumPixels\"], \"Invalid irradiance convolution input.\");\n const L = IRRADIANCE_CONVOLUTION_LIMITS;\n if (r.schemaVersion !== IRRADIANCE_CONVOLUTION_SCHEMA_VERSION || r.boundary !== \"zero-outside-grid\") {\n throw new InvalidConfigurationError(\"Unsupported irradiance convolution schema or boundary.\");\n }\n const width = positiveInteger(r.width, L.maximumDimension), height = positiveInteger(r.height, L.maximumDimension);\n const maximumPixels = resolveRaisedLimit(r.maximumPixels, L.maximumPixels, L.ceilingPixels,\n \"Irradiance convolution maximumPixels must be an integer from 1 to \" + L.ceilingPixels + \".\");\n if (width * height > maximumPixels) throw new InvalidConfigurationError(\"Irradiance convolution grid exceeds the pixel limit.\");\n const radius = r.kernelRadius;\n if (typeof radius !== \"number\" || !Number.isSafeInteger(radius) || radius < 0 || radius > L.maximumKernelRadius) {\n throw new InvalidConfigurationError(\"Invalid irradiance convolution kernel radius.\");\n }\n const patchWidth = positiveInteger(r.patchWidth, width), patchHeight = positiveInteger(r.patchHeight, height);\n const patchColumns = Math.ceil(width / patchWidth), patchRows = Math.ceil(height / patchHeight);\n const kernelSize = 2 * radius + 1, tapCount = kernelSize * kernelSize, patchCount = patchColumns * patchRows;\n if (patchCount > L.maximumPatchCount || patchCount * tapCount > L.maximumKernelWeights) {\n throw new InvalidConfigurationError(\"Irradiance convolution kernels exceed the declared limits.\");\n }\n const samples = r.samples, kernels = r.kernels;\n if (!(samples instanceof Float32Array || samples instanceof Float64Array) || samples.length !== width * height ||\n !(kernels instanceof Float32Array || kernels instanceof Float64Array) || kernels.length !== patchCount * tapCount) {\n throw new InvalidConfigurationError(\"Irradiance convolution arrays do not match the declared geometry.\");\n }\n const maximumSampleMagnitude = checkedMaximumSampleMagnitude(samples);\n const maximumKernelSum = checkedMaximumKernelSum(kernels, patchCount, tapCount);\n return Object.freeze({ width, height, kernelRadius: radius, kernelSize, tapCount, patchWidth, patchHeight, patchColumns, patchRows,\n maximumKernelSum, maximumSampleMagnitude });\n}\n\n/**\n * Reference gather for output rows `[y0, y1)`, written into `output` at `(y - y0) * width + x`.\n * The summation order (v outer, u inner, both ascending) is part of the contract shared with the\n * WGSL (WebGPU shader) kernel.\n * @internal\n */\nexport function convolveIrradianceRows(plan: IrradianceConvolutionPlan, samples: Float32Array | Float64Array,\n kernels: Float32Array | Float64Array, y0: number, y1: number, output: Float64Array): void {\n const { width, height, kernelRadius: r, kernelSize, tapCount, patchWidth, patchHeight, patchColumns, patchRows } = plan;\n for (let y = y0; y < y1; y++) {\n for (let x = 0; x < width; x++) {\n let sum = 0;\n for (let v = -r; v <= r; v++) {\n const sy = y - v;\n if (sy < 0 || sy >= height) continue;\n const patchRowBase = Math.min(Math.floor(sy / patchHeight), patchRows - 1) * patchColumns;\n const kernelRow = (v + r) * kernelSize + r, sampleRow = sy * width;\n for (let u = -r; u <= r; u++) {\n const sx = x - u;\n if (sx < 0 || sx >= width) continue;\n const patch = patchRowBase + Math.min(Math.floor(sx / patchWidth), patchColumns - 1);\n sum += samples[sampleRow + sx]! * kernels[patch * tapCount + kernelRow + u]!;\n }\n }\n output[(y - y0) * width + x] = sum;\n }\n }\n}\n\n/** Synchronous float64 reference for bounded grids. Use the image-kernel accelerator for large grids. */\nexport function calculateIrradianceConvolution(input: IrradianceConvolutionInput): { plan: IrradianceConvolutionPlan; values: Float64Array } {\n const plan = calculateIrradianceConvolutionPlan(input);\n const values = new Float64Array(plan.width * plan.height);\n convolveIrradianceRows(plan, input.samples, input.kernels, 0, plan.height, values);\n return { plan, values };\n}\n\n/**\n * Internal (#453; not exported from the package): runs several convolutions that share one geometry (width, height,\n * kernel radius, patch size, boundary, pixel limit) with one validation of that geometry. The first call is\n * `calculateIrradianceConvolution` itself, with every check. Each later call checks its own arrays exactly as the plan\n * does (array types and lengths against the geometry, every sample, every kernel weight and kernel sum, with the same\n * messages) and then runs the same gather (`convolveIrradianceRows`) with the first call's plan, whose geometry fields are\n * the same numbers a full validation of this call would give. The values are therefore identical to one\n * `calculateIrradianceConvolution` per call; only the repeated record and geometry checks are skipped.\n * @internal\n */\nexport function createIrradianceConvolutionRunner(geometry: Omit<IrradianceConvolutionInput, \"samples\" | \"kernels\">):\n (samples: Float32Array | Float64Array, kernels: Float32Array | Float64Array) => Float64Array {\n let plan: IrradianceConvolutionPlan | undefined;\n return (samples, kernels) => {\n if (plan === undefined) {\n const first = calculateIrradianceConvolution({ ...geometry, samples, kernels });\n plan = first.plan;\n return first.values;\n }\n const patchCount = plan.patchColumns * plan.patchRows;\n if (!(samples instanceof Float32Array || samples instanceof Float64Array) || samples.length !== plan.width * plan.height ||\n !(kernels instanceof Float32Array || kernels instanceof Float64Array) || kernels.length !== patchCount * plan.tapCount) {\n throw new InvalidConfigurationError(\"Irradiance convolution arrays do not match the declared geometry.\");\n }\n checkedMaximumSampleMagnitude(samples);\n checkedMaximumKernelSum(kernels, patchCount, plan.tapCount);\n const values = new Float64Array(plan.width * plan.height);\n convolveIrradianceRows(plan, samples, kernels, 0, plan.height, values);\n return values;\n };\n}\n"]}
@@ -212,6 +212,19 @@ export declare function parseSceneToSensorIrradianceProfile(value: unknown): Sce
212
212
  * factor 1/(1 + m)² from it, so the two cannot drift apart.
213
213
  */
214
214
  export declare function thinLensWorkingFNumberFactor(focalLengthMm: number, objectDistanceM: number): number;
215
+ /**
216
+ * Internal (#453; not exported from the package): the sensor-plane spectral irradiance that
217
+ * {@link calculateSceneRadianceToSensorIrradiance} returns, for repeated queries of one optical state. This call runs the
218
+ * request-independent checks once (the public function's own code, in its order); each call of the returned function then
219
+ * runs the public function's per-query steps on an engine-built request and a result already parsed by the caller: the identity
220
+ * match, the wavelength-basis check, the transmission or T-stop path, the front-of-lens filters, the field throughput and
221
+ * the finite, non-negative irradiance, with the same operations, order and errors. It skips only the re-validation of the
222
+ * unchanged optical inputs, the re-parse of the request and result, and the result envelope.
223
+ */
224
+ export declare function prepareSceneRadianceToSensorIrradianceValue(optics: Pick<CalculateSceneRadianceToSensorIrradianceInput, "profile" | "focalLengthMm" | "nominalFNumber" | "focus" | "frontOfLensFilters">): (request: SceneRadianceEvaluationRequest, result: SceneRadianceEvaluationResult, imagePointMm: {
225
+ x: number;
226
+ y: number;
227
+ }, fieldThroughput: OpticalBridgeFieldThroughput) => number;
215
228
  /**
216
229
  * Applies the declared pupil acceptance and exactly one transmission or field-throughput path to
217
230
  * spectral scene radiance. Returns the irradiance before the sensor stack, with conservative
@@ -484,30 +484,7 @@ function validateSceneIdentity(request, result) {
484
484
  throw new InvalidScientificInputError("Scene-radiance request/result identity must match exactly.");
485
485
  }
486
486
  }
487
- /**
488
- * Applies the declared pupil acceptance and exactly one transmission or field-throughput path to
489
- * spectral scene radiance. Returns the irradiance before the sensor stack, with conservative
490
- * evidence.
491
- *
492
- * The optical bridge turns outgoing scene radiance (W/m²/sr/nm) into irradiance (W/m²/nm) arriving
493
- * at the front of the sensor stack.
494
- * - Circular paraxial acceptance is pi/(4*Nworking²). At finite focus with unit pupil magnification,
495
- * the working f-number is N*(1+m).
496
- * - Spectral transmission and an effective working T-stop are alternative paths; use one.
497
- * - Field throughput is applied exactly once.
498
- * - Sensor response and PSF (point-spread function) blur happen later in the pipeline.
499
- * @param input - CalculateSceneRadianceToSensorIrradianceInput. See that type for coordinates, units and profile binding.
500
- * @returns CalculationResult<
501
- SceneToSensorIrradianceResult
502
- >. The result states its shape and scientific status; a successful call does not imply calibration.
503
- *
504
- * @see docs/API_REFERENCE.md for the root export and exact type graph.
505
- */
506
- export function calculateSceneRadianceToSensorIrradiance(input) {
507
- requireInputKeys(input, "calculateSceneRadianceToSensorIrradiance", ["sceneRadianceRequest", "sceneRadianceResult", "profile", "focalLengthMm", "nominalFNumber", "focus", "imagePointMm", "fieldThroughput", "frontOfLensFilters"]);
508
- const request = parseSceneRadianceEvaluationRequest(input.sceneRadianceRequest);
509
- const sceneResult = parseSceneRadianceEvaluationResult(input.sceneRadianceResult);
510
- validateSceneIdentity(request, sceneResult);
487
+ function prepareSceneRadianceToSensorIrradiance(input) {
511
488
  const profile = parseSceneToSensorIrradianceProfile(input.profile);
512
489
  const focalLengthMm = requirePositiveFiniteScientific(input.focalLengthMm, "focalLengthMm");
513
490
  const nominalFNumber = requirePositiveFiniteScientific(input.nominalFNumber, "nominalFNumber");
@@ -535,6 +512,15 @@ export function calculateSceneRadianceToSensorIrradiance(input) {
535
512
  throw new InvalidScientificInputError("Paraxial geometric acceptance must remain finite and greater than zero.");
536
513
  }
537
514
  const transmission = profile.transmission;
515
+ return { profile, focalLengthMm, nominalFNumber, working, workingFNumberValue, geometricAcceptance, transmission };
516
+ }
517
+ /**
518
+ * The per-query part of {@link calculateSceneRadianceToSensorIrradiance}, after the request and result are parsed and their
519
+ * identities matched: the wavelength-basis check, the transmission or T-stop path, the front-of-lens filters, the field
520
+ * throughput and the irradiance, with the same operations in the same order.
521
+ */
522
+ function applySceneRadianceToSensorIrradiance(prepared, input, request, sceneResult) {
523
+ const { workingFNumberValue, geometricAcceptance, transmission } = prepared;
538
524
  if (request.wavelengthBasis !==
539
525
  transmission.wavelengthBasis) {
540
526
  throw new InvalidScientificInputError("Scene-radiance and optical-transmission wavelength bases must match; implicit air/vacuum conversion is not performed.");
@@ -616,6 +602,54 @@ export function calculateSceneRadianceToSensorIrradiance(input) {
616
602
  irradiance < 0) {
617
603
  throw new InvalidScientificInputError("Sensor-plane spectral irradiance must remain finite and non-negative.");
618
604
  }
605
+ return { spectralTransmissionFactor, effectiveWorkingTStop, tStopEquivalentTransmissionFactor, effectiveAcceptance, transmissionEvidence,
606
+ frontOfLensFilterCount, frontOfLensFilterPolarizationModeled, frontOfLensFilterTransmissionFactor, frontOfLensFilterAttenuationStops,
607
+ frontOfLensFilterEvidence, frontOfLensFilterLimitations, fieldThroughputFactor, sceneRadiance, irradiance };
608
+ }
609
+ /**
610
+ * Internal (#453; not exported from the package): the sensor-plane spectral irradiance that
611
+ * {@link calculateSceneRadianceToSensorIrradiance} returns, for repeated queries of one optical state. This call runs the
612
+ * request-independent checks once (the public function's own code, in its order); each call of the returned function then
613
+ * runs the public function's per-query steps on an engine-built request and a result already parsed by the caller: the identity
614
+ * match, the wavelength-basis check, the transmission or T-stop path, the front-of-lens filters, the field throughput and
615
+ * the finite, non-negative irradiance, with the same operations, order and errors. It skips only the re-validation of the
616
+ * unchanged optical inputs, the re-parse of the request and result, and the result envelope.
617
+ */
618
+ export function prepareSceneRadianceToSensorIrradianceValue(optics) {
619
+ const prepared = prepareSceneRadianceToSensorIrradiance(optics);
620
+ const frontOfLensFilters = optics.frontOfLensFilters;
621
+ return (request, result, imagePointMm, fieldThroughput) => {
622
+ validateSceneIdentity(request, result);
623
+ return applySceneRadianceToSensorIrradiance(prepared, { imagePointMm, fieldThroughput, frontOfLensFilters }, request, result).irradiance;
624
+ };
625
+ }
626
+ /**
627
+ * Applies the declared pupil acceptance and exactly one transmission or field-throughput path to
628
+ * spectral scene radiance. Returns the irradiance before the sensor stack, with conservative
629
+ * evidence.
630
+ *
631
+ * The optical bridge turns outgoing scene radiance (W/m²/sr/nm) into irradiance (W/m²/nm) arriving
632
+ * at the front of the sensor stack.
633
+ * - Circular paraxial acceptance is pi/(4*Nworking²). At finite focus with unit pupil magnification,
634
+ * the working f-number is N*(1+m).
635
+ * - Spectral transmission and an effective working T-stop are alternative paths; use one.
636
+ * - Field throughput is applied exactly once.
637
+ * - Sensor response and PSF (point-spread function) blur happen later in the pipeline.
638
+ * @param input - CalculateSceneRadianceToSensorIrradianceInput. See that type for coordinates, units and profile binding.
639
+ * @returns CalculationResult<
640
+ SceneToSensorIrradianceResult
641
+ >. The result states its shape and scientific status; a successful call does not imply calibration.
642
+ *
643
+ * @see docs/API_REFERENCE.md for the root export and exact type graph.
644
+ */
645
+ export function calculateSceneRadianceToSensorIrradiance(input) {
646
+ requireInputKeys(input, "calculateSceneRadianceToSensorIrradiance", ["sceneRadianceRequest", "sceneRadianceResult", "profile", "focalLengthMm", "nominalFNumber", "focus", "imagePointMm", "fieldThroughput", "frontOfLensFilters"]);
647
+ const request = parseSceneRadianceEvaluationRequest(input.sceneRadianceRequest);
648
+ const sceneResult = parseSceneRadianceEvaluationResult(input.sceneRadianceResult);
649
+ validateSceneIdentity(request, sceneResult);
650
+ const prepared = prepareSceneRadianceToSensorIrradiance(input);
651
+ const { profile, focalLengthMm, nominalFNumber, working, workingFNumberValue, geometricAcceptance, transmission } = prepared;
652
+ const { spectralTransmissionFactor, effectiveWorkingTStop, tStopEquivalentTransmissionFactor, effectiveAcceptance, transmissionEvidence, frontOfLensFilterCount, frontOfLensFilterPolarizationModeled, frontOfLensFilterTransmissionFactor, frontOfLensFilterAttenuationStops, frontOfLensFilterEvidence, frontOfLensFilterLimitations, fieldThroughputFactor, sceneRadiance, irradiance } = applySceneRadianceToSensorIrradiance(prepared, input, request, sceneResult);
619
653
  const limitations = [
620
654
  ...profile.limitations,
621
655
  ...frontOfLensFilterLimitations,