@photivra/engine 2.16.0 → 2.17.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.
Files changed (93) hide show
  1. package/CHANGELOG.md +56 -1
  2. package/CITATION.cff +1 -1
  3. package/README.md +6 -6
  4. package/SECURITY.md +1 -1
  5. package/THIRD_PARTY.md +24 -0
  6. package/dist/camera/lens-projection.d.ts +5 -0
  7. package/dist/camera/lens-projection.js +10 -0
  8. package/dist/camera/lens-projection.js.map +1 -1
  9. package/dist/core/version.d.ts +1 -1
  10. package/dist/core/version.js +1 -1
  11. package/dist/core/version.js.map +1 -1
  12. package/dist/equipment/generic-lens-catalogue.d.ts +227 -0
  13. package/dist/equipment/generic-lens-catalogue.js +243 -0
  14. package/dist/equipment/generic-lens-catalogue.js.map +1 -0
  15. package/dist/facade/defocus-bokeh-zones.d.ts +33 -0
  16. package/dist/facade/defocus-bokeh-zones.js +53 -0
  17. package/dist/facade/defocus-bokeh-zones.js.map +1 -0
  18. package/dist/facade/defocus-image-space-provider.d.ts +9 -0
  19. package/dist/facade/defocus-image-space-provider.js +96 -7
  20. package/dist/facade/defocus-image-space-provider.js.map +1 -1
  21. package/dist/facade/diffraction-provider.d.ts +29 -0
  22. package/dist/facade/diffraction-provider.js +117 -11
  23. package/dist/facade/diffraction-provider.js.map +1 -1
  24. package/dist/facade/display-view-gpu-session.js +4 -0
  25. package/dist/facade/display-view-gpu-session.js.map +1 -1
  26. package/dist/facade/display-view-gpu.d.ts +10 -0
  27. package/dist/facade/display-view-gpu.js.map +1 -1
  28. package/dist/facade/display-view-lens-signal.d.ts +30 -0
  29. package/dist/facade/display-view-lens-signal.js +427 -0
  30. package/dist/facade/display-view-lens-signal.js.map +1 -0
  31. package/dist/facade/expected-signal-setup.d.ts +7 -0
  32. package/dist/facade/expected-signal-setup.js +12 -2
  33. package/dist/facade/expected-signal-setup.js.map +1 -1
  34. package/dist/facade/expected-signal.d.ts +8 -0
  35. package/dist/facade/expected-signal.js +150 -0
  36. package/dist/facade/expected-signal.js.map +1 -1
  37. package/dist/facade/generic-lens-capture.d.ts +6 -0
  38. package/dist/facade/generic-lens-capture.js +8 -0
  39. package/dist/facade/generic-lens-capture.js.map +1 -0
  40. package/dist/facade/large-defocus-lattice.d.ts +39 -0
  41. package/dist/facade/large-defocus-lattice.js +64 -0
  42. package/dist/facade/large-defocus-lattice.js.map +1 -0
  43. package/dist/facade/scene-radiance.d.ts +5 -0
  44. package/dist/facade/scene-radiance.js +5 -2
  45. package/dist/facade/scene-radiance.js.map +1 -1
  46. package/dist/facade/simulate-capture.d.ts +38 -5
  47. package/dist/facade/simulate-capture.js +306 -49
  48. package/dist/facade/simulate-capture.js.map +1 -1
  49. package/dist/facade/simulate-display-view.js +22 -16
  50. package/dist/facade/simulate-display-view.js.map +1 -1
  51. package/dist/gpu/display-view-quality-gpu.d.ts +0 -9
  52. package/dist/gpu/display-view-quality-gpu.js +6 -0
  53. package/dist/gpu/display-view-quality-gpu.js.map +1 -1
  54. package/dist/gpu/display-view-speed-gpu.js +2 -0
  55. package/dist/gpu/display-view-speed-gpu.js.map +1 -1
  56. package/dist/index.d.ts +3 -0
  57. package/dist/index.js +3 -0
  58. package/dist/index.js.map +1 -1
  59. package/dist/optics/defocus-bokeh-kernel.d.ts +207 -0
  60. package/dist/optics/defocus-bokeh-kernel.js +380 -0
  61. package/dist/optics/defocus-bokeh-kernel.js.map +1 -0
  62. package/dist/optics/defocus-disc-kernel.d.ts +41 -19
  63. package/dist/optics/defocus-disc-kernel.js +44 -23
  64. package/dist/optics/defocus-disc-kernel.js.map +1 -1
  65. package/dist/optics/generic-lens-psf.d.ts +68 -0
  66. package/dist/optics/generic-lens-psf.js +337 -0
  67. package/dist/optics/generic-lens-psf.js.map +1 -0
  68. package/dist/optics/illumination-vignetting.d.ts +6 -0
  69. package/dist/optics/illumination-vignetting.js +24 -7
  70. package/dist/optics/illumination-vignetting.js.map +1 -1
  71. package/dist/optics/sensor-environment-query.d.ts +6 -0
  72. package/dist/optics/sensor-environment-query.js +13 -6
  73. package/dist/optics/sensor-environment-query.js.map +1 -1
  74. package/dist/optics/site-kernel-runs.d.ts +77 -0
  75. package/dist/optics/site-kernel-runs.js +200 -0
  76. package/dist/optics/site-kernel-runs.js.map +1 -0
  77. package/dist/output/generic-lens-corrections.d.ts +24 -0
  78. package/dist/output/generic-lens-corrections.js +51 -0
  79. package/dist/output/generic-lens-corrections.js.map +1 -0
  80. package/docs/AGENT_GUIDE.md +1 -0
  81. package/docs/API_REFERENCE.md +49 -1
  82. package/docs/QUICK_START.md +257 -17
  83. package/docs/USAGE.md +57 -5
  84. package/docs/api/core-version.md +1 -1
  85. package/docs/api/equipment-generic-lens-catalogue.md +281 -0
  86. package/docs/api/exports.json +137 -2
  87. package/docs/api/facade-simulate-capture.md +23 -2
  88. package/docs/api/optics-defocus-bokeh-kernel.md +145 -0
  89. package/docs/api/optics-defocus-disc-kernel.md +22 -15
  90. package/docs/api/optics-sensor-environment-query.md +6 -0
  91. package/docs/api/output-generic-lens-corrections.md +39 -0
  92. package/llms.txt +1 -0
  93. package/package.json +1 -1
@@ -0,0 +1,337 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * The PSF of a generic catalogue lens (#549) on a sensor's site lattice, as a field- and wavelength-dependent sampled PSF
4
+ * profile for the environment PSF quadrature (`simulateCapture` `lens.catalogue`).
5
+ *
6
+ * Model (an approximation): scalar Fraunhofer diffraction of a uniform circular pupil of diameter D = f/N carrying the
7
+ * lens's Seidel wavefront (`resolveGenericLens`; the expansion of `createWavefrontPupilGrid`, Hopkins 1950; Wyant & Creath
8
+ * 1992), propagated to the thin-lens image distance z (working f-number z/D; Goodman, Introduction to Fourier Optics, ch. 4-6).
9
+ * U(x) = Σ_pupil exp(i·2π·W(u)/λ)·exp(−i·2π·x·u/(λ·z)) is evaluated with a zero-padded radix-2 FFT (Cooley & Tukey 1965) on
10
+ * an image grid of pitch δ = min(λ·N_w/2, p/4) (p the site pitch), and |U|² is integrated over each site's square cell (exact
11
+ * overlap of each δ-square with the cell). Lateral colour displaces the PSF radially by Δr(λ). The kernel window is (2R + 1)²
12
+ * sites; the energy outside it is reported and put back by unit-sum renormalisation, as for `lens.diffraction`.
13
+ *
14
+ * Nodes: a 5 × 5 regular grid of field points (0, ±half the frame, ±the frame edge plus the kernel reach) × 5 wavelengths
15
+ * (380, 450, 550, 650, 780 nm), each computed at its own field azimuth (no rotation of a radial node), interpolated by the
16
+ * engine's centroid-aligned multilinear scheme. The quadrant x, y ≥ 0 is computed and mirrored (the Seidel PSF of a mirrored
17
+ * field point is the mirrored PSF).
18
+ *
19
+ * Kernel orientation: a kernel's row index increases with image +Y, the orientation the environment PSF quadrature applies
20
+ * (`sensorPsfSourcePoint`), so coma and lateral colour point away from the optical axis in the rendered image.
21
+ */
22
+ import { InvalidScientificInputError } from "../core/validation.js";
23
+ import { genericLensDispersion } from "../equipment/generic-lens-catalogue.js";
24
+ import { markEngineOwnedLensSampledPsfProfile } from "./lens-psf-profile.js";
25
+ import { convolveSiteKernels } from "./defocus-disc-kernel.js";
26
+ /** Wavelength nodes of a catalogue PSF profile, nm (they span the facade's 380–780 nm quadrature). */
27
+ export const GENERIC_LENS_PSF_WAVELENGTHS_NM = /* @__PURE__ */ Object.freeze([380, 450, 550, 650, 780]);
28
+ /** Largest kernel radius of the aberration PSF in sites (before any defocus disc is convolved in). */
29
+ export const GENERIC_LENS_PSF_MAXIMUM_KERNEL_RADIUS_SITES = 12;
30
+ /** Largest kernel radius in sites of the catalogue PSF with a defocus disc convolved in (`simulateCapture` refuses more: cost). */
31
+ export const GENERIC_LENS_PSF_MAXIMUM_COMBINED_RADIUS_SITES = 16;
32
+ /** Energy fraction a node may leave outside the chosen kernel window (it is put back by renormalisation and reported). */
33
+ export const GENERIC_LENS_PSF_WINDOW_TRUNCATION = 0.03;
34
+ /** Largest FFT size per axis. */
35
+ const MAXIMUM_TRANSFORM = 2048;
36
+ /** Largest optical-path step between neighbouring pupil samples, in waves (Nyquist is 0.5). */
37
+ const MAXIMUM_PHASE_STEP_WAVES = 0.35;
38
+ const DESIGN_WAVELENGTH_UM = 0.55;
39
+ /** In-place iterative radix-2 FFT, exp(−i·2π·k·m/n). */
40
+ function radix2Fft(re, im, n, cos, sin) {
41
+ for (let i = 1, j = 0; i < n; i++) {
42
+ let bit = n >> 1;
43
+ for (; j & bit; bit >>= 1)
44
+ j ^= bit;
45
+ j ^= bit;
46
+ if (i < j) {
47
+ let t = re[i];
48
+ re[i] = re[j];
49
+ re[j] = t;
50
+ t = im[i];
51
+ im[i] = im[j];
52
+ im[j] = t;
53
+ }
54
+ }
55
+ for (let size = 2; size <= n; size <<= 1) {
56
+ const half = size >> 1, step = n / size;
57
+ for (let start = 0; start < n; start += size)
58
+ for (let k = 0; k < half; k++) {
59
+ const c = cos[k * step], s = sin[k * step], a = start + k, b = a + half;
60
+ const tr = re[b] * c + im[b] * s, ti = im[b] * c - re[b] * s;
61
+ re[b] = re[a] - tr;
62
+ im[b] = im[a] - ti;
63
+ re[a] = re[a] + tr;
64
+ im[a] = im[a] + ti;
65
+ }
66
+ }
67
+ }
68
+ /** One axis's overlap of δ-squares centred at positions (s·δ + shift) with site cells [(i − ½)p, (i + ½)p], i ∈ [−R, R]. */
69
+ function overlaps(count, delta, shift, pitch, radius) {
70
+ const cell = new Int32Array(count).fill(-1), weight = new Float64Array(count), cell2 = new Int32Array(count).fill(-1), weight2 = new Float64Array(count);
71
+ for (let k = 0; k < count; k++) {
72
+ const signed = k < count / 2 ? k : k - count, centre = signed * delta + shift, lo = centre - delta / 2, hi = centre + delta / 2;
73
+ const first = Math.floor(lo / pitch + 0.5), last = Math.floor(hi / pitch + 0.5);
74
+ for (let i = first; i <= last && i <= first + 1; i++) {
75
+ if (i < -radius || i > radius)
76
+ continue;
77
+ const w = (Math.min(hi, (i + 0.5) * pitch) - Math.max(lo, (i - 0.5) * pitch)) / delta;
78
+ if (w <= 0)
79
+ continue;
80
+ if (cell[k] === -1) {
81
+ cell[k] = i + radius;
82
+ weight[k] = w;
83
+ }
84
+ else {
85
+ cell2[k] = i + radius;
86
+ weight2[k] = w;
87
+ }
88
+ }
89
+ }
90
+ return { cell, weight, cell2, weight2 };
91
+ }
92
+ /** Seidel OPD (µm) at normalised pupil (px, py) for a field point at height H and azimuth φ, at wavelength λ (µm). */
93
+ function opdMicrometers(lens, H, cosPhi, sinPhi, lambdaUm, px, py) {
94
+ const a = lens.character.aberrations, r2 = px * px + py * py, along = px * cosPhi + py * sinPhi;
95
+ const defocus = a.balancingDefocusWaves + a.axialColorWaves * genericLensDispersion(lambdaUm * 1000);
96
+ // Positive coma W131 sends the comet's flare away from the axis in the image (H ρ³ cos(θ − φ), with the FFT sign above).
97
+ return DESIGN_WAVELENGTH_UM * (defocus * r2 + a.sphericalWaves * r2 * r2 + a.comaWaves * H * r2 * along + a.astigmatismWaves * H * H * along * along + a.fieldCurvatureWaves * H * H * r2);
98
+ }
99
+ /** Largest transverse ray aberration 2·N_w·|∂W/∂ρ| (µm) over the pupil, from a 33 × 33 grid. */
100
+ function geometricRadiusMicrometers(lens, H, lambdaUm, workingF) {
101
+ let largest = 0;
102
+ const n = 33, h = 1e-4;
103
+ for (let j = 0; j < n; j++)
104
+ for (let i = 0; i < n; i++) {
105
+ const px = -1 + 2 * i / (n - 1), py = -1 + 2 * j / (n - 1);
106
+ if (px * px + py * py > 1)
107
+ continue;
108
+ const gx = (opdMicrometers(lens, H, 1, 0, lambdaUm, px + h, py) - opdMicrometers(lens, H, 1, 0, lambdaUm, px - h, py)) / (2 * h);
109
+ const gy = (opdMicrometers(lens, H, 1, 0, lambdaUm, px, py + h) - opdMicrometers(lens, H, 1, 0, lambdaUm, px, py - h)) / (2 * h);
110
+ largest = Math.max(largest, Math.hypot(gx, gy));
111
+ }
112
+ return 2 * workingF * largest;
113
+ }
114
+ function nodeKernel(lens, xMm, yMm, lambdaNm, workingF, pupilDiameterMm, zMm, pitch, radius) {
115
+ const lambdaUm = lambdaNm / 1000, r = Math.hypot(xMm, yMm), H = Math.min(1, r / lens.halfDiagonalMm);
116
+ const cosPhi = r > 0 ? xMm / r : 1, sinPhi = r > 0 ? yMm / r : 0;
117
+ // Lateral colour: radial displacement L·R·H·D(λ), µm.
118
+ const shift = lens.character.lateralColor.fraction * lens.halfDiagonalMm * 1000 * H * genericLensDispersion(lambdaNm);
119
+ const delta = Math.min(lambdaUm * workingF / 2, pitch / 4);
120
+ let size = 64;
121
+ // The FFT period must hold the window, or (when the PSF is much smaller than the window, for large pixels) the PSF itself:
122
+ // the geometric blur plus 16·λ·N_w of Airy rings (the energy beyond, about 1 %, folds back into the period).
123
+ const psfExtentUm = 2 * (geometricRadiusMicrometers(lens, H, lambdaUm, workingF) + 16 * lambdaUm * workingF);
124
+ while (size * delta < Math.min(2.5 * (radius + 0.5) * pitch, psfExtentUm) + 2 * Math.abs(shift))
125
+ size *= 2;
126
+ for (;;) {
127
+ if (size > MAXIMUM_TRANSFORM)
128
+ throw new InvalidScientificInputError("The catalogue lens PSF needs an FFT larger than " + MAXIMUM_TRANSFORM + " per axis.");
129
+ // Pupil pitch: δ · p_pupil = λ·z / size (mm).
130
+ const pupilPitchMm = lambdaUm / 1000 * zMm / (size * delta / 1000), half = pupilDiameterMm / 2, reach = Math.floor(half / pupilPitchMm);
131
+ const re = new Float64Array(size * size), im = new Float64Array(size * size), rowUsed = new Uint8Array(size);
132
+ let open = 0, phaseStep = 0;
133
+ const opd = new Float64Array((2 * reach + 1) * (2 * reach + 1)).fill(NaN);
134
+ for (let v = -reach; v <= reach; v++)
135
+ for (let u = -reach; u <= reach; u++) {
136
+ const px = u * pupilPitchMm / half, py = v * pupilPitchMm / half;
137
+ if (px * px + py * py > 1 + 1e-12)
138
+ continue;
139
+ const w = opdMicrometers(lens, H, cosPhi, sinPhi, lambdaUm, px, py), phase = 2 * Math.PI * w / lambdaUm;
140
+ opd[(v + reach) * (2 * reach + 1) + u + reach] = w;
141
+ const index = ((v + size) % size) * size + (u + size) % size;
142
+ re[index] = Math.cos(phase);
143
+ im[index] = Math.sin(phase);
144
+ rowUsed[(v + size) % size] = 1;
145
+ open++;
146
+ }
147
+ const width = 2 * reach + 1;
148
+ for (let j = 0; j < width; j++)
149
+ for (let i = 0; i < width; i++) {
150
+ const w = opd[j * width + i];
151
+ if (Number.isNaN(w))
152
+ continue;
153
+ if (i + 1 < width && !Number.isNaN(opd[j * width + i + 1]))
154
+ phaseStep = Math.max(phaseStep, Math.abs(opd[j * width + i + 1] - w) / lambdaUm);
155
+ if (j + 1 < width && !Number.isNaN(opd[(j + 1) * width + i]))
156
+ phaseStep = Math.max(phaseStep, Math.abs(opd[(j + 1) * width + i] - w) / lambdaUm);
157
+ }
158
+ if (open < 9)
159
+ throw new InvalidScientificInputError("The catalogue lens PSF pupil has fewer than 9 samples.");
160
+ if (phaseStep > MAXIMUM_PHASE_STEP_WAVES) {
161
+ size *= 2;
162
+ continue;
163
+ }
164
+ const cos = new Float64Array(size / 2), sin = new Float64Array(size / 2);
165
+ for (let k = 0; k < size / 2; k++) {
166
+ cos[k] = Math.cos(2 * Math.PI * k / size);
167
+ sin[k] = Math.sin(2 * Math.PI * k / size);
168
+ }
169
+ const rowRe = new Float64Array(size), rowIm = new Float64Array(size);
170
+ for (let y = 0; y < size; y++) {
171
+ if (!rowUsed[y])
172
+ continue;
173
+ rowRe.set(re.subarray(y * size, (y + 1) * size));
174
+ rowIm.set(im.subarray(y * size, (y + 1) * size));
175
+ radix2Fft(rowRe, rowIm, size, cos, sin);
176
+ re.set(rowRe, y * size);
177
+ im.set(rowIm, y * size);
178
+ }
179
+ const colRe = new Float64Array(size), colIm = new Float64Array(size);
180
+ const ox = overlaps(size, delta, shift * cosPhi, pitch, radius), oy = overlaps(size, delta, shift * sinPhi, pitch, radius);
181
+ const w2 = 2 * radius + 1, kernel = new Float64Array(w2 * w2);
182
+ let total = 0, inside = 0;
183
+ for (let x = 0; x < size; x++) {
184
+ for (let y = 0; y < size; y++) {
185
+ colRe[y] = re[y * size + x];
186
+ colIm[y] = im[y * size + x];
187
+ }
188
+ radix2Fft(colRe, colIm, size, cos, sin);
189
+ const cx = ox.cell[x], cx2 = ox.cell2[x], wx = ox.weight[x], wx2 = ox.weight2[x];
190
+ for (let y = 0; y < size; y++) {
191
+ const intensity = colRe[y] * colRe[y] + colIm[y] * colIm[y];
192
+ total += intensity;
193
+ if (cx < 0)
194
+ continue;
195
+ const cy = oy.cell[y];
196
+ if (cy < 0)
197
+ continue;
198
+ const cy2 = oy.cell2[y], wy = oy.weight[y], wy2 = oy.weight2[y];
199
+ // Row index increases with image +Y (the quadrature's orientation).
200
+ kernel[cy * w2 + cx] += intensity * wx * wy;
201
+ inside += intensity * wx * wy;
202
+ if (cx2 >= 0) {
203
+ kernel[cy * w2 + cx2] += intensity * wx2 * wy;
204
+ inside += intensity * wx2 * wy;
205
+ }
206
+ if (cy2 >= 0) {
207
+ kernel[cy2 * w2 + cx] += intensity * wx * wy2;
208
+ inside += intensity * wx * wy2;
209
+ if (cx2 >= 0) {
210
+ kernel[cy2 * w2 + cx2] += intensity * wx2 * wy2;
211
+ inside += intensity * wx2 * wy2;
212
+ }
213
+ }
214
+ }
215
+ }
216
+ if (!(total > 0) || !(inside > 0))
217
+ throw new InvalidScientificInputError("The catalogue lens PSF has no energy in its window.");
218
+ return { kernel, total, transform: size, phaseStep };
219
+ }
220
+ }
221
+ function mirror(kernel, radius, flipX, flipY) {
222
+ const w = 2 * radius + 1, out = new Float64Array(kernel.length);
223
+ for (let j = 0; j < w; j++)
224
+ for (let i = 0; i < w; i++)
225
+ out[(flipY ? w - 1 - j : j) * w + (flipX ? w - 1 - i : i)] = kernel[j * w + i];
226
+ return out;
227
+ }
228
+ /** Thin-lens image distance z (mm), pupil diameter D = f/N (mm) and working f-number z/D at a finite focus. */
229
+ function pupilGeometry(lens, focusDistanceM) {
230
+ const f = lens.focalLengthMm, d = focusDistanceM * 1000;
231
+ if (!(d > f))
232
+ throw new InvalidScientificInputError("The focus distance must exceed the focal length.");
233
+ const z = 1 / (1 / f - 1 / d), D = f / lens.character.fNumber;
234
+ return { z, D, workingF: z / D };
235
+ }
236
+ /**
237
+ * An upper bound of the aberration PSF's kernel radius in sites (the window is then narrowed to keep at most
238
+ * {@link GENERIC_LENS_PSF_WINDOW_TRUNCATION} of every node's energy outside): the largest transverse ray aberration 2·N_w·|∂W/∂ρ| over the pupil at the frame
239
+ * corner and every wavelength node, plus the lateral-colour shift, plus the Airy third dark ring 3.24·λ·N_w at 780 nm, rounded up
240
+ * to whole sites (at least 1, at most {@link GENERIC_LENS_PSF_MAXIMUM_KERNEL_RADIUS_SITES}).
241
+ */
242
+ export function genericLensPsfKernelRadius(input) {
243
+ const { lens, sitePitchMicrometers: pitch } = input, { workingF } = pupilGeometry(lens, input.focusDistanceM);
244
+ const longest = GENERIC_LENS_PSF_WAVELENGTHS_NM[GENERIC_LENS_PSF_WAVELENGTHS_NM.length - 1] / 1000;
245
+ let reachUm = 0;
246
+ for (const nm of GENERIC_LENS_PSF_WAVELENGTHS_NM) {
247
+ const shift = Math.abs(lens.character.lateralColor.fraction * lens.halfDiagonalMm * 1000 * genericLensDispersion(nm));
248
+ reachUm = Math.max(reachUm, geometricRadiusMicrometers(lens, 1, nm / 1000, workingF) + shift);
249
+ }
250
+ reachUm += 3.24 * longest * workingF;
251
+ return Math.min(GENERIC_LENS_PSF_MAXIMUM_KERNEL_RADIUS_SITES, Math.max(1, Math.ceil(reachUm / pitch - 0.5)));
252
+ }
253
+ /**
254
+ * Builds the catalogue lens's sampled PSF profile for the environment PSF quadrature: 5 × 5 field nodes × 5 wavelengths, each
255
+ * kernel on the site lattice. Engine-owned (parsed once). Deterministic: the same input gives the same profile.
256
+ */
257
+ export function createGenericLensPsfProfile(input) {
258
+ const { lens, sitePitchMicrometers: pitch } = input;
259
+ const f = lens.focalLengthMm, N = lens.character.fNumber, { z, D, workingF } = pupilGeometry(lens, input.focusDistanceM);
260
+ const ex = input.fieldHalfExtentMm, fh = input.frameHalfMm;
261
+ if (!(ex.x > fh.x / 2 && ex.y > fh.y / 2 && fh.x > 0 && fh.y > 0))
262
+ throw new InvalidScientificInputError("The PSF field extent must exceed half the frame.");
263
+ const xs = [0, fh.x / 2, ex.x], ys = [0, fh.y / 2, ex.y];
264
+ const bound = genericLensPsfKernelRadius({ lens, focusDistanceM: input.focusDistanceM, sitePitchMicrometers: pitch });
265
+ const quadrant = new Map();
266
+ let minimumTransform = Infinity, maximumTransform = 0, phaseStep = 0;
267
+ for (const x of xs)
268
+ for (const y of ys)
269
+ for (const nm of GENERIC_LENS_PSF_WAVELENGTHS_NM) {
270
+ const node = nodeKernel(lens, x, y, nm, workingF, D, z, pitch, bound);
271
+ minimumTransform = Math.min(minimumTransform, node.transform);
272
+ maximumTransform = Math.max(maximumTransform, node.transform);
273
+ phaseStep = Math.max(phaseStep, node.phaseStep);
274
+ quadrant.set(x + ":" + y + ":" + nm, node);
275
+ }
276
+ // The window: the smallest radius that keeps at most 1 % of every node's energy outside it (at most the bound).
277
+ const outside = (r) => {
278
+ let worst = 0;
279
+ for (const node of quadrant.values()) {
280
+ let inside = 0;
281
+ for (let j = bound - r; j <= bound + r; j++)
282
+ for (let i = bound - r; i <= bound + r; i++)
283
+ inside += node.kernel[j * (2 * bound + 1) + i];
284
+ worst = Math.max(worst, 1 - inside / node.total);
285
+ }
286
+ return worst;
287
+ };
288
+ let radius = 1;
289
+ while (radius < bound && outside(radius) > GENERIC_LENS_PSF_WINDOW_TRUNCATION)
290
+ radius++;
291
+ const truncated = Math.max(0, outside(radius));
292
+ const crop = (node) => {
293
+ const w0 = 2 * bound + 1, w1 = 2 * radius + 1, out = new Float64Array(w1 * w1);
294
+ let sum = 0;
295
+ for (let j = 0; j < w1; j++)
296
+ for (let i = 0; i < w1; i++) {
297
+ const v = node.kernel[(j + bound - radius) * w0 + i + bound - radius];
298
+ out[j * w1 + i] = v;
299
+ sum += v;
300
+ }
301
+ for (let k = 0; k < out.length; k++)
302
+ out[k] = out[k] / sum;
303
+ return out;
304
+ };
305
+ const extra = input.extraKernel, kernelRadius = radius + (extra ? extra.radius : 0), w = 2 * kernelRadius + 1;
306
+ const fieldX = [-ex.x, -fh.x / 2, 0, fh.x / 2, ex.x], fieldY = [-ex.y, -fh.y / 2, 0, fh.y / 2, ex.y];
307
+ const uncertainty = { kind: "not-quantified", limitation: "Generic catalogue lens PSF: Photivra's own Seidel coefficients, not a measured lens." };
308
+ const nodes = [];
309
+ const focusDiopters = 1 / input.focusDistanceM;
310
+ for (const x of fieldX)
311
+ for (const y of fieldY)
312
+ for (const nm of GENERIC_LENS_PSF_WAVELENGTHS_NM) {
313
+ const base = quadrant.get(Math.abs(x) + ":" + Math.abs(y) + ":" + nm);
314
+ let kernel = mirror(crop(base), radius, x < 0, y < 0);
315
+ if (extra)
316
+ kernel = convolveSiteKernels(kernel, radius, extra.weights, extra.radius);
317
+ nodes.push({ nodeId: "pv-" + x.toFixed(6) + "-" + y.toFixed(6) + "-" + nm,
318
+ coordinate: { focalLengthMm: f, focusDiopters, apertureFNumber: N, fieldXmm: x, fieldYmm: y, wavelengthNm: nm, signedDefocusImagePlaneMicrometers: 0 },
319
+ kernel: { widthSamples: w, heightSamples: w, samplePitchMicrometersX: pitch, samplePitchMicrometersY: pitch, centerSampleX: kernelRadius, centerSampleY: kernelRadius,
320
+ normalizedIntensity: Array.from(kernel) },
321
+ bestFocusImagePlaneOffsetMicrometers: 0, relativePupilThroughputFactor: 1, evidence: input.evidence, uncertainty });
322
+ }
323
+ const profile = { schemaVersion: "0.1.0", profileId: "photivra-" + lens.lensId + ":psf", profileVersion: "1.0.0", scientificStatus: "approximation",
324
+ opticalDomain: "lens-primary-optical-path-only", coordinateSystem: "image-plane-metric", fieldAxes: "+X right, +Y up", kernelEnergyNormalization: "unit-energy-shape",
325
+ interpolation: "bounded-regular-grid-centroid-aligned", throughputOwnership: "separate-relative-pupil-throughput-factor",
326
+ responseIncludes: { diffraction: true, aberration: true, defocus: extra !== undefined, pupilClippingShape: false },
327
+ sensorOpticalStackIncluded: false, sensorSamplingIncluded: false, reconstructionIncluded: false, strayLightIncluded: false,
328
+ axes: { focalLengthMm: [f], focusDiopters: [focusDiopters], apertureFNumber: [N], fieldXmm: fieldX, fieldYmm: fieldY, wavelengthNm: [...GENERIC_LENS_PSF_WAVELENGTHS_NM],
329
+ signedDefocusImagePlaneMicrometers: [0] }, nodes, evidence: input.evidence,
330
+ limitations: ["Generic catalogue lens (#549): scalar Fraunhofer PSF of Photivra's own Seidel wavefront with lateral colour, integrated over site cells; not a measured lens.",
331
+ "Field and wavelength nodes are interpolated with centroid alignment; between nodes the PSF is a blend, not a recomputed PSF."] };
332
+ markEngineOwnedLensSampledPsfProfile(profile);
333
+ return { profile, aberrationKernelRadiusSites: radius, kernelRadiusSites: kernelRadius, maximumTruncatedEnergyFraction: truncated,
334
+ transformSize: { minimum: minimumTransform, maximum: maximumTransform }, maximumPhaseStepWaves: phaseStep, fieldXmm: fieldX, fieldYmm: fieldY,
335
+ wavelengthsNm: [...GENERIC_LENS_PSF_WAVELENGTHS_NM] };
336
+ }
337
+ //# sourceMappingURL=generic-lens-psf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generic-lens-psf.js","sourceRoot":"","sources":["../../src/optics/generic-lens-psf.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * The PSF of a generic catalogue lens (#549) on a sensor's site lattice, as a field- and wavelength-dependent sampled PSF\n * profile for the environment PSF quadrature (`simulateCapture` `lens.catalogue`).\n *\n * Model (an approximation): scalar Fraunhofer diffraction of a uniform circular pupil of diameter D = f/N carrying the\n * lens's Seidel wavefront (`resolveGenericLens`; the expansion of `createWavefrontPupilGrid`, Hopkins 1950; Wyant & Creath\n * 1992), propagated to the thin-lens image distance z (working f-number z/D; Goodman, Introduction to Fourier Optics, ch. 4-6).\n * U(x) = Σ_pupil exp(i·2π·W(u)/λ)·exp(−i·2π·x·u/(λ·z)) is evaluated with a zero-padded radix-2 FFT (Cooley & Tukey 1965) on\n * an image grid of pitch δ = min(λ·N_w/2, p/4) (p the site pitch), and |U|² is integrated over each site's square cell (exact\n * overlap of each δ-square with the cell). Lateral colour displaces the PSF radially by Δr(λ). The kernel window is (2R + 1)²\n * sites; the energy outside it is reported and put back by unit-sum renormalisation, as for `lens.diffraction`.\n *\n * Nodes: a 5 × 5 regular grid of field points (0, ±half the frame, ±the frame edge plus the kernel reach) × 5 wavelengths\n * (380, 450, 550, 650, 780 nm), each computed at its own field azimuth (no rotation of a radial node), interpolated by the\n * engine's centroid-aligned multilinear scheme. The quadrant x, y ≥ 0 is computed and mirrored (the Seidel PSF of a mirrored\n * field point is the mirrored PSF).\n *\n * Kernel orientation: a kernel's row index increases with image +Y, the orientation the environment PSF quadrature applies\n * (`sensorPsfSourcePoint`), so coma and lateral colour point away from the optical axis in the rendered image.\n */\n\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport type { EvidenceProvenance } from \"../core/evidence-provenance.js\";\nimport { genericLensDispersion, type ResolvedGenericLens } from \"../equipment/generic-lens-catalogue.js\";\nimport { markEngineOwnedLensSampledPsfProfile, type LensSampledPsfGridNode, type LensSampledPsfProfile } from \"./lens-psf-profile.js\";\nimport { convolveSiteKernels } from \"./defocus-disc-kernel.js\";\n\n/** Wavelength nodes of a catalogue PSF profile, nm (they span the facade's 380–780 nm quadrature). */\nexport const GENERIC_LENS_PSF_WAVELENGTHS_NM = /* @__PURE__ */ Object.freeze([380, 450, 550, 650, 780] as const);\n/** Largest kernel radius of the aberration PSF in sites (before any defocus disc is convolved in). */\nexport const GENERIC_LENS_PSF_MAXIMUM_KERNEL_RADIUS_SITES = 12;\n/** Largest kernel radius in sites of the catalogue PSF with a defocus disc convolved in (`simulateCapture` refuses more: cost). */\nexport const GENERIC_LENS_PSF_MAXIMUM_COMBINED_RADIUS_SITES = 16;\n/** Energy fraction a node may leave outside the chosen kernel window (it is put back by renormalisation and reported). */\nexport const GENERIC_LENS_PSF_WINDOW_TRUNCATION = 0.03;\n/** Largest FFT size per axis. */\nconst MAXIMUM_TRANSFORM = 2048;\n/** Largest optical-path step between neighbouring pupil samples, in waves (Nyquist is 0.5). */\nconst MAXIMUM_PHASE_STEP_WAVES = 0.35;\nconst DESIGN_WAVELENGTH_UM = 0.55;\n\nexport interface GenericLensPsfInput {\n /** The lens resolved at the capture's f-number and focus distance (`resolveGenericLens`). */\n lens: ResolvedGenericLens;\n /** Finite focus distance, m (the thin-lens image distance sets the propagation distance). */\n focusDistanceM: number;\n sitePitchMicrometers: number;\n /** Half extent of the field the profile must cover, mm (the frame's half width and height plus the PSF and query reach). */\n fieldHalfExtentMm: { x: number; y: number };\n /** The frame's half width and height, mm (the inner field nodes sit at half of them). */\n frameHalfMm: { x: number; y: number };\n /** Optional site kernel (2R_d + 1)² to convolve into every node (the `lens.defocus` disc). */\n extraKernel?: { weights: Float64Array; radius: number };\n evidence: readonly EvidenceProvenance[];\n}\n\nexport interface GenericLensPsf {\n profile: LensSampledPsfProfile;\n /** Radius of the aberration PSF window, sites, and of the applied kernel (with `extraKernel`). */\n aberrationKernelRadiusSites: number;\n kernelRadiusSites: number;\n /** Largest energy fraction outside the aberration window over the nodes (put back by renormalisation). */\n maximumTruncatedEnergyFraction: number;\n /** FFT sizes used (smallest, largest) and the largest pupil phase step, waves. */\n transformSize: { minimum: number; maximum: number };\n maximumPhaseStepWaves: number;\n /** Field node coordinates, mm, and wavelengths, nm. */\n fieldXmm: readonly number[];\n fieldYmm: readonly number[];\n wavelengthsNm: readonly number[];\n}\n\n/** In-place iterative radix-2 FFT, exp(−i·2π·k·m/n). */\nfunction radix2Fft(re: Float64Array, im: Float64Array, n: number, cos: Float64Array, sin: Float64Array): void {\n for (let i = 1, j = 0; i < n; i++) {\n let bit = n >> 1;\n for (; j & bit; bit >>= 1) j ^= bit;\n j ^= bit;\n if (i < j) { let t = re[i]!; re[i] = re[j]!; re[j] = t; t = im[i]!; im[i] = im[j]!; im[j] = t; }\n }\n for (let size = 2; size <= n; size <<= 1) {\n const half = size >> 1, step = n / size;\n for (let start = 0; start < n; start += size) for (let k = 0; k < half; k++) {\n const c = cos[k * step]!, s = sin[k * step]!, a = start + k, b = a + half;\n const tr = re[b]! * c + im[b]! * s, ti = im[b]! * c - re[b]! * s;\n re[b] = re[a]! - tr; im[b] = im[a]! - ti; re[a] = re[a]! + tr; im[a] = im[a]! + ti;\n }\n }\n}\n\n/** One axis's overlap of δ-squares centred at positions (s·δ + shift) with site cells [(i − ½)p, (i + ½)p], i ∈ [−R, R]. */\nfunction overlaps(count: number, delta: number, shift: number, pitch: number, radius: number): { cell: Int32Array; weight: Float64Array; cell2: Int32Array; weight2: Float64Array } {\n const cell = new Int32Array(count).fill(-1), weight = new Float64Array(count), cell2 = new Int32Array(count).fill(-1), weight2 = new Float64Array(count);\n for (let k = 0; k < count; k++) {\n const signed = k < count / 2 ? k : k - count, centre = signed * delta + shift, lo = centre - delta / 2, hi = centre + delta / 2;\n const first = Math.floor(lo / pitch + 0.5), last = Math.floor(hi / pitch + 0.5);\n for (let i = first; i <= last && i <= first + 1; i++) {\n if (i < -radius || i > radius) continue;\n const w = (Math.min(hi, (i + 0.5) * pitch) - Math.max(lo, (i - 0.5) * pitch)) / delta;\n if (w <= 0) continue;\n if (cell[k] === -1) { cell[k] = i + radius; weight[k] = w; } else { cell2[k] = i + radius; weight2[k] = w; }\n }\n }\n return { cell, weight, cell2, weight2 };\n}\n\n/** A node's energy per site cell over the (2R₀ + 1)² window (not normalised) and the whole period's energy. */\ninterface NodeResult { kernel: Float64Array; total: number; transform: number; phaseStep: number }\n\n/** Seidel OPD (µm) at normalised pupil (px, py) for a field point at height H and azimuth φ, at wavelength λ (µm). */\nfunction opdMicrometers(lens: ResolvedGenericLens, H: number, cosPhi: number, sinPhi: number, lambdaUm: number, px: number, py: number): number {\n const a = lens.character.aberrations, r2 = px * px + py * py, along = px * cosPhi + py * sinPhi;\n const defocus = a.balancingDefocusWaves + a.axialColorWaves * genericLensDispersion(lambdaUm * 1000);\n // Positive coma W131 sends the comet's flare away from the axis in the image (H ρ³ cos(θ − φ), with the FFT sign above).\n return DESIGN_WAVELENGTH_UM * (defocus * r2 + a.sphericalWaves * r2 * r2 + a.comaWaves * H * r2 * along + a.astigmatismWaves * H * H * along * along + a.fieldCurvatureWaves * H * H * r2);\n}\n\n/** Largest transverse ray aberration 2·N_w·|∂W/∂ρ| (µm) over the pupil, from a 33 × 33 grid. */\nfunction geometricRadiusMicrometers(lens: ResolvedGenericLens, H: number, lambdaUm: number, workingF: number): number {\n let largest = 0;\n const n = 33, h = 1e-4;\n for (let j = 0; j < n; j++) for (let i = 0; i < n; i++) {\n const px = -1 + 2 * i / (n - 1), py = -1 + 2 * j / (n - 1);\n if (px * px + py * py > 1) continue;\n const gx = (opdMicrometers(lens, H, 1, 0, lambdaUm, px + h, py) - opdMicrometers(lens, H, 1, 0, lambdaUm, px - h, py)) / (2 * h);\n const gy = (opdMicrometers(lens, H, 1, 0, lambdaUm, px, py + h) - opdMicrometers(lens, H, 1, 0, lambdaUm, px, py - h)) / (2 * h);\n largest = Math.max(largest, Math.hypot(gx, gy));\n }\n return 2 * workingF * largest;\n}\n\nfunction nodeKernel(lens: ResolvedGenericLens, xMm: number, yMm: number, lambdaNm: number, workingF: number, pupilDiameterMm: number, zMm: number,\n pitch: number, radius: number): NodeResult {\n const lambdaUm = lambdaNm / 1000, r = Math.hypot(xMm, yMm), H = Math.min(1, r / lens.halfDiagonalMm);\n const cosPhi = r > 0 ? xMm / r : 1, sinPhi = r > 0 ? yMm / r : 0;\n // Lateral colour: radial displacement L·R·H·D(λ), µm.\n const shift = lens.character.lateralColor.fraction * lens.halfDiagonalMm * 1000 * H * genericLensDispersion(lambdaNm);\n const delta = Math.min(lambdaUm * workingF / 2, pitch / 4);\n let size = 64;\n // The FFT period must hold the window, or (when the PSF is much smaller than the window, for large pixels) the PSF itself:\n // the geometric blur plus 16·λ·N_w of Airy rings (the energy beyond, about 1 %, folds back into the period).\n const psfExtentUm = 2 * (geometricRadiusMicrometers(lens, H, lambdaUm, workingF) + 16 * lambdaUm * workingF);\n while (size * delta < Math.min(2.5 * (radius + 0.5) * pitch, psfExtentUm) + 2 * Math.abs(shift)) size *= 2;\n for (;;) {\n if (size > MAXIMUM_TRANSFORM) throw new InvalidScientificInputError(\"The catalogue lens PSF needs an FFT larger than \" + MAXIMUM_TRANSFORM + \" per axis.\");\n // Pupil pitch: δ · p_pupil = λ·z / size (mm).\n const pupilPitchMm = lambdaUm / 1000 * zMm / (size * delta / 1000), half = pupilDiameterMm / 2, reach = Math.floor(half / pupilPitchMm);\n const re = new Float64Array(size * size), im = new Float64Array(size * size), rowUsed = new Uint8Array(size);\n let open = 0, phaseStep = 0;\n const opd = new Float64Array((2 * reach + 1) * (2 * reach + 1)).fill(NaN);\n for (let v = -reach; v <= reach; v++) for (let u = -reach; u <= reach; u++) {\n const px = u * pupilPitchMm / half, py = v * pupilPitchMm / half;\n if (px * px + py * py > 1 + 1e-12) continue;\n const w = opdMicrometers(lens, H, cosPhi, sinPhi, lambdaUm, px, py), phase = 2 * Math.PI * w / lambdaUm;\n opd[(v + reach) * (2 * reach + 1) + u + reach] = w;\n const index = ((v + size) % size) * size + (u + size) % size;\n re[index] = Math.cos(phase); im[index] = Math.sin(phase); rowUsed[(v + size) % size] = 1; open++;\n }\n const width = 2 * reach + 1;\n for (let j = 0; j < width; j++) for (let i = 0; i < width; i++) {\n const w = opd[j * width + i]!;\n if (Number.isNaN(w)) continue;\n if (i + 1 < width && !Number.isNaN(opd[j * width + i + 1]!)) phaseStep = Math.max(phaseStep, Math.abs(opd[j * width + i + 1]! - w) / lambdaUm);\n if (j + 1 < width && !Number.isNaN(opd[(j + 1) * width + i]!)) phaseStep = Math.max(phaseStep, Math.abs(opd[(j + 1) * width + i]! - w) / lambdaUm);\n }\n if (open < 9) throw new InvalidScientificInputError(\"The catalogue lens PSF pupil has fewer than 9 samples.\");\n if (phaseStep > MAXIMUM_PHASE_STEP_WAVES) { size *= 2; continue; }\n const cos = new Float64Array(size / 2), sin = new Float64Array(size / 2);\n for (let k = 0; k < size / 2; k++) { cos[k] = Math.cos(2 * Math.PI * k / size); sin[k] = Math.sin(2 * Math.PI * k / size); }\n const rowRe = new Float64Array(size), rowIm = new Float64Array(size);\n for (let y = 0; y < size; y++) {\n if (!rowUsed[y]) continue;\n rowRe.set(re.subarray(y * size, (y + 1) * size)); rowIm.set(im.subarray(y * size, (y + 1) * size));\n radix2Fft(rowRe, rowIm, size, cos, sin);\n re.set(rowRe, y * size); im.set(rowIm, y * size);\n }\n const colRe = new Float64Array(size), colIm = new Float64Array(size);\n const ox = overlaps(size, delta, shift * cosPhi, pitch, radius), oy = overlaps(size, delta, shift * sinPhi, pitch, radius);\n const w2 = 2 * radius + 1, kernel = new Float64Array(w2 * w2);\n let total = 0, inside = 0;\n for (let x = 0; x < size; x++) {\n for (let y = 0; y < size; y++) { colRe[y] = re[y * size + x]!; colIm[y] = im[y * size + x]!; }\n radix2Fft(colRe, colIm, size, cos, sin);\n const cx = ox.cell[x]!, cx2 = ox.cell2[x]!, wx = ox.weight[x]!, wx2 = ox.weight2[x]!;\n for (let y = 0; y < size; y++) {\n const intensity = colRe[y]! * colRe[y]! + colIm[y]! * colIm[y]!;\n total += intensity;\n if (cx < 0) continue;\n const cy = oy.cell[y]!;\n if (cy < 0) continue;\n const cy2 = oy.cell2[y]!, wy = oy.weight[y]!, wy2 = oy.weight2[y]!;\n // Row index increases with image +Y (the quadrature's orientation).\n kernel[cy * w2 + cx]! += intensity * wx * wy; inside += intensity * wx * wy;\n if (cx2 >= 0) { kernel[cy * w2 + cx2]! += intensity * wx2 * wy; inside += intensity * wx2 * wy; }\n if (cy2 >= 0) {\n kernel[cy2 * w2 + cx]! += intensity * wx * wy2; inside += intensity * wx * wy2;\n if (cx2 >= 0) { kernel[cy2 * w2 + cx2]! += intensity * wx2 * wy2; inside += intensity * wx2 * wy2; }\n }\n }\n }\n if (!(total > 0) || !(inside > 0)) throw new InvalidScientificInputError(\"The catalogue lens PSF has no energy in its window.\");\n return { kernel, total, transform: size, phaseStep };\n }\n}\n\nfunction mirror(kernel: Float64Array, radius: number, flipX: boolean, flipY: boolean): Float64Array {\n const w = 2 * radius + 1, out = new Float64Array(kernel.length);\n for (let j = 0; j < w; j++) for (let i = 0; i < w; i++) out[(flipY ? w - 1 - j : j) * w + (flipX ? w - 1 - i : i)] = kernel[j * w + i]!;\n return out;\n}\n\n/** Thin-lens image distance z (mm), pupil diameter D = f/N (mm) and working f-number z/D at a finite focus. */\nfunction pupilGeometry(lens: ResolvedGenericLens, focusDistanceM: number): { z: number; D: number; workingF: number } {\n const f = lens.focalLengthMm, d = focusDistanceM * 1000;\n if (!(d > f)) throw new InvalidScientificInputError(\"The focus distance must exceed the focal length.\");\n const z = 1 / (1 / f - 1 / d), D = f / lens.character.fNumber;\n return { z, D, workingF: z / D };\n}\n\n/**\n * An upper bound of the aberration PSF's kernel radius in sites (the window is then narrowed to keep at most\n * {@link GENERIC_LENS_PSF_WINDOW_TRUNCATION} of every node's energy outside): the largest transverse ray aberration 2·N_w·|∂W/∂ρ| over the pupil at the frame\n * corner and every wavelength node, plus the lateral-colour shift, plus the Airy third dark ring 3.24·λ·N_w at 780 nm, rounded up\n * to whole sites (at least 1, at most {@link GENERIC_LENS_PSF_MAXIMUM_KERNEL_RADIUS_SITES}).\n */\nexport function genericLensPsfKernelRadius(input: { lens: ResolvedGenericLens; focusDistanceM: number; sitePitchMicrometers: number }): number {\n const { lens, sitePitchMicrometers: pitch } = input, { workingF } = pupilGeometry(lens, input.focusDistanceM);\n const longest = GENERIC_LENS_PSF_WAVELENGTHS_NM[GENERIC_LENS_PSF_WAVELENGTHS_NM.length - 1]! / 1000;\n let reachUm = 0;\n for (const nm of GENERIC_LENS_PSF_WAVELENGTHS_NM) {\n const shift = Math.abs(lens.character.lateralColor.fraction * lens.halfDiagonalMm * 1000 * genericLensDispersion(nm));\n reachUm = Math.max(reachUm, geometricRadiusMicrometers(lens, 1, nm / 1000, workingF) + shift);\n }\n reachUm += 3.24 * longest * workingF;\n return Math.min(GENERIC_LENS_PSF_MAXIMUM_KERNEL_RADIUS_SITES, Math.max(1, Math.ceil(reachUm / pitch - 0.5)));\n}\n\n/**\n * Builds the catalogue lens's sampled PSF profile for the environment PSF quadrature: 5 × 5 field nodes × 5 wavelengths, each\n * kernel on the site lattice. Engine-owned (parsed once). Deterministic: the same input gives the same profile.\n */\nexport function createGenericLensPsfProfile(input: GenericLensPsfInput): GenericLensPsf {\n const { lens, sitePitchMicrometers: pitch } = input;\n const f = lens.focalLengthMm, N = lens.character.fNumber, { z, D, workingF } = pupilGeometry(lens, input.focusDistanceM);\n const ex = input.fieldHalfExtentMm, fh = input.frameHalfMm;\n if (!(ex.x > fh.x / 2 && ex.y > fh.y / 2 && fh.x > 0 && fh.y > 0)) throw new InvalidScientificInputError(\"The PSF field extent must exceed half the frame.\");\n const xs = [0, fh.x / 2, ex.x], ys = [0, fh.y / 2, ex.y];\n const bound = genericLensPsfKernelRadius({ lens, focusDistanceM: input.focusDistanceM, sitePitchMicrometers: pitch });\n const quadrant = new Map<string, NodeResult>();\n let minimumTransform = Infinity, maximumTransform = 0, phaseStep = 0;\n for (const x of xs) for (const y of ys) for (const nm of GENERIC_LENS_PSF_WAVELENGTHS_NM) {\n const node = nodeKernel(lens, x, y, nm, workingF, D, z, pitch, bound);\n minimumTransform = Math.min(minimumTransform, node.transform);\n maximumTransform = Math.max(maximumTransform, node.transform); phaseStep = Math.max(phaseStep, node.phaseStep);\n quadrant.set(x + \":\" + y + \":\" + nm, node);\n }\n // The window: the smallest radius that keeps at most 1 % of every node's energy outside it (at most the bound).\n const outside = (r: number): number => {\n let worst = 0;\n for (const node of quadrant.values()) {\n let inside = 0;\n for (let j = bound - r; j <= bound + r; j++) for (let i = bound - r; i <= bound + r; i++) inside += node.kernel[j * (2 * bound + 1) + i]!;\n worst = Math.max(worst, 1 - inside / node.total);\n }\n return worst;\n };\n let radius = 1;\n while (radius < bound && outside(radius) > GENERIC_LENS_PSF_WINDOW_TRUNCATION) radius++;\n const truncated = Math.max(0, outside(radius));\n const crop = (node: NodeResult): Float64Array => {\n const w0 = 2 * bound + 1, w1 = 2 * radius + 1, out = new Float64Array(w1 * w1);\n let sum = 0;\n for (let j = 0; j < w1; j++) for (let i = 0; i < w1; i++) { const v = node.kernel[(j + bound - radius) * w0 + i + bound - radius]!; out[j * w1 + i] = v; sum += v; }\n for (let k = 0; k < out.length; k++) out[k] = out[k]! / sum;\n return out;\n };\n const extra = input.extraKernel, kernelRadius = radius + (extra ? extra.radius : 0), w = 2 * kernelRadius + 1;\n const fieldX = [-ex.x, -fh.x / 2, 0, fh.x / 2, ex.x], fieldY = [-ex.y, -fh.y / 2, 0, fh.y / 2, ex.y];\n const uncertainty = { kind: \"not-quantified\" as const, limitation: \"Generic catalogue lens PSF: Photivra's own Seidel coefficients, not a measured lens.\" };\n const nodes: LensSampledPsfGridNode[] = [];\n const focusDiopters = 1 / input.focusDistanceM;\n for (const x of fieldX) for (const y of fieldY) for (const nm of GENERIC_LENS_PSF_WAVELENGTHS_NM) {\n const base = quadrant.get(Math.abs(x) + \":\" + Math.abs(y) + \":\" + nm)!;\n let kernel = mirror(crop(base), radius, x < 0, y < 0);\n if (extra) kernel = convolveSiteKernels(kernel, radius, extra.weights, extra.radius);\n nodes.push({ nodeId: \"pv-\" + x.toFixed(6) + \"-\" + y.toFixed(6) + \"-\" + nm,\n coordinate: { focalLengthMm: f, focusDiopters, apertureFNumber: N, fieldXmm: x, fieldYmm: y, wavelengthNm: nm, signedDefocusImagePlaneMicrometers: 0 },\n kernel: { widthSamples: w, heightSamples: w, samplePitchMicrometersX: pitch, samplePitchMicrometersY: pitch, centerSampleX: kernelRadius, centerSampleY: kernelRadius,\n normalizedIntensity: Array.from(kernel) },\n bestFocusImagePlaneOffsetMicrometers: 0, relativePupilThroughputFactor: 1, evidence: input.evidence, uncertainty });\n }\n const profile: LensSampledPsfProfile = { schemaVersion: \"0.1.0\", profileId: \"photivra-\" + lens.lensId + \":psf\", profileVersion: \"1.0.0\", scientificStatus: \"approximation\",\n opticalDomain: \"lens-primary-optical-path-only\", coordinateSystem: \"image-plane-metric\", fieldAxes: \"+X right, +Y up\", kernelEnergyNormalization: \"unit-energy-shape\",\n interpolation: \"bounded-regular-grid-centroid-aligned\", throughputOwnership: \"separate-relative-pupil-throughput-factor\",\n responseIncludes: { diffraction: true, aberration: true, defocus: extra !== undefined, pupilClippingShape: false },\n sensorOpticalStackIncluded: false, sensorSamplingIncluded: false, reconstructionIncluded: false, strayLightIncluded: false,\n axes: { focalLengthMm: [f], focusDiopters: [focusDiopters], apertureFNumber: [N], fieldXmm: fieldX, fieldYmm: fieldY, wavelengthNm: [...GENERIC_LENS_PSF_WAVELENGTHS_NM],\n signedDefocusImagePlaneMicrometers: [0] }, nodes, evidence: input.evidence,\n limitations: [\"Generic catalogue lens (#549): scalar Fraunhofer PSF of Photivra's own Seidel wavefront with lateral colour, integrated over site cells; not a measured lens.\",\n \"Field and wavelength nodes are interpolated with centroid alignment; between nodes the PSF is a blend, not a recomputed PSF.\"] };\n markEngineOwnedLensSampledPsfProfile(profile);\n return { profile, aberrationKernelRadiusSites: radius, kernelRadiusSites: kernelRadius, maximumTruncatedEnergyFraction: truncated,\n transformSize: { minimum: minimumTransform, maximum: maximumTransform }, maximumPhaseStepWaves: phaseStep, fieldXmm: fieldX, fieldYmm: fieldY,\n wavelengthsNm: [...GENERIC_LENS_PSF_WAVELENGTHS_NM] };\n}\n"]}
@@ -56,6 +56,12 @@ export interface IlluminationVignetting {
56
56
  profileMinimumThroughputFactor: number;
57
57
  profileMaximumThroughputFactor: number;
58
58
  }
59
+ /**
60
+ * @internal (#563) The `linearThroughputFactor` that {@link calculateIlluminationVignetting} returns, from a profile validated once:
61
+ * the same arithmetic and the same errors, without the result envelope, for callers that evaluate one profile at many points
62
+ * (the image-space field factor of `prepareDefocusImageSpaceGeometry`).
63
+ */
64
+ export declare function prepareIlluminationVignettingThroughput(profile: IlluminationVignettingProfile): (imagePointMm: LensFieldPointMm) => number;
59
65
  /**
60
66
  * Calculates generic illumination falloff across the image (vignetting) as a
61
67
  * factor that multiplies linear light.
@@ -93,6 +93,29 @@ function validateProfile(profile) {
93
93
  maximumThroughputFactor
94
94
  };
95
95
  }
96
+ /** The normalized radius and throughput factor at an image point, for a validated profile (the arithmetic of calculateIlluminationVignetting). */
97
+ function throughputAt(profile, imagePointMm) {
98
+ const radiusMm = Math.hypot(imagePointMm.x, imagePointMm.y);
99
+ const normalizedRadius = radiusMm / profile.normalizationRadiusMm;
100
+ if (normalizedRadius > profile.maximumNormalizedRadius) {
101
+ throw new InvalidScientificInputError("imagePointMm lies outside the illumination vignetting profile maximumNormalizedRadius.");
102
+ }
103
+ const squaredRadius = normalizedRadius * normalizedRadius;
104
+ return { normalizedRadius, linearThroughputFactor: throughputFromSquaredRadius(squaredRadius, profile.coefficients) };
105
+ }
106
+ /**
107
+ * @internal (#563) The `linearThroughputFactor` that {@link calculateIlluminationVignetting} returns, from a profile validated once:
108
+ * the same arithmetic and the same errors, without the result envelope, for callers that evaluate one profile at many points
109
+ * (the image-space field factor of `prepareDefocusImageSpaceGeometry`).
110
+ */
111
+ export function prepareIlluminationVignettingThroughput(profile) {
112
+ const validated = validateProfile(profile);
113
+ return (imagePointMm) => {
114
+ requireFinite("imagePointMm.x", imagePointMm.x);
115
+ requireFinite("imagePointMm.y", imagePointMm.y);
116
+ return throughputAt(validated, imagePointMm).linearThroughputFactor;
117
+ };
118
+ }
96
119
  function assumptions() {
97
120
  return [
98
121
  "Generic rotationally symmetric illumination-vignetting approximation evaluated from ideal image-plane field radius",
@@ -117,13 +140,7 @@ export function calculateIlluminationVignetting(input) {
117
140
  requireFinite("imagePointMm.x", input.imagePointMm.x);
118
141
  requireFinite("imagePointMm.y", input.imagePointMm.y);
119
142
  const profile = validateProfile(input.profile);
120
- const radiusMm = Math.hypot(input.imagePointMm.x, input.imagePointMm.y);
121
- const normalizedRadius = radiusMm / profile.normalizationRadiusMm;
122
- if (normalizedRadius > profile.maximumNormalizedRadius) {
123
- throw new InvalidScientificInputError("imagePointMm lies outside the illumination vignetting profile maximumNormalizedRadius.");
124
- }
125
- const squaredRadius = normalizedRadius * normalizedRadius;
126
- const linearThroughputFactor = throughputFromSquaredRadius(squaredRadius, profile.coefficients);
143
+ const { normalizedRadius, linearThroughputFactor } = throughputAt(profile, input.imagePointMm);
127
144
  const attenuationStops = linearThroughputFactor === 1
128
145
  ? 0
129
146
  : -Math.log2(linearThroughputFactor);
@@ -1 +1 @@
1
- {"version":3,"file":"illumination-vignetting.js","sourceRoot":"","sources":["../../src/optics/illumination-vignetting.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Generic illumination falloff across the image (vignetting), as a factor that multiplies linear\n * light.\n *\n * A profile is accepted only if its relative throughput stays strictly positive and never exceeds 1\n * (the value on the optical axis) anywhere in its declared range.\n * @see docs/PHYSICS_FOUNDATION.md for the equations, units, coordinate conventions, known blockers\n * and support limits.\n */\n\nimport {\n approximationResult,\n type CalculationResult\n} from \"../core/calculation-result.js\";\nimport {\n InvalidScientificInputError,\n requirePositiveFinite\n} from \"../core/validation.js\";\nimport type { LensFieldPointMm } from \"./radial-distortion.js\";\nimport { requireInputObject, requireInputKeys } from \"../core/record-validation.js\";\n\nexport interface IlluminationVignettingCoefficients {\n /** Coefficient on normalized radius squared. */\n r2: number;\n /** Coefficient on normalized radius to the fourth power. */\n r4: number;\n /** Coefficient on normalized radius to the sixth power. */\n r6: number;\n}\n\nexport interface IlluminationVignettingProfile {\n /** Physical image-plane radius used to normalize field position, in mm. */\n normalizationRadiusMm: number;\n /** Maximum normalized image-plane radius over which the profile is valid. */\n maximumNormalizedRadius: number;\n /**\n * Generic radial relative-illumination coefficients.\n *\n * Relative linear throughput is:\n * 1 + r2 * rho^2 + r4 * rho^4 + r6 * rho^6\n *\n * where rho is physical image-plane radius / normalizationRadiusMm.\n */\n coefficients: IlluminationVignettingCoefficients;\n}\n\nexport interface CalculateIlluminationVignettingInput {\n /**\n * Ideal image-plane field position relative to the optical axis, in mm.\n *\n * Throughput is evaluated at this ideal field coordinate. Geometric\n * distortion is neither applied nor inverted.\n */\n imagePointMm: LensFieldPointMm;\n profile: IlluminationVignettingProfile;\n}\n\nexport interface IlluminationVignetting {\n imagePointMm: LensFieldPointMm;\n normalizedRadius: number;\n /**\n * Factor that multiplies scene-linear or channel-linear signal.\n *\n * 1 means no attenuation. Values must stay in (0, 1].\n */\n linearThroughputFactor: number;\n /** Positive exposure loss relative to the optical axis, in stops. */\n attenuationStops: number;\n profileMinimumThroughputFactor: number;\n profileMaximumThroughputFactor: number;\n}\n\ninterface ValidatedIlluminationProfile {\n normalizationRadiusMm: number;\n maximumNormalizedRadius: number;\n coefficients: IlluminationVignettingCoefficients;\n minimumThroughputFactor: number;\n maximumThroughputFactor: number;\n}\n\nfunction requireFinite(name: string, value: number): void {\n if (!Number.isFinite(value)) {\n throw new InvalidScientificInputError(`${name} must be finite.`);\n }\n}\n\nfunction throughputFromSquaredRadius(\n squaredRadius: number,\n coefficients: IlluminationVignettingCoefficients\n): number {\n return (\n 1 +\n coefficients.r2 * squaredRadius +\n coefficients.r4 * squaredRadius * squaredRadius +\n coefficients.r6 *\n squaredRadius *\n squaredRadius *\n squaredRadius\n );\n}\n\n/**\n * Real roots of dT/ds, where s = rho². Every interior maximum or minimum of the throughput lies at\n * one of these roots. The validator checks them along with both ends of the range. So a profile\n * that dips below zero, or rises above one, mid-field is rejected even when both ends are valid.\n *\n * The quadratic is scaled before solving and uses the numerically stable q-form roots. This stops a\n * tiny cubic term from erasing the smaller root through cancellation. The radial-distortion\n * validator uses the same scheme.\n */\nfunction throughputCriticalSquaredRadii(\n coefficients: IlluminationVignettingCoefficients\n): number[] {\n // T(s) = 1 + r2*s + r4*s² + r6*s³\n // dT/ds = r2 + 2*r4*s + 3*r6*s²\n const a = 3 * coefficients.r6;\n const b = 2 * coefficients.r4;\n const c = coefficients.r2;\n const coefficientScale = Math.max(Math.abs(a), Math.abs(b), Math.abs(c));\n if (coefficientScale === 0) {\n return [];\n }\n const scaledA = a / coefficientScale;\n const scaledB = b / coefficientScale;\n const scaledC = c / coefficientScale;\n\n if (scaledA === 0) {\n return scaledB === 0 ? [] : [-scaledC / scaledB];\n }\n\n const discriminant = scaledB * scaledB - 4 * scaledA * scaledC;\n if (discriminant < 0) {\n return [];\n }\n const root = Math.sqrt(discriminant);\n const q = -0.5 * (scaledB + (scaledB >= 0 ? root : -root));\n if (q === 0) {\n // Only when b = c = 0: the double root is s = 0.\n return [0];\n }\n return [q / scaledA, scaledC / q];\n}\n\nfunction validateProfile(\n profile: IlluminationVignettingProfile\n): ValidatedIlluminationProfile {\n requirePositiveFinite(\n \"profile.normalizationRadiusMm\",\n profile.normalizationRadiusMm\n );\n requirePositiveFinite(\n \"profile.maximumNormalizedRadius\",\n profile.maximumNormalizedRadius\n );\n requireFinite(\"profile.coefficients.r2\", profile.coefficients.r2);\n requireFinite(\"profile.coefficients.r4\", profile.coefficients.r4);\n requireFinite(\"profile.coefficients.r6\", profile.coefficients.r6);\n\n const maximumSquaredRadius =\n profile.maximumNormalizedRadius * profile.maximumNormalizedRadius;\n const candidates = [\n 0,\n maximumSquaredRadius,\n ...throughputCriticalSquaredRadii(profile.coefficients).filter(\n (value) => value > 0 && value < maximumSquaredRadius\n )\n ];\n const throughputs = candidates.map((squaredRadius) =>\n throughputFromSquaredRadius(squaredRadius, profile.coefficients)\n );\n const minimumThroughputFactor = Math.min(...throughputs);\n const maximumThroughputFactor = Math.max(...throughputs);\n\n if (\n !Number.isFinite(minimumThroughputFactor) ||\n minimumThroughputFactor <= 0\n ) {\n throw new InvalidScientificInputError(\n \"Illumination vignetting profile must keep relative linear throughput strictly greater than zero over maximumNormalizedRadius.\"\n );\n }\n\n if (\n !Number.isFinite(maximumThroughputFactor) ||\n maximumThroughputFactor > 1\n ) {\n throw new InvalidScientificInputError(\n \"Illumination vignetting profile must not amplify relative linear throughput above the optical-axis value of 1.\"\n );\n }\n\n return {\n normalizationRadiusMm: profile.normalizationRadiusMm,\n maximumNormalizedRadius: profile.maximumNormalizedRadius,\n coefficients: { ...profile.coefficients },\n minimumThroughputFactor,\n maximumThroughputFactor\n };\n}\n\nfunction assumptions(): readonly string[] {\n return [\n \"Generic rotationally symmetric illumination-vignetting approximation evaluated from ideal image-plane field radius\",\n \"Relative linear throughput is normalized to 1 on the optical axis\",\n \"The model attenuates scene-linear/channel-linear signal only and must be applied before display/gamma encoding\",\n \"The model changes throughput only; it does not alter field coordinates, pupil shape, PSF shape, bokeh, or focus\",\n \"Mechanical/pupil vignetting and cat's-eye bokeh are separate pupil/PSF effects\",\n \"The profile is a generic caller-supplied radial approximation, not calibrated radiometry or a named-lens measurement\"\n ];\n}\n\n/**\n * Calculates generic illumination falloff across the image (vignetting) as a\n * factor that multiplies linear light.\n *\n * A profile is accepted only if its relative throughput stays strictly\n * positive and never exceeds 1 (the value on the optical axis) anywhere in\n * its declared range.\n */\nexport function calculateIlluminationVignetting(\n input: CalculateIlluminationVignettingInput\n): CalculationResult<IlluminationVignetting> {\n requireInputKeys(input, \"calculateIlluminationVignetting\", [\"imagePointMm\", \"profile\"]);\n requireInputObject(input.imagePointMm, \"imagePointMm\");\n requireFinite(\"imagePointMm.x\", input.imagePointMm.x);\n requireFinite(\"imagePointMm.y\", input.imagePointMm.y);\n\n const profile = validateProfile(input.profile);\n const radiusMm = Math.hypot(input.imagePointMm.x, input.imagePointMm.y);\n const normalizedRadius = radiusMm / profile.normalizationRadiusMm;\n\n if (normalizedRadius > profile.maximumNormalizedRadius) {\n throw new InvalidScientificInputError(\n \"imagePointMm lies outside the illumination vignetting profile maximumNormalizedRadius.\"\n );\n }\n\n const squaredRadius = normalizedRadius * normalizedRadius;\n const linearThroughputFactor = throughputFromSquaredRadius(\n squaredRadius,\n profile.coefficients\n );\n\n const attenuationStops =\n linearThroughputFactor === 1\n ? 0\n : -Math.log2(linearThroughputFactor);\n\n return approximationResult(\n {\n imagePointMm: { ...input.imagePointMm },\n normalizedRadius,\n linearThroughputFactor,\n attenuationStops,\n profileMinimumThroughputFactor: profile.minimumThroughputFactor,\n profileMaximumThroughputFactor: profile.maximumThroughputFactor\n },\n \"generic-radial-illumination-vignetting\",\n \"1.0.0\",\n assumptions()\n );\n}\n"]}
1
+ {"version":3,"file":"illumination-vignetting.js","sourceRoot":"","sources":["../../src/optics/illumination-vignetting.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Generic illumination falloff across the image (vignetting), as a factor that multiplies linear\n * light.\n *\n * A profile is accepted only if its relative throughput stays strictly positive and never exceeds 1\n * (the value on the optical axis) anywhere in its declared range.\n * @see docs/PHYSICS_FOUNDATION.md for the equations, units, coordinate conventions, known blockers\n * and support limits.\n */\n\nimport {\n approximationResult,\n type CalculationResult\n} from \"../core/calculation-result.js\";\nimport {\n InvalidScientificInputError,\n requirePositiveFinite\n} from \"../core/validation.js\";\nimport type { LensFieldPointMm } from \"./radial-distortion.js\";\nimport { requireInputObject, requireInputKeys } from \"../core/record-validation.js\";\n\nexport interface IlluminationVignettingCoefficients {\n /** Coefficient on normalized radius squared. */\n r2: number;\n /** Coefficient on normalized radius to the fourth power. */\n r4: number;\n /** Coefficient on normalized radius to the sixth power. */\n r6: number;\n}\n\nexport interface IlluminationVignettingProfile {\n /** Physical image-plane radius used to normalize field position, in mm. */\n normalizationRadiusMm: number;\n /** Maximum normalized image-plane radius over which the profile is valid. */\n maximumNormalizedRadius: number;\n /**\n * Generic radial relative-illumination coefficients.\n *\n * Relative linear throughput is:\n * 1 + r2 * rho^2 + r4 * rho^4 + r6 * rho^6\n *\n * where rho is physical image-plane radius / normalizationRadiusMm.\n */\n coefficients: IlluminationVignettingCoefficients;\n}\n\nexport interface CalculateIlluminationVignettingInput {\n /**\n * Ideal image-plane field position relative to the optical axis, in mm.\n *\n * Throughput is evaluated at this ideal field coordinate. Geometric\n * distortion is neither applied nor inverted.\n */\n imagePointMm: LensFieldPointMm;\n profile: IlluminationVignettingProfile;\n}\n\nexport interface IlluminationVignetting {\n imagePointMm: LensFieldPointMm;\n normalizedRadius: number;\n /**\n * Factor that multiplies scene-linear or channel-linear signal.\n *\n * 1 means no attenuation. Values must stay in (0, 1].\n */\n linearThroughputFactor: number;\n /** Positive exposure loss relative to the optical axis, in stops. */\n attenuationStops: number;\n profileMinimumThroughputFactor: number;\n profileMaximumThroughputFactor: number;\n}\n\ninterface ValidatedIlluminationProfile {\n normalizationRadiusMm: number;\n maximumNormalizedRadius: number;\n coefficients: IlluminationVignettingCoefficients;\n minimumThroughputFactor: number;\n maximumThroughputFactor: number;\n}\n\nfunction requireFinite(name: string, value: number): void {\n if (!Number.isFinite(value)) {\n throw new InvalidScientificInputError(`${name} must be finite.`);\n }\n}\n\nfunction throughputFromSquaredRadius(\n squaredRadius: number,\n coefficients: IlluminationVignettingCoefficients\n): number {\n return (\n 1 +\n coefficients.r2 * squaredRadius +\n coefficients.r4 * squaredRadius * squaredRadius +\n coefficients.r6 *\n squaredRadius *\n squaredRadius *\n squaredRadius\n );\n}\n\n/**\n * Real roots of dT/ds, where s = rho². Every interior maximum or minimum of the throughput lies at\n * one of these roots. The validator checks them along with both ends of the range. So a profile\n * that dips below zero, or rises above one, mid-field is rejected even when both ends are valid.\n *\n * The quadratic is scaled before solving and uses the numerically stable q-form roots. This stops a\n * tiny cubic term from erasing the smaller root through cancellation. The radial-distortion\n * validator uses the same scheme.\n */\nfunction throughputCriticalSquaredRadii(\n coefficients: IlluminationVignettingCoefficients\n): number[] {\n // T(s) = 1 + r2*s + r4*s² + r6*s³\n // dT/ds = r2 + 2*r4*s + 3*r6*s²\n const a = 3 * coefficients.r6;\n const b = 2 * coefficients.r4;\n const c = coefficients.r2;\n const coefficientScale = Math.max(Math.abs(a), Math.abs(b), Math.abs(c));\n if (coefficientScale === 0) {\n return [];\n }\n const scaledA = a / coefficientScale;\n const scaledB = b / coefficientScale;\n const scaledC = c / coefficientScale;\n\n if (scaledA === 0) {\n return scaledB === 0 ? [] : [-scaledC / scaledB];\n }\n\n const discriminant = scaledB * scaledB - 4 * scaledA * scaledC;\n if (discriminant < 0) {\n return [];\n }\n const root = Math.sqrt(discriminant);\n const q = -0.5 * (scaledB + (scaledB >= 0 ? root : -root));\n if (q === 0) {\n // Only when b = c = 0: the double root is s = 0.\n return [0];\n }\n return [q / scaledA, scaledC / q];\n}\n\nfunction validateProfile(\n profile: IlluminationVignettingProfile\n): ValidatedIlluminationProfile {\n requirePositiveFinite(\n \"profile.normalizationRadiusMm\",\n profile.normalizationRadiusMm\n );\n requirePositiveFinite(\n \"profile.maximumNormalizedRadius\",\n profile.maximumNormalizedRadius\n );\n requireFinite(\"profile.coefficients.r2\", profile.coefficients.r2);\n requireFinite(\"profile.coefficients.r4\", profile.coefficients.r4);\n requireFinite(\"profile.coefficients.r6\", profile.coefficients.r6);\n\n const maximumSquaredRadius =\n profile.maximumNormalizedRadius * profile.maximumNormalizedRadius;\n const candidates = [\n 0,\n maximumSquaredRadius,\n ...throughputCriticalSquaredRadii(profile.coefficients).filter(\n (value) => value > 0 && value < maximumSquaredRadius\n )\n ];\n const throughputs = candidates.map((squaredRadius) =>\n throughputFromSquaredRadius(squaredRadius, profile.coefficients)\n );\n const minimumThroughputFactor = Math.min(...throughputs);\n const maximumThroughputFactor = Math.max(...throughputs);\n\n if (\n !Number.isFinite(minimumThroughputFactor) ||\n minimumThroughputFactor <= 0\n ) {\n throw new InvalidScientificInputError(\n \"Illumination vignetting profile must keep relative linear throughput strictly greater than zero over maximumNormalizedRadius.\"\n );\n }\n\n if (\n !Number.isFinite(maximumThroughputFactor) ||\n maximumThroughputFactor > 1\n ) {\n throw new InvalidScientificInputError(\n \"Illumination vignetting profile must not amplify relative linear throughput above the optical-axis value of 1.\"\n );\n }\n\n return {\n normalizationRadiusMm: profile.normalizationRadiusMm,\n maximumNormalizedRadius: profile.maximumNormalizedRadius,\n coefficients: { ...profile.coefficients },\n minimumThroughputFactor,\n maximumThroughputFactor\n };\n}\n\n/** The normalized radius and throughput factor at an image point, for a validated profile (the arithmetic of calculateIlluminationVignetting). */\nfunction throughputAt(profile: ValidatedIlluminationProfile, imagePointMm: LensFieldPointMm): { normalizedRadius: number; linearThroughputFactor: number } {\n const radiusMm = Math.hypot(imagePointMm.x, imagePointMm.y);\n const normalizedRadius = radiusMm / profile.normalizationRadiusMm;\n\n if (normalizedRadius > profile.maximumNormalizedRadius) {\n throw new InvalidScientificInputError(\n \"imagePointMm lies outside the illumination vignetting profile maximumNormalizedRadius.\"\n );\n }\n\n const squaredRadius = normalizedRadius * normalizedRadius;\n return { normalizedRadius, linearThroughputFactor: throughputFromSquaredRadius(squaredRadius, profile.coefficients) };\n}\n\n/**\n * @internal (#563) The `linearThroughputFactor` that {@link calculateIlluminationVignetting} returns, from a profile validated once:\n * the same arithmetic and the same errors, without the result envelope, for callers that evaluate one profile at many points\n * (the image-space field factor of `prepareDefocusImageSpaceGeometry`).\n */\nexport function prepareIlluminationVignettingThroughput(profile: IlluminationVignettingProfile): (imagePointMm: LensFieldPointMm) => number {\n const validated = validateProfile(profile);\n return (imagePointMm) => {\n requireFinite(\"imagePointMm.x\", imagePointMm.x);\n requireFinite(\"imagePointMm.y\", imagePointMm.y);\n return throughputAt(validated, imagePointMm).linearThroughputFactor;\n };\n}\n\nfunction assumptions(): readonly string[] {\n return [\n \"Generic rotationally symmetric illumination-vignetting approximation evaluated from ideal image-plane field radius\",\n \"Relative linear throughput is normalized to 1 on the optical axis\",\n \"The model attenuates scene-linear/channel-linear signal only and must be applied before display/gamma encoding\",\n \"The model changes throughput only; it does not alter field coordinates, pupil shape, PSF shape, bokeh, or focus\",\n \"Mechanical/pupil vignetting and cat's-eye bokeh are separate pupil/PSF effects\",\n \"The profile is a generic caller-supplied radial approximation, not calibrated radiometry or a named-lens measurement\"\n ];\n}\n\n/**\n * Calculates generic illumination falloff across the image (vignetting) as a\n * factor that multiplies linear light.\n *\n * A profile is accepted only if its relative throughput stays strictly\n * positive and never exceeds 1 (the value on the optical axis) anywhere in\n * its declared range.\n */\nexport function calculateIlluminationVignetting(\n input: CalculateIlluminationVignettingInput\n): CalculationResult<IlluminationVignetting> {\n requireInputKeys(input, \"calculateIlluminationVignetting\", [\"imagePointMm\", \"profile\"]);\n requireInputObject(input.imagePointMm, \"imagePointMm\");\n requireFinite(\"imagePointMm.x\", input.imagePointMm.x);\n requireFinite(\"imagePointMm.y\", input.imagePointMm.y);\n\n const profile = validateProfile(input.profile);\n const { normalizedRadius, linearThroughputFactor } = throughputAt(profile, input.imagePointMm);\n\n const attenuationStops =\n linearThroughputFactor === 1\n ? 0\n : -Math.log2(linearThroughputFactor);\n\n return approximationResult(\n {\n imagePointMm: { ...input.imagePointMm },\n normalizedRadius,\n linearThroughputFactor,\n attenuationStops,\n profileMinimumThroughputFactor: profile.minimumThroughputFactor,\n profileMaximumThroughputFactor: profile.maximumThroughputFactor\n },\n \"generic-radial-illumination-vignetting\",\n \"1.0.0\",\n assumptions()\n );\n}\n"]}
@@ -42,6 +42,12 @@ export interface EnvironmentLensFieldMapping {
42
42
  profile: BrownConradyDistortionProfile;
43
43
  };
44
44
  radiometricAreaCorrection: "inverse-distortion-jacobian";
45
+ /**
46
+ * #549, optional: what an ideal image point outside a radial projection's image circle (for example beyond r = 2f of the
47
+ * equisolid fisheye) does. Omitted: the query fails, as before. `"backward-ray"`: the query looks straight back (0, 0, −1),
48
+ * a direction no forward scene answers, so a provider that treats directions beyond 90° as empty gives it zero radiance.
49
+ */
50
+ outsideImageCircle?: "backward-ray";
45
51
  }
46
52
  /** @internal Parsed mapping with validated profiles, reused across one plan's queries. */
47
53
  export interface PreparedEnvironmentLensFieldMapping {