@photivra/engine 2.14.1 → 2.15.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 (37) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/CITATION.cff +1 -1
  3. package/README.md +6 -6
  4. package/SECURITY.md +1 -1
  5. package/dist/core/version.d.ts +1 -1
  6. package/dist/core/version.js +1 -1
  7. package/dist/core/version.js.map +1 -1
  8. package/dist/facade/simulate-capture.d.ts +1 -1
  9. package/dist/facade/simulate-capture.js +15 -4
  10. package/dist/facade/simulate-capture.js.map +1 -1
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.js +1 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/optics/defocus-disc-kernel.d.ts +90 -1
  15. package/dist/optics/defocus-disc-kernel.js +58 -5
  16. package/dist/optics/defocus-disc-kernel.js.map +1 -1
  17. package/dist/optics/diffraction-psf-profile.d.ts +9 -4
  18. package/dist/optics/diffraction-psf-profile.js +14 -6
  19. package/dist/optics/diffraction-psf-profile.js.map +1 -1
  20. package/dist/optics/lens-psf-kernels.d.ts +13 -2
  21. package/dist/optics/lens-psf-kernels.js +23 -6
  22. package/dist/optics/lens-psf-kernels.js.map +1 -1
  23. package/dist/optics/site-diffraction-kernel.d.ts +1 -1
  24. package/dist/optics/site-diffraction-kernel.js +37 -17
  25. package/dist/optics/site-diffraction-kernel.js.map +1 -1
  26. package/docs/AGENT_GUIDE.md +1 -0
  27. package/docs/API_REFERENCE.md +12 -1
  28. package/docs/QUICK_START.md +20 -4
  29. package/docs/USAGE.md +17 -0
  30. package/docs/api/core-version.md +1 -1
  31. package/docs/api/exports.json +22 -2
  32. package/docs/api/facade-simulate-capture.md +1 -1
  33. package/docs/api/optics-defocus-disc-kernel.md +86 -0
  34. package/docs/api/optics-diffraction-psf-profile.md +6 -3
  35. package/docs/api/optics-lens-psf-kernels.md +3 -2
  36. package/llms.txt +1 -0
  37. package/package.json +1 -1
@@ -1,7 +1,9 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
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).
3
+ * The geometric defocus disc of a flat scene, sampled on the square site lattice, for
4
+ * `lens.defocus: { model: "geometric-disc" }` (#492). Internal except for the radius cap
5
+ * {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES} and the pre-check {@link calculateDefocusDiscKernelRadius}, which are
6
+ * root exports (#532), so a caller can check a request against the cap before rendering.
5
7
  *
6
8
  * Model. For a scene plane at the subject distance d and a thin lens focused at s, an ideal uniform circular pupil
7
9
  * spreads every in-focus image point into a uniform disc of diameter c = f²·|s − d| / (N·d·(s − f)) on the sensor
@@ -31,17 +33,27 @@
31
33
  * valid kernel). R is capped at {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES}; past it the call fails, naming the
32
34
  * cap and the radius asked for, instead of clipping the disc.
33
35
  */
36
+ import { calculatedResult } from "../core/calculation-result.js";
34
37
  import { portableAsin } from "../core/portable-math.js";
35
- import { InvalidScientificInputError } from "../core/validation.js";
38
+ import { requireInputKeys } from "../core/record-validation.js";
39
+ import { InvalidScientificInputError, requirePositiveFinite } from "../core/validation.js";
36
40
  import { calculateDefocusCircle } from "./depth-of-field.js";
37
41
  /**
38
- * Largest kernel radius in sites, R ≤ 64 (a 129 × 129 window, 16 641 taps per site; the diffraction kernels stop at 32).
42
+ * Largest kernel radius in sites of `lens.defocus: { model: "geometric-disc" }`, R ≤ 64: a disc of at most 129 sites
43
+ * across (a 129 × 129 window, 16 641 taps per site; the diffraction kernels stop at 32). `simulateCapture`,
44
+ * `simulateDisplayView` and `prepareDisplayView` refuse a request whose disc needs more, and never clip the disc; check a
45
+ * request first with {@link calculateDefocusDiscKernelRadius}.
46
+ *
39
47
  * Set from the cost measured in the capture (#492, step 2a; docs/QUICK_START.md, "Optics"): the capture's time grows with
40
48
  * 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
49
  * 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
50
  * 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
51
  */
44
52
  export const DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES = 64;
53
+ /** Whether a kernel radius is within {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES}: the one test the renderer and the pre-check share. */
54
+ function isWithinDefocusDiscKernelLimit(radius) {
55
+ return radius <= DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES;
56
+ }
45
57
  /** Kernel radius in sites for a disc of `diameterSites`: the cells [i − ½, i + ½] the disc reaches with positive area. */
46
58
  export function defocusDiscKernelRadius(diameterSites) {
47
59
  requireDiameter(diameterSites);
@@ -54,7 +66,7 @@ export function defocusDiscKernelRadius(diameterSites) {
54
66
  */
55
67
  export function requireDefocusDiscKernelRadius(diameterSites) {
56
68
  const radius = defocusDiscKernelRadius(diameterSites);
57
- if (radius > DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES)
69
+ if (!isWithinDefocusDiscKernelLimit(radius))
58
70
  throw new InvalidScientificInputError("The defocus disc of " + diameterSites + " sites needs a kernel radius of " + radius +
59
71
  " sites; the limit is " + DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES + " sites (DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES).");
60
72
  return radius;
@@ -152,6 +164,47 @@ export function resolveDefocusDisc(input) {
152
164
  const diameterSites = diameterMicrometers / pitchMicrometers;
153
165
  return { diameterMicrometers, diameterSites, kernelRadiusSites: defocusDiscKernelRadius(diameterSites) };
154
166
  }
167
+ /**
168
+ * Checks, before rendering, whether `lens.defocus: { model: "geometric-disc" }` can render a request: the geometric
169
+ * defocus disc's diameter on the site lattice, the kernel radius it needs, and whether that radius is within
170
+ * {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES} (#532). Pure and cheap: no kernel is built and nothing is rendered.
171
+ *
172
+ * It runs the same resolution and the same limit test as `simulateCapture`, `simulateDisplayView` and `prepareDisplayView`
173
+ * (the blur circle c = f²·|s − d| / (N·d·(s − f)) of `calculateDefocusCircle`, thin-lens model 1.1.0, divided by the pitch),
174
+ * so for the same focal length, f-number, distances and pitch a request they refuse for its defocus size has
175
+ * `withinLimit: false`, and one they accept has `withinLimit: true`. Pass the values the capture uses: in an automatic
176
+ * exposure mode that is the resolved f-number. The limit applies to the disc alone; with `lens.diffraction` the combined
177
+ * kernel (diffraction radius plus defocus radius, at most 32 + 64 sites) has no further limit. Other reasons a capture
178
+ * refuses `lens.defocus` out of focus (camera rotation, projection, distortion, vignetting, pupil sampling, a caller PSF)
179
+ * are not checked here.
180
+ *
181
+ * The blur circle is a geometric thin-lens approximation for an ideal uniform circular pupil (no diffraction, aberrations
182
+ * or pupil vignetting). Inputs follow `calculateDefocusCircle`: all positive and finite, both distances beyond the focal
183
+ * length; invalid values fail with `InvalidScientificInputError`, unknown fields with `InvalidConfigurationError`.
184
+ *
185
+ * @param input Lens, f-number, focus and subject distances, and the sensor's site pitch.
186
+ * @returns The disc and its kernel radius, the limit, and whether the request is within it and by how much it is not.
187
+ */
188
+ export function calculateDefocusDiscKernelRadius(input) {
189
+ requireInputKeys(input, "calculateDefocusDiscKernelRadius", ["focalLengthMm", "aperture", "focusDistanceM", "subjectDistanceM", "pixelPitchMicrometers"]);
190
+ requirePositiveFinite("pixelPitchMicrometers", input.pixelPitchMicrometers);
191
+ const disc = resolveDefocusDisc({ focalLengthMm: input.focalLengthMm, aperture: input.aperture, focusDistanceM: input.focusDistanceM,
192
+ subjectDistanceM: input.subjectDistanceM, pitchMicrometers: input.pixelPitchMicrometers });
193
+ const withinLimit = isWithinDefocusDiscKernelLimit(disc.kernelRadiusSites);
194
+ return calculatedResult({
195
+ diameterMicrometers: disc.diameterMicrometers,
196
+ diameterSites: disc.diameterSites,
197
+ kernelRadiusSites: disc.kernelRadiusSites,
198
+ maximumKernelRadiusSites: DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES,
199
+ maximumDiameterSites: 2 * DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES + 1,
200
+ withinLimit,
201
+ excessKernelRadiusSites: withinLimit ? 0 : disc.kernelRadiusSites - DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES
202
+ }, "defocus-disc-kernel-radius", "1.0.0", [
203
+ "Geometric thin-lens blur circle of an ideal uniform circular pupil (thin-lens-defocus-circle 1.1.0), an approximation",
204
+ "The disc and limit test lens.defocus { model: \"geometric-disc\" } applies in simulateCapture and the display view",
205
+ "Only the defocus size limit is checked"
206
+ ]);
207
+ }
155
208
  /**
156
209
  * The full 2-D convolution of two square site kernels (row-major, radii `radiusA` and `radiusB`): a kernel of radius
157
210
  * 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
@@ -1 +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"]}
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 * The geometric defocus disc of a flat scene, sampled on the square site lattice, for\n * `lens.defocus: { model: \"geometric-disc\" }` (#492). Internal except for the radius cap\n * {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES} and the pre-check {@link calculateDefocusDiscKernelRadius}, which are\n * root exports (#532), so a caller can check a request against the cap before rendering.\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 { calculatedResult, type CalculationResult } from \"../core/calculation-result.js\";\nimport { portableAsin } from \"../core/portable-math.js\";\nimport { requireInputKeys } from \"../core/record-validation.js\";\nimport { InvalidScientificInputError, requirePositiveFinite } from \"../core/validation.js\";\nimport { calculateDefocusCircle } from \"./depth-of-field.js\";\n\n/**\n * Largest kernel radius in sites of `lens.defocus: { model: \"geometric-disc\" }`, R ≤ 64: a disc of at most 129 sites\n * across (a 129 × 129 window, 16 641 taps per site; the diffraction kernels stop at 32). `simulateCapture`,\n * `simulateDisplayView` and `prepareDisplayView` refuse a request whose disc needs more, and never clip the disc; check a\n * request first with {@link calculateDefocusDiscKernelRadius}.\n *\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/** Whether a kernel radius is within {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES}: the one test the renderer and the pre-check share. */\nfunction isWithinDefocusDiscKernelLimit(radius: number): boolean {\n return radius <= DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES;\n}\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 (!isWithinDefocusDiscKernelLimit(radius))\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\nexport interface CalculateDefocusDiscKernelRadiusInput {\n /** Lens focal length in millimetres (`lens.focalLengthMm`). */\n focalLengthMm: number;\n /** F-number the capture uses: `lens.fNumber`, or in an automatic exposure mode the resolved `exposure.settings.fNumber`. */\n aperture: number;\n /** Focus-plane distance from the lens plane in metres (`lens.focusDistanceM`; `simulateCapture` defaults it to the subject distance). */\n focusDistanceM: number;\n /** Scene-plane distance from the lens plane in metres (`subjectDistanceM`). */\n subjectDistanceM: number;\n /** Sensor site pitch in micrometres (`sensor.pixelPitchMicrometers`), the lattice the disc is sampled on. */\n pixelPitchMicrometers: number;\n}\n\nexport interface DefocusDiscKernelRadius {\n /** Geometric blur-circle diameter c at the sensor in micrometres (`calculateDefocusCircle`, thin-lens model 1.1.0); 0 in focus. */\n diameterMicrometers: number;\n /** c / pixel pitch: the disc's diameter in sites. */\n diameterSites: number;\n /** Kernel radius R in sites the renderer needs for this disc, ⌈c / (2·pitch) − ½⌉ (0 for a disc inside one site, and in focus). */\n kernelRadiusSites: number;\n /** {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES}, 64. */\n maximumKernelRadiusSites: number;\n /** Largest disc diameter in sites within the limit, 2·64 + 1 = 129: within the limit exactly when `diameterSites` ≤ 129. */\n maximumDiameterSites: number;\n /** True when `kernelRadiusSites` ≤ `maximumKernelRadiusSites`: `lens.defocus` renders the request; false: it refuses it. */\n withinLimit: boolean;\n /** How far past the limit the kernel radius is, max(0, R − 64) sites; 0 when within the limit. */\n excessKernelRadiusSites: number;\n}\n\n/**\n * Checks, before rendering, whether `lens.defocus: { model: \"geometric-disc\" }` can render a request: the geometric\n * defocus disc's diameter on the site lattice, the kernel radius it needs, and whether that radius is within\n * {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES} (#532). Pure and cheap: no kernel is built and nothing is rendered.\n *\n * It runs the same resolution and the same limit test as `simulateCapture`, `simulateDisplayView` and `prepareDisplayView`\n * (the blur circle c = f²·|s − d| / (N·d·(s − f)) of `calculateDefocusCircle`, thin-lens model 1.1.0, divided by the pitch),\n * so for the same focal length, f-number, distances and pitch a request they refuse for its defocus size has\n * `withinLimit: false`, and one they accept has `withinLimit: true`. Pass the values the capture uses: in an automatic\n * exposure mode that is the resolved f-number. The limit applies to the disc alone; with `lens.diffraction` the combined\n * kernel (diffraction radius plus defocus radius, at most 32 + 64 sites) has no further limit. Other reasons a capture\n * refuses `lens.defocus` out of focus (camera rotation, projection, distortion, vignetting, pupil sampling, a caller PSF)\n * are not checked here.\n *\n * The blur circle is a geometric thin-lens approximation for an ideal uniform circular pupil (no diffraction, aberrations\n * or pupil vignetting). Inputs follow `calculateDefocusCircle`: all positive and finite, both distances beyond the focal\n * length; invalid values fail with `InvalidScientificInputError`, unknown fields with `InvalidConfigurationError`.\n *\n * @param input Lens, f-number, focus and subject distances, and the sensor's site pitch.\n * @returns The disc and its kernel radius, the limit, and whether the request is within it and by how much it is not.\n */\nexport function calculateDefocusDiscKernelRadius(input: CalculateDefocusDiscKernelRadiusInput): CalculationResult<DefocusDiscKernelRadius> {\n requireInputKeys(input, \"calculateDefocusDiscKernelRadius\", [\"focalLengthMm\", \"aperture\", \"focusDistanceM\", \"subjectDistanceM\", \"pixelPitchMicrometers\"]);\n requirePositiveFinite(\"pixelPitchMicrometers\", input.pixelPitchMicrometers);\n const disc = resolveDefocusDisc({ focalLengthMm: input.focalLengthMm, aperture: input.aperture, focusDistanceM: input.focusDistanceM,\n subjectDistanceM: input.subjectDistanceM, pitchMicrometers: input.pixelPitchMicrometers });\n const withinLimit = isWithinDefocusDiscKernelLimit(disc.kernelRadiusSites);\n return calculatedResult({\n diameterMicrometers: disc.diameterMicrometers,\n diameterSites: disc.diameterSites,\n kernelRadiusSites: disc.kernelRadiusSites,\n maximumKernelRadiusSites: DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES,\n maximumDiameterSites: 2 * DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES + 1,\n withinLimit,\n excessKernelRadiusSites: withinLimit ? 0 : disc.kernelRadiusSites - DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES\n }, \"defocus-disc-kernel-radius\", \"1.0.0\", [\n \"Geometric thin-lens blur circle of an ideal uniform circular pupil (thin-lens-defocus-circle 1.1.0), an approximation\",\n \"The disc and limit test lens.defocus { model: \\\"geometric-disc\\\" } applies in simulateCapture and the display view\",\n \"Only the defocus size limit is checked\"\n ]);\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"]}
@@ -42,7 +42,7 @@
42
42
  * so captures at sensor pitch are unchanged; larger cells (binned and whole-frame views) change in the last bits only.
43
43
  *
44
44
  * **Reuse (2.6).** The cells depend only on (β, R) (and the test-only node scale), so `calculateAiryCellEnergies` keeps the
45
- * 256 most recently used results and returns copies: tiles of one frame rendered in the same worker, and captures that
45
+ * 800 most recently used results (256 before 2.15.0; 800 holds two whole 400-node grids) and returns copies: tiles of one frame rendered in the same worker, and captures that
46
46
  * change only the exposure, ISO, scene or output, reuse the kernels instead of integrating them again. The frequency-domain
47
47
  * loop also takes cos θ and sin θ from a per-call table computed with the same expressions, so the values are unchanged.
48
48
  *
@@ -64,9 +64,14 @@ export declare const DIFFRACTION_LIMITED_PSF_PROFILE_MODEL: Readonly<{
64
64
  modelId: "photivra-diffraction-limited-circular-pupil-psf";
65
65
  modelVersion: "1.0.0";
66
66
  }>;
67
- /** Input limits: at most 64 wavelengths, kernel radius 0–32 samples (the irradiance-convolution limit). */
67
+ /**
68
+ * Input limits: at most 400 wavelengths, kernel radius 0–32 samples (the irradiance-convolution limit). 400 is the finest spectral
69
+ * sampling of `simulateCapture` and the display view (`sensor.spectralStepNanometers` 1 nm over 380–780 nm), so `lens.diffraction`
70
+ * builds one exact kernel per spectral node at every allowed step; it was 64 before 2.15.0, which refused every step under 6.25 nm.
71
+ * The work and memory are linear in the count (measured in docs/PSF_FOUNDATION.md, "Spectral nodes").
72
+ */
68
73
  export declare const DIFFRACTION_LIMITED_PSF_PROFILE_LIMITS: Readonly<{
69
- maximumWavelengths: 64;
74
+ maximumWavelengths: 400;
70
75
  maximumKernelRadiusSamples: 32;
71
76
  }>;
72
77
  /**
@@ -80,7 +85,7 @@ export interface CreateDiffractionLimitedPsfProfileInput {
80
85
  focalLengthMm: number;
81
86
  apertureFNumber: number;
82
87
  focus: FocusPlane;
83
- /** Strictly increasing wavelengths, nm, in the medium of propagation (no air/vacuum conversion is applied). 1–64. */
88
+ /** Strictly increasing wavelengths, nm, in the medium of propagation (no air/vacuum conversion is applied). 1–400. */
84
89
  wavelengthsNm: readonly number[];
85
90
  /** Square sample pitch of every kernel, µm. */
86
91
  samplePitchMicrometers: number;
@@ -43,7 +43,7 @@
43
43
  * so captures at sensor pitch are unchanged; larger cells (binned and whole-frame views) change in the last bits only.
44
44
  *
45
45
  * **Reuse (2.6).** The cells depend only on (β, R) (and the test-only node scale), so `calculateAiryCellEnergies` keeps the
46
- * 256 most recently used results and returns copies: tiles of one frame rendered in the same worker, and captures that
46
+ * 800 most recently used results (256 before 2.15.0; 800 holds two whole 400-node grids) and returns copies: tiles of one frame rendered in the same worker, and captures that
47
47
  * change only the exposure, ISO, scene or output, reuse the kernels instead of integrating them again. The frequency-domain
48
48
  * loop also takes cos θ and sin θ from a per-call table computed with the same expressions, so the values are unchanged.
49
49
  *
@@ -66,8 +66,13 @@ import { parseLensSampledPsfProfile } from "./lens-psf-profile.js";
66
66
  export const DIFFRACTION_LIMITED_PSF_PROFILE_MODEL = /* @__PURE__ */ Object.freeze({
67
67
  modelId: "photivra-diffraction-limited-circular-pupil-psf", modelVersion: "1.0.0"
68
68
  });
69
- /** Input limits: at most 64 wavelengths, kernel radius 0–32 samples (the irradiance-convolution limit). */
70
- export const DIFFRACTION_LIMITED_PSF_PROFILE_LIMITS = /* @__PURE__ */ Object.freeze({ maximumWavelengths: 64, maximumKernelRadiusSamples: 32 });
69
+ /**
70
+ * Input limits: at most 400 wavelengths, kernel radius 0–32 samples (the irradiance-convolution limit). 400 is the finest spectral
71
+ * sampling of `simulateCapture` and the display view (`sensor.spectralStepNanometers` 1 nm over 380–780 nm), so `lens.diffraction`
72
+ * builds one exact kernel per spectral node at every allowed step; it was 64 before 2.15.0, which refused every step under 6.25 nm.
73
+ * The work and memory are linear in the count (measured in docs/PSF_FOUNDATION.md, "Spectral nodes").
74
+ */
75
+ export const DIFFRACTION_LIMITED_PSF_PROFILE_LIMITS = /* @__PURE__ */ Object.freeze({ maximumWavelengths: 400, maximumKernelRadiusSamples: 32 });
71
76
  /**
72
77
  * Internal: β = p·f_c (cell side over the cut-off period) from which `calculateAiryCellEnergies` integrates the cells in
73
78
  * real space (2.5). Above every β of a `simulateCapture` pitch up to 100 µm, whose captures keep the frequency-domain values.
@@ -156,8 +161,11 @@ export function calculateAiryCellEnergies(beta, radius, scale = 1) {
156
161
  AIRY_CELL_CACHE.delete(AIRY_CELL_CACHE.keys().next().value);
157
162
  return result;
158
163
  }
159
- /** Memoized cell energies (2.6): 256 entries of (R + 1)² values, at most 256 × 33² × 8 bytes ≈ 2.2 MB. */
160
- const AIRY_CELL_CACHE_ENTRIES = 256;
164
+ /**
165
+ * Memoized cell energies (2.6): 800 entries of (R + 1)² values, at most 800 × 33² × 8 bytes ≈ 7.0 MB. 2.15.0: 800 (was 256), so the
166
+ * least-recently-used order still reuses a whole grid of the finest spectral sampling (400 nodes), which 256 entries never did.
167
+ */
168
+ const AIRY_CELL_CACHE_ENTRIES = 800;
161
169
  const AIRY_CELL_CACHE = new Map();
162
170
  /** Hankel asymptotic coefficients a_k(ν) of DLMF 10.17.1 for ν = 0 and 1, k = 0..15. */
163
171
  const HANKEL = (() => {
@@ -363,7 +371,7 @@ export function createDiffractionLimitedPsfProfile(input) {
363
371
  const fieldX = positive(field.x, "fieldHalfExtentMm.x"), fieldY = positive(field.y, "fieldHalfExtentMm.y");
364
372
  const list = r.wavelengthsNm;
365
373
  if (!Array.isArray(list) || list.length < 1 || list.length > L.maximumWavelengths)
366
- throw new InvalidConfigurationError("wavelengthsNm must list 1 to 64 wavelengths.");
374
+ throw new InvalidConfigurationError("wavelengthsNm must list 1 to " + L.maximumWavelengths + " wavelengths.");
367
375
  const wavelengths = list.map((value, index) => positive(value, "wavelengthsNm[" + index + "]"));
368
376
  for (let i = 1; i < wavelengths.length; i++)
369
377
  if (!(wavelengths[i] > wavelengths[i - 1]))
@@ -1 +1 @@
1
- {"version":3,"file":"diffraction-psf-profile.js","sourceRoot":"","sources":["../../src/optics/diffraction-psf-profile.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Builds a `LensSampledPsfProfile` for an ideal, aberration-free, in-focus lens with a uniformly lit\n * circular pupil: the Airy pattern of scalar Fraunhofer diffraction, one unit-sum kernel per declared\n * wavelength on ONE shared sample grid (2.1.0).\n *\n * **Model.** The pupil diameter is D = f/N (entrance pupil = exit pupil, unity pupil magnification) and\n * the pupil-to-image distance z is the thin-lens image distance for the focus (f at infinity), so the\n * image-side (working) f-number is N_w = z/D. The incoherent optical transfer function (OTF) of a\n * uniform circular pupil is the normalized autocorrelation of the pupil,\n * H(ρ) = (2/π)·(acos ρ − ρ·√(1 − ρ²)), ρ = |f|/f_c ≤ 1, f_c = 1/(λ·N_w),\n * and the intensity PSF is its inverse Fourier transform (the Airy pattern, first zero at 1.21967·λ·N_w)\n * (MIT OpenCourseWare 2.71 Optics, Spring 2009, lectures 21–22, free: the intensity PSF as the transform of\n * the pupil autocorrelation and the circular-pupil MTF; also see: Goodman, Introduction to Fourier Optics,\n * 4th ed., 2017, §7.3).\n *\n * **Cell integration (exact, no Bessel functions).** Kernel sample (i, j) is the PSF energy inside the\n * square cell of side p centred on (i·p, j·p). The cell integral is the PSF convolved with the cell's\n * indicator, evaluated at the cell centre, so in the frequency domain\n * K(i, j) = p² ∫∫ H(|f|/f_c)·sinc(p·f_x)·sinc(p·f_y)·cos(2π·p·i·f_x)·cos(2π·p·j·f_y) d²f,\n * with sinc(x) = sin(πx)/(πx) (the sine terms vanish because H is even). It is evaluated in polar\n * coordinates: a trapezoid rule in angle (exponentially convergent for this smooth periodic integrand) and\n * Gauss–Legendre in t with ρ = 1 − t², which removes the (1 − ρ)^{3/2} edge behaviour of H. The node counts\n * follow from the integrand's bandwidth with a fixed margin; the tests check convergence by doubling them and\n * check the values against an independent real-space integration of `calculateIdealPolygonDiffractionPsf` in\n * its many-blade (circular) limit. The sum of all cells is exactly 1 (Parseval), so the energy captured by\n * the finite kernel is Σ K over its window and the truncated energy is 1 − Σ K.\n *\n * **Cells much larger than the pattern (2.5; threshold lowered to β = 32 after 2.8.1).** The frequency-domain node counts grow with β = p·f_c (the\n * cell side over the cut-off period), so the work grows as β². From β ≥ `AIRY_REAL_SPACE_MINIMUM_BETA` (32) the cells are\n * integrated in real space instead, with the same model and no frequency integral: the Airy encircled energy is\n * EE(r) = 1 − J₀(x)² − J₁(x)², x = π·r·f_c (it follows from J₀′ = −J₁ and J₁′ = J₀ − J₁/x, DLMF §10.6(i), free), so\n * the energy of the rectangle [0, X] × [0, Y] is (1/2π)∫ EE(r_edge(θ)) dθ over its quadrant, a one-dimensional\n * integral in θ. Every cell is a signed sum of such rectangles. J₀² + J₁² comes from the Hankel asymptotic expansion\n * (DLMF §10.17(i), eq. 10.17.3, free) from x = 300, with eight terms in each of P and Q (remainder under 1e-17,\n * DLMF §10.17(iii)), and below it from Miller's backward recurrence normalized by J₀ + 2ΣJ₂ₖ = 1 (DLMF §3.6(iii),\n * 10.12.4, free). The θ integral uses 16-point Gauss–Legendre panels on which the phase 2x changes by at most\n * 2 radians, so the work grows only as β. The tests check the two methods against each other from β = 32 to 400, the\n * real-space cells against the asymptotic leakage 8√2/(π³·β) of the centre cell, and convergence under doubled panel\n * counts. The threshold was 192 in 2.5; it moved to 32 because from there the real-space cells agree with the frequency-domain cells\n * within about 1e-12 and cost a few to a few hundred times less (a 750 µm whole-frame cell at f/8 took 3.4 s for 20\n * wavelengths). Every β a native pitch up to 10 µm reaches (at most 10/(0.38·1.4) ≈ 19) stays in the frequency domain,\n * so captures at sensor pitch are unchanged; larger cells (binned and whole-frame views) change in the last bits only.\n *\n * **Reuse (2.6).** The cells depend only on (β, R) (and the test-only node scale), so `calculateAiryCellEnergies` keeps the\n * 256 most recently used results and returns copies: tiles of one frame rendered in the same worker, and captures that\n * change only the exposure, ISO, scene or output, reuse the kernels instead of integrating them again. The frequency-domain\n * loop also takes cos θ and sin θ from a per-call table computed with the same expressions, so the values are unchanged.\n *\n * **Truncation and normalization.** The kernel is the (2R+1)² window of cells. The Airy tails fall only as\n * r⁻³, so a finite window always misses some energy: it is reported per wavelength, and the window is then\n * renormalized to unit sum because the sampled-profile contract requires unit-energy kernels. The missing\n * tail energy is thereby put back inside the window (an approximation that slightly raises low-frequency\n * contrast compared with the untruncated pattern).\n *\n * **Approximations (labelled):** scalar (no polarization), aberration-free, uniform pupil amplitude, exactly in\n * focus (no defocus), thin lens with unity pupil magnification, the on-axis PSF used across the declared field\n * (isoplanatic), PSF energy binned into the sample cells, finite window renormalized. Not a measured lens.\n * @see docs/PSF_FOUNDATION.md, \"Diffraction-limited sampled profile (2.1)\".\n */\n\nimport { approximationResult, type CalculationResult } from \"../core/calculation-result.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { requireAllowlistedRecord } from \"../core/record-validation.js\";\nimport type { EvidenceProvenance } from \"../core/evidence-provenance.js\";\nimport type { FocusPlane } from \"./focus-state.js\";\nimport { calculateThinLensImageDistance } from \"./thin-lens.js\";\nimport { parseLensSampledPsfProfile, type LensSampledPsfProfile } from \"./lens-psf-profile.js\";\n\nexport const DIFFRACTION_LIMITED_PSF_PROFILE_MODEL = /* @__PURE__ */ Object.freeze({\n modelId: \"photivra-diffraction-limited-circular-pupil-psf\" as const, modelVersion: \"1.0.0\" as const });\n/** Input limits: at most 64 wavelengths, kernel radius 0–32 samples (the irradiance-convolution limit). */\nexport const DIFFRACTION_LIMITED_PSF_PROFILE_LIMITS = /* @__PURE__ */ Object.freeze({ maximumWavelengths: 64, maximumKernelRadiusSamples: 32 });\n/**\n * Internal: β = p·f_c (cell side over the cut-off period) from which `calculateAiryCellEnergies` integrates the cells in\n * real space (2.5). Above every β of a `simulateCapture` pitch up to 100 µm, whose captures keep the frequency-domain values.\n */\nexport const AIRY_REAL_SPACE_MINIMUM_BETA = 32;\n/** Radius of the first dark ring of the Airy pattern in units of λ·N_w: j₁,₁/π, j₁,₁ = 3.8317059702 (first zero of J₁). */\nconst AIRY_FIRST_ZERO_PER_LAMBDA_N = 3.8317059702075125 / Math.PI;\n\nexport interface CreateDiffractionLimitedPsfProfileInput {\n /** Only `circular` in 2.1.0 (polygon pupils: `calculateIdealPolygonDiffractionPsf` gives point densities). */\n aperture: \"circular\";\n focalLengthMm: number;\n apertureFNumber: number;\n focus: FocusPlane;\n /** Strictly increasing wavelengths, nm, in the medium of propagation (no air/vacuum conversion is applied). 1–64. */\n wavelengthsNm: readonly number[];\n /** Square sample pitch of every kernel, µm. */\n samplePitchMicrometers: number;\n /** Kernel half-width in samples; each kernel is (2R + 1)². 0–32. */\n kernelRadiusSamples: number;\n /** Half-extent of the image field over which the on-axis PSF is declared (profile field axes ±x, ±y), mm. */\n fieldHalfExtentMm: { x: number; y: number };\n profileId: string;\n}\n\nexport interface DiffractionLimitedPsfWavelength {\n wavelengthNm: number;\n /** f_c = 1/(λ·N_w), cycles per µm. */\n cutoffFrequencyCyclesPerMicrometer: number;\n /** 1.21967·λ·N_w, µm. */\n firstZeroRadiusMicrometers: number;\n /** Σ of the cell energies inside the window before renormalization (of a total of 1). */\n capturedEnergyFraction: number;\n /** 1 − capturedEnergyFraction: the Airy tail energy outside the window, put back by renormalization. */\n truncatedEnergyFraction: number;\n /** Fraction of the renormalized kernel in the centre sample. */\n centreSampleFraction: number;\n}\n\nexport interface DiffractionLimitedPsfProfile {\n profile: LensSampledPsfProfile;\n model: typeof DIFFRACTION_LIMITED_PSF_PROFILE_MODEL;\n pupilDiameterMm: number;\n pupilToImageDistanceMm: number;\n workingFNumber: number;\n kernelRadiusSamples: number;\n samplePitchMicrometers: number;\n wavelengths: readonly DiffractionLimitedPsfWavelength[];\n /** Largest truncated energy over the wavelengths. */\n maximumTruncatedEnergyFraction: number;\n /**\n * How the cells were integrated. 2.5: wavelengths with β = p·f_c ≥ `AIRY_REAL_SPACE_MINIMUM_BETA` (192 in 2.5–2.8.1, then 32) use the real-space\n * encircled-energy method; `realSpaceWavelengthsNm` lists them, and `method` is `\"frequency-domain-and-real-space\"` when both ran.\n * `maximumRadialNodes` counts frequency-domain radial nodes (0 when only the real-space method ran); `maximumAngularNodes` the\n * angular nodes of either method (for the real-space method, all θ nodes of one wavelength).\n */\n quadrature: { method: \"polar-frequency-domain-trapezoid-gauss-legendre\" | \"real-space-encircled-energy-gauss-legendre\" | \"frequency-domain-and-real-space\";\n maximumRadialNodes: number; maximumAngularNodes: number; realSpaceWavelengthsNm?: readonly number[] };\n}\n\nconst EVIDENCE: readonly EvidenceProvenance[] = /* @__PURE__ */ Object.freeze([/* @__PURE__ */ Object.freeze({ sourceOrigin: \"photivra\" as const,\n sourceReference: \"photivra:diffraction-limited-psf-profile\", reuseStatus: \"photivra-owned\" as const })]);\nconst LIMITATIONS = /* @__PURE__ */ Object.freeze([\n \"Scalar Fraunhofer diffraction of an ideal, uniformly lit circular pupil (Airy pattern); no aberration, apodization, polarization or stray light.\",\n \"Exactly in focus: no defocus or depth of field. Thin lens with unity pupil magnification; the pupil-to-image distance is the thin-lens image distance.\",\n \"The on-axis PSF is declared unchanged across the field (isoplanatic); no field-dependent aberration or pupil clipping.\",\n \"PSF energy is binned into square sample cells; the finite window misses the Airy tail energy listed per wavelength and is renormalized to unit sum.\",\n \"Wavelengths are used as given, in the declared basis, without air/vacuum conversion.\"\n]);\n\nfunction positive(value: unknown, name: string): number {\n if (typeof value !== \"number\" || !Number.isFinite(value) || value <= 0) throw new InvalidConfigurationError(name + \" must be a positive finite number.\");\n return value;\n}\n\n/** Gauss–Legendre nodes and weights on [0, 1] (Newton iteration on Pₙ; DLMF §3.5(v)). Cached per n. */\nconst GAUSS_LEGENDRE = new Map<number, { nodes: Float64Array; weights: Float64Array }>();\nfunction gaussLegendre(n: number): { nodes: Float64Array; weights: Float64Array } {\n const cached = GAUSS_LEGENDRE.get(n);\n if (cached) return cached;\n const nodes = new Float64Array(n), weights = new Float64Array(n);\n for (let k = 0; k < Math.ceil(n / 2); k++) {\n let x = Math.cos(Math.PI * (k + 0.75) / (n + 0.5));\n for (let iteration = 0; iteration < 100; iteration++) {\n let p0 = 1, p1 = x;\n for (let m = 2; m <= n; m++) { const p2 = ((2 * m - 1) * x * p1 - (m - 1) * p0) / m; p0 = p1; p1 = p2; }\n const step = p1 / (n * (x * p1 - p0) / (x * x - 1));\n x -= step;\n if (Math.abs(step) < 1e-16) break;\n }\n let p0 = 1, p1 = x;\n for (let m = 2; m <= n; m++) { const p2 = ((2 * m - 1) * x * p1 - (m - 1) * p0) / m; p0 = p1; p1 = p2; }\n const derivative = n * (x * p1 - p0) / (x * x - 1);\n const w = 1 / ((1 - x * x) * derivative * derivative);\n // Map [-1, 1] to [0, 1]: t = (1 ± x)/2, weight w (2/(1-x²)P'² halved).\n nodes[k] = (1 - x) / 2; weights[k] = w;\n nodes[n - 1 - k] = (1 + x) / 2; weights[n - 1 - k] = w;\n }\n const result = { nodes, weights };\n if (GAUSS_LEGENDRE.size < 64) GAUSS_LEGENDRE.set(n, result);\n return result;\n}\nfunction sinc(x: number): number {\n if (x === 0) return 1;\n const a = Math.PI * x;\n return Math.sin(a) / a;\n}\n\n/**\n * Cell energies K(i, j), 0 ≤ i, j ≤ R, of the Airy pattern for β = p·f_c (cell side in units of the cut-off\n * period): in the frequency domain below `AIRY_REAL_SPACE_MINIMUM_BETA`, in real space from it (2.5). `scale`\n * multiplies the node or panel counts (1 in production; the tests use 2 to check convergence).\n */\nexport function calculateAiryCellEnergies(beta: number, radius: number, scale = 1): { cells: Float64Array; radialNodes: number; angularNodes: number; realSpace: boolean } {\n // 2.6: a pure function of (β, R, scale), so results are memoized (most recently used first) and handed out as copies.\n // Tiles of one frame, and captures that change only the exposure or the scene, rebuild the same kernels.\n const key = beta + \"|\" + radius + \"|\" + scale, cached = AIRY_CELL_CACHE.get(key);\n if (cached) {\n AIRY_CELL_CACHE.delete(key); AIRY_CELL_CACHE.set(key, cached);\n return { ...cached, cells: cached.cells.slice() };\n }\n const result = beta >= AIRY_REAL_SPACE_MINIMUM_BETA ? { ...calculateAiryCellEnergiesInRealSpace(beta, radius, scale), radialNodes: 0, realSpace: true } :\n { ...calculateAiryCellEnergiesInFrequencyDomain(beta, radius, scale), realSpace: false };\n AIRY_CELL_CACHE.set(key, { ...result, cells: result.cells.slice() });\n if (AIRY_CELL_CACHE.size > AIRY_CELL_CACHE_ENTRIES) AIRY_CELL_CACHE.delete(AIRY_CELL_CACHE.keys().next().value!);\n return result;\n}\n/** Memoized cell energies (2.6): 256 entries of (R + 1)² values, at most 256 × 33² × 8 bytes ≈ 2.2 MB. */\nconst AIRY_CELL_CACHE_ENTRIES = 256;\nconst AIRY_CELL_CACHE = new Map<string, { cells: Float64Array; radialNodes: number; angularNodes: number; realSpace: boolean }>();\n\n/** Hankel asymptotic coefficients a_k(ν) of DLMF 10.17.1 for ν = 0 and 1, k = 0..15. */\nconst HANKEL = ((): { a0: Float64Array; a1: Float64Array } => {\n const a0 = new Float64Array(16), a1 = new Float64Array(16);\n a0[0] = 1; a1[0] = 1;\n for (let k = 1; k < 16; k++) {\n const odd = (2 * k - 1) * (2 * k - 1);\n a0[k] = a0[k - 1]! * (0 - odd) / (8 * k);\n a1[k] = a1[k - 1]! * (4 - odd) / (8 * k);\n }\n return { a0, a1 };\n})();\n/** Smallest argument of `besselSquareSum`: there the 16-term expansion's remainder is below 1e-17. */\nconst HANKEL_MINIMUM_ARGUMENT = 300;\n\n/**\n * J₀(x)² + J₁(x)² for x > 0. From x ≥ 300 by the Hankel expansion J_ν(x) = √(2/(πx))·(P_ν cos ω_ν − Q_ν sin ω_ν), ω_ν = x − νπ/2 − π/4,\n * P_ν = Σ(−1)^k a_2k(ν)/x^2k, Q_ν = Σ(−1)^k a_2k+1(ν)/x^2k+1 (DLMF 10.17.3), eight terms each. With ω₁ = ω₀ − π/2,\n * J₁ = √(2/(πx))·(P₁ sin ω₀ + Q₁ cos ω₀). Below 300 by Miller's backward recurrence J_{n−1} = (2n/x)·J_n − J_{n+1} (DLMF 10.6.1;\n * the algorithm, DLMF §3.6(iii)), started at an order above x where J_n is negligible, normalized by J₀ + 2·Σ J_2k = 1\n * (DLMF 10.12.4). Internal.\n */\nexport function besselSquareSum(x: number): number {\n if (!(x > 0) || !Number.isFinite(x)) throw new InvalidConfigurationError(\"J0 and J1 need a positive finite argument.\");\n if (x < HANKEL_MINIMUM_ARGUMENT) return besselSquareSumByRecurrence(x);\n const { a0, a1 } = HANKEL, inverse = 1 / x;\n let p0 = 0, q0 = 0, p1 = 0, q1 = 0, power = 1;\n for (let k = 0; k < 16; k++) {\n const sign = (k >> 1) % 2 === 0 ? 1 : -1;\n if (k % 2 === 0) { p0 += sign * a0[k]! * power; p1 += sign * a1[k]! * power; } else { q0 += sign * a0[k]! * power; q1 += sign * a1[k]! * power; }\n power *= inverse;\n }\n const omega = x - Math.PI / 4, c = Math.cos(omega), s = Math.sin(omega);\n const j0 = p0 * c - q0 * s, j1 = p1 * s + q1 * c;\n return 2 / (Math.PI * x) * (j0 * j0 + j1 * j1);\n}\n/**\n * Miller's backward recurrence for 0 < x < 300 (see `besselSquareSum`). The start order x + 10·∛x + 40 (even) lies far enough in the\n * region where J_n(x) falls faster than exponentially that the start values' error is below double precision at J₀ and J₁; the\n * values are rescaled when they grow large, and normalized before squaring.\n */\nfunction besselSquareSumByRecurrence(x: number): number {\n const start = 2 * Math.ceil((x + 10 * Math.cbrt(x) + 40) / 2);\n let next = 0, current = 1e-100, sum = 0, j1 = 0;\n for (let n = start; n >= 1; n--) {\n const previous = (2 * n / x) * current - next;\n next = current; current = previous;\n if (n - 1 === 1) j1 = current;\n if (n - 1 > 0 && (n - 1) % 2 === 0) sum += 2 * current;\n if (Math.abs(current) > 1e100) { current *= 1e-100; next *= 1e-100; sum *= 1e-100; j1 *= 1e-100; }\n }\n sum += current;\n const j0 = current / sum, j1n = j1 / sum;\n return j0 * j0 + j1n * j1n;\n}\n\n/**\n * Real-space cell energies (2.5): see the module comment. G(Z, φ) = ∫₀^φ [J₀² + J₁²](π·β·Z/cos θ) dθ for cell boundaries at\n * half-integer distances Z (in cells), accumulated along θ for each Z with every needed φ = atan(W/Z) as a panel boundary.\n * The rectangle [0, X] × [0, Y] holds 1/4 − (G(X, atan(Y/X)) + G(Y, atan(X/Y)))/(2π) of the energy. Internal.\n */\nexport function calculateAiryCellEnergiesInRealSpace(beta: number, radius: number, scale = 1): { cells: Float64Array; angularNodes: number } {\n const size = radius + 1, table = new Float64Array(size * size), { nodes, weights } = gaussLegendre(16);\n let angularNodes = 0;\n for (let z = 0; z < size; z++) {\n const Z = z + 0.5, k = Math.PI * beta * Z;\n let sum = 0, t0 = 1, theta0 = 0;\n for (let w = 0; w < size; w++) {\n // Panels equal in t = 1/cos θ, so the phase 2x = 2kt changes by at most 2 radians on each.\n const t1 = Math.hypot(Z, w + 0.5) / Z, theta1 = Math.atan2(w + 0.5, Z), panels = Math.max(1, Math.ceil(scale * k * (t1 - t0)));\n let a = theta0;\n for (let n = 1; n <= panels; n++) {\n const b = n === panels ? theta1 : Math.acos(1 / (t0 + (t1 - t0) * n / panels)), h = b - a;\n let panel = 0;\n for (let g = 0; g < 16; g++) panel += weights[g]! * besselSquareSum(k / Math.cos(a + h * nodes[g]!));\n sum += panel * h;\n a = b;\n }\n angularNodes += 16 * panels;\n table[z * size + w] = sum;\n t0 = t1; theta0 = theta1;\n }\n }\n // Boundaries of cell i along one axis as (half-integer index, weight): the centre cell is twice [0, 1/2].\n const bounds = (i: number): [number, number][] => i === 0 ? [[0, 2]] : [[i, 1], [i - 1, -1]];\n const cells = new Float64Array(size * size);\n for (let j = 0; j < size; j++) for (let i = 0; i < size; i++) {\n let constant = 0, leak = 0;\n for (const [a, wa] of bounds(i)) for (const [b, wb] of bounds(j)) {\n constant += wa * wb;\n leak += wa * wb * (table[a * size + b]! + table[b * size + a]!);\n }\n cells[j * size + i] = constant / 4 - leak / (2 * Math.PI);\n }\n return { cells, angularNodes };\n}\n\n/** Frequency-domain cell energies (2.1; see the module comment). Internal. */\nexport function calculateAiryCellEnergiesInFrequencyDomain(beta: number, radius: number, scale = 1): { cells: Float64Array; radialNodes: number; angularNodes: number } {\n // Bandwidth of the angular and radial integrands: c = π·β·(2R + 1)·√2 (the sinc factors and the highest cosine).\n const c = Math.PI * beta * (2 * radius + 1) * Math.SQRT2, margin = 10 * Math.cbrt(c) + 24;\n const quarter = Math.ceil(scale * (c + margin) / 4), radialNodes = Math.ceil(scale * (c + margin));\n const { nodes, weights } = gaussLegendre(radialNodes), size = radius + 1, cells = new Float64Array(size * size);\n const ca = new Float64Array(size), cb = new Float64Array(size), angularStep = (Math.PI / 2) / quarter;\n // cos θ and sin θ of every angular node, computed once with the same expressions as inside the loop (same values).\n const cosines = new Float64Array(quarter + 1), sines = new Float64Array(quarter + 1);\n for (let k = 0; k <= quarter; k++) { const theta = k * angularStep; cosines[k] = Math.cos(theta); sines[k] = Math.sin(theta); }\n for (let r = 0; r < radialNodes; r++) {\n const t = nodes[r]!, rho = 1 - t * t;\n // H(1 − t²) without cancellation: acos(1 − t²) = 2·asin(t/√2), ρ·√(1 − ρ²) = ρ·t·√(2 − t²).\n const otf = (2 / Math.PI) * (2 * Math.asin(t / Math.SQRT2) - rho * t * Math.sqrt(2 - t * t));\n // ∫₀¹ g(ρ) dρ = ∫₀¹ g(1 − t²)·2t dt; the 4 is the four symmetric quadrants of the angular trapezoid.\n const radialWeight = weights[r]! * 2 * t * rho * otf * 4 * angularStep;\n for (let k = 0; k <= quarter; k++) {\n const x = beta * rho * cosines[k]!, y = beta * rho * sines[k]!;\n const w = radialWeight * (k === 0 || k === quarter ? 0.5 : 1) * sinc(x) * sinc(y);\n // cos(2π·i·x) and cos(2π·j·y) by the Chebyshev recurrence.\n const cx = Math.cos(2 * Math.PI * x), cy = Math.cos(2 * Math.PI * y);\n ca[0] = 1; cb[0] = 1;\n if (size > 1) { ca[1] = cx; cb[1] = cy; }\n for (let i = 2; i < size; i++) { ca[i] = 2 * cx * ca[i - 1]! - ca[i - 2]!; cb[i] = 2 * cy * cb[i - 1]! - cb[i - 2]!; }\n for (let j = 0; j < size; j++) {\n const wj = w * cb[j]!;\n for (let i = 0; i < size; i++) { const o = j * size + i; cells[o] = cells[o]! + wj * ca[i]!; }\n }\n }\n }\n const area = beta * beta;\n for (let i = 0; i < cells.length; i++) cells[i] = cells[i]! * area;\n return { cells, radialNodes, angularNodes: 4 * quarter };\n}\n\n/**\n * Creates a sampled lens PSF profile for an ideal diffraction-limited lens with a circular pupil: one\n * unit-sum Airy kernel per wavelength on one shared (2R+1)² grid, with the truncated energy reported per\n * wavelength. The profile declares the given focal length, f-number and focus exactly, and the field axes\n * ±`fieldHalfExtentMm` with the same kernel at every field node.\n * @param input - See `CreateDiffractionLimitedPsfProfileInput`.\n * @returns The profile and its diffraction record. An approximation; not a measured lens.\n * @see docs/PSF_FOUNDATION.md, \"Diffraction-limited sampled profile (2.1)\".\n */\nexport function createDiffractionLimitedPsfProfile(input: CreateDiffractionLimitedPsfProfileInput): CalculationResult<DiffractionLimitedPsfProfile> {\n const r = requireAllowlistedRecord(input, [\"aperture\", \"focalLengthMm\", \"apertureFNumber\", \"focus\", \"wavelengthsNm\", \"samplePitchMicrometers\",\n \"kernelRadiusSamples\", \"fieldHalfExtentMm\", \"profileId\"], \"Invalid diffraction-limited PSF profile input.\");\n if (r.aperture !== \"circular\") throw new InvalidConfigurationError(\"aperture must be \\\"circular\\\".\");\n const focalLengthMm = positive(r.focalLengthMm, \"focalLengthMm\"), fNumber = positive(r.apertureFNumber, \"apertureFNumber\");\n const pitch = positive(r.samplePitchMicrometers, \"samplePitchMicrometers\");\n const L = DIFFRACTION_LIMITED_PSF_PROFILE_LIMITS;\n const radius = r.kernelRadiusSamples;\n if (typeof radius !== \"number\" || !Number.isSafeInteger(radius) || radius < 0 || radius > L.maximumKernelRadiusSamples) {\n throw new InvalidConfigurationError(\"kernelRadiusSamples must be an integer from 0 to \" + L.maximumKernelRadiusSamples + \".\");\n }\n const focus = requireAllowlistedRecord(r.focus, [\"kind\", \"distanceM\"], \"Invalid focus.\");\n let focusDiopters: number, distanceMm: number;\n if (focus.kind === \"infinity\") {\n if (focus.distanceM !== undefined) throw new InvalidConfigurationError(\"An infinity focus has no distanceM.\");\n focusDiopters = 0; distanceMm = focalLengthMm;\n } else if (focus.kind === \"finite\") {\n const distanceM = positive(focus.distanceM, \"focus.distanceM\");\n focusDiopters = 1 / distanceM;\n distanceMm = calculateThinLensImageDistance({ focalLengthMm, objectDistanceM: distanceM }).value.imageDistanceMm;\n } else throw new InvalidConfigurationError(\"focus.kind must be finite or infinity.\");\n const field = requireAllowlistedRecord(r.fieldHalfExtentMm, [\"x\", \"y\"], \"Invalid fieldHalfExtentMm.\");\n const fieldX = positive(field.x, \"fieldHalfExtentMm.x\"), fieldY = positive(field.y, \"fieldHalfExtentMm.y\");\n const list = r.wavelengthsNm;\n if (!Array.isArray(list) || list.length < 1 || list.length > L.maximumWavelengths) throw new InvalidConfigurationError(\"wavelengthsNm must list 1 to 64 wavelengths.\");\n const wavelengths = (list as unknown[]).map((value, index) => positive(value, \"wavelengthsNm[\" + index + \"]\"));\n for (let i = 1; i < wavelengths.length; i++) if (!(wavelengths[i]! > wavelengths[i - 1]!)) throw new InvalidConfigurationError(\"wavelengthsNm must be strictly increasing.\");\n if (typeof r.profileId !== \"string\" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(r.profileId)) throw new InvalidConfigurationError(\"profileId must be a public opaque ID.\");\n\n const pupilDiameterMm = focalLengthMm / fNumber, workingFNumber = distanceMm / pupilDiameterMm, size = 2 * radius + 1;\n let maximumRadialNodes = 0, maximumAngularNodes = 0, maximumTruncated = 0;\n const records: DiffractionLimitedPsfWavelength[] = [], kernels: number[][] = [], realSpaceWavelengthsNm: number[] = [];\n for (const nm of wavelengths) {\n const lambdaN = nm / 1000 * workingFNumber, cutoff = 1 / lambdaN;\n const airy = calculateAiryCellEnergies(pitch * cutoff, radius);\n if (airy.realSpace) realSpaceWavelengthsNm.push(nm);\n maximumRadialNodes = Math.max(maximumRadialNodes, airy.radialNodes); maximumAngularNodes = Math.max(maximumAngularNodes, airy.angularNodes);\n const values: number[] = new Array(size * size);\n let captured = 0;\n for (let ky = 0; ky < size; ky++) for (let kx = 0; kx < size; kx++) {\n const value = airy.cells[Math.abs(ky - radius) * (radius + 1) + Math.abs(kx - radius)]!;\n // Every cell energy of a non-negative PSF is positive; a negative value would be a quadrature failure.\n if (!(value > -1e-13)) throw new InvalidConfigurationError(\"Diffraction cell integration failed to converge.\");\n values[ky * size + kx] = Math.max(0, value);\n captured += values[ky * size + kx]!;\n }\n if (!(captured > 0 && captured <= 1 + 1e-9)) throw new InvalidConfigurationError(\"Diffraction cell integration failed to converge.\");\n const truncated = Math.max(0, 1 - captured);\n maximumTruncated = Math.max(maximumTruncated, truncated);\n const kernel = values.map((v) => v / captured);\n kernels.push(kernel);\n records.push({ wavelengthNm: nm, cutoffFrequencyCyclesPerMicrometer: cutoff, firstZeroRadiusMicrometers: AIRY_FIRST_ZERO_PER_LAMBDA_N * lambdaN,\n capturedEnergyFraction: captured, truncatedEnergyFraction: truncated, centreSampleFraction: kernel[radius * size + radius]! });\n }\n const uncertainty = { kind: \"not-quantified\" as const, limitation: \"Ideal diffraction-limited model; no measured lens or quadrature error bound.\" };\n const nodes = [-fieldX, fieldX].flatMap((fx) => [-fieldY, fieldY].flatMap((fy) => wavelengths.map((nm, index) => ({\n nodeId: \"airy-\" + (fx < 0 ? \"xm\" : \"xp\") + \"-\" + (fy < 0 ? \"ym\" : \"yp\") + \"-\" + index,\n coordinate: { focalLengthMm, focusDiopters, apertureFNumber: fNumber, fieldXmm: fx, fieldYmm: fy, wavelengthNm: nm, signedDefocusImagePlaneMicrometers: 0 },\n kernel: { widthSamples: size, heightSamples: size, samplePitchMicrometersX: pitch, samplePitchMicrometersY: pitch, centerSampleX: radius, centerSampleY: radius,\n normalizedIntensity: kernels[index]! },\n bestFocusImagePlaneOffsetMicrometers: 0, relativePupilThroughputFactor: 1, evidence: EVIDENCE, uncertainty }))));\n const profile = parseLensSampledPsfProfile({ schemaVersion: \"0.1.0\", profileId: r.profileId, profileVersion: DIFFRACTION_LIMITED_PSF_PROFILE_MODEL.modelVersion,\n scientificStatus: \"approximation\", opticalDomain: \"lens-primary-optical-path-only\", coordinateSystem: \"image-plane-metric\", fieldAxes: \"+X right, +Y up\",\n kernelEnergyNormalization: \"unit-energy-shape\", interpolation: \"bounded-regular-grid-multilinear\", throughputOwnership: \"separate-relative-pupil-throughput-factor\",\n responseIncludes: { diffraction: true, aberration: false, defocus: false, pupilClippingShape: false },\n sensorOpticalStackIncluded: false, sensorSamplingIncluded: false, reconstructionIncluded: false, strayLightIncluded: false,\n axes: { focalLengthMm: [focalLengthMm], focusDiopters: [focusDiopters], apertureFNumber: [fNumber], fieldXmm: [-fieldX, fieldX], fieldYmm: [-fieldY, fieldY],\n wavelengthNm: wavelengths, signedDefocusImagePlaneMicrometers: [0] },\n nodes, evidence: EVIDENCE, limitations: [...LIMITATIONS] });\n return approximationResult({ profile, model: DIFFRACTION_LIMITED_PSF_PROFILE_MODEL, pupilDiameterMm, pupilToImageDistanceMm: distanceMm, workingFNumber,\n kernelRadiusSamples: radius, samplePitchMicrometers: pitch, wavelengths: records, maximumTruncatedEnergyFraction: maximumTruncated,\n quadrature: realSpaceWavelengthsNm.length === 0 ? { method: \"polar-frequency-domain-trapezoid-gauss-legendre\" as const, maximumRadialNodes, maximumAngularNodes } :\n { method: realSpaceWavelengthsNm.length === wavelengths.length ? \"real-space-encircled-energy-gauss-legendre\" as const : \"frequency-domain-and-real-space\" as const,\n maximumRadialNodes, maximumAngularNodes, realSpaceWavelengthsNm } },\n DIFFRACTION_LIMITED_PSF_PROFILE_MODEL.modelId, DIFFRACTION_LIMITED_PSF_PROFILE_MODEL.modelVersion, [...LIMITATIONS]);\n}\n"]}
1
+ {"version":3,"file":"diffraction-psf-profile.js","sourceRoot":"","sources":["../../src/optics/diffraction-psf-profile.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AAEH,OAAO,EAAE,mBAAmB,EAA0B,MAAM,+BAA+B,CAAC;AAC5F,OAAO,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAC3E,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AAGxE,OAAO,EAAE,8BAA8B,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,EAAE,0BAA0B,EAA8B,MAAM,uBAAuB,CAAC;AAE/F,MAAM,CAAC,MAAM,qCAAqC,GAAG,eAAe,CAAC,MAAM,CAAC,MAAM,CAAC;IACjF,OAAO,EAAE,iDAA0D,EAAE,YAAY,EAAE,OAAgB;CAAE,CAAC,CAAC;AACzG;;;;;GAKG;AACH,MAAM,CAAC,MAAM,sCAAsC,GAAG,eAAe,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,kBAAkB,EAAE,GAAG,EAAE,0BAA0B,EAAE,EAAE,EAAE,CAAC,CAAC;AACjJ;;;GAGG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAG,EAAE,CAAC;AAC/C,2HAA2H;AAC3H,MAAM,4BAA4B,GAAG,kBAAkB,GAAG,IAAI,CAAC,EAAE,CAAC;AAsDlE,MAAM,QAAQ,GAAkC,eAAe,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,YAAY,EAAE,UAAmB;QAC9I,eAAe,EAAE,0CAA0C,EAAE,WAAW,EAAE,gBAAyB,EAAE,CAAC,CAAC,CAAC,CAAC;AAC3G,MAAM,WAAW,GAAG,eAAe,CAAC,MAAM,CAAC,MAAM,CAAC;IAChD,kJAAkJ;IAClJ,wJAAwJ;IACxJ,wHAAwH;IACxH,qJAAqJ;IACrJ,sFAAsF;CACvF,CAAC,CAAC;AAEH,SAAS,QAAQ,CAAC,KAAc,EAAE,IAAY;IAC5C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC;QAAE,MAAM,IAAI,yBAAyB,CAAC,IAAI,GAAG,oCAAoC,CAAC,CAAC;IACzJ,OAAO,KAAK,CAAC;AACf,CAAC;AAED,uGAAuG;AACvG,MAAM,cAAc,GAAG,IAAI,GAAG,EAA0D,CAAC;AACzF,SAAS,aAAa,CAAC,CAAS;IAC9B,MAAM,MAAM,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACrC,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAC1B,MAAM,KAAK,GAAG,IAAI,YAAY,CAAC,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI,YAAY,CAAC,CAAC,CAAC,CAAC;IACjE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC1C,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;QACnD,KAAK,IAAI,SAAS,GAAG,CAAC,EAAE,SAAS,GAAG,GAAG,EAAE,SAAS,EAAE,EAAE,CAAC;YACrD,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;YACnB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;gBAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC;gBAAC,EAAE,GAAG,EAAE,CAAC;gBAAC,EAAE,GAAG,EAAE,CAAC;YAAC,CAAC;YACxG,MAAM,IAAI,GAAG,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YACpD,CAAC,IAAI,IAAI,CAAC;YACV,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,KAAK;gBAAE,MAAM;QACpC,CAAC;QACD,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;QACnB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC;YAAC,EAAE,GAAG,EAAE,CAAC;YAAC,EAAE,GAAG,EAAE,CAAC;QAAC,CAAC;QACxG,MAAM,UAAU,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;QACnD,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,UAAU,GAAG,UAAU,CAAC,CAAC;QACtD,uEAAuE;QACvE,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;QAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QACvC,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;QAAC,OAAO,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;IACzD,CAAC;IACD,MAAM,MAAM,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;IAClC,IAAI,cAAc,CAAC,IAAI,GAAG,EAAE;QAAE,cAAc,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IAC5D,OAAO,MAAM,CAAC;AAChB,CAAC;AACD,SAAS,IAAI,CAAC,CAAS;IACrB,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IACtB,MAAM,CAAC,GAAG,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC;IACtB,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;AACzB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,yBAAyB,CAAC,IAAY,EAAE,MAAc,EAAE,KAAK,GAAG,CAAC;IAC/E,sHAAsH;IACtH,yGAAyG;IACzG,MAAM,GAAG,GAAG,IAAI,GAAG,GAAG,GAAG,MAAM,GAAG,GAAG,GAAG,KAAK,EAAE,MAAM,GAAG,eAAe,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACjF,IAAI,MAAM,EAAE,CAAC;QACX,eAAe,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAAC,eAAe,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAC9D,OAAO,EAAE,GAAG,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;IACpD,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,IAAI,4BAA4B,CAAC,CAAC,CAAC,EAAE,GAAG,oCAAoC,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACvJ,EAAE,GAAG,0CAA0C,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;IAC3F,eAAe,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,GAAG,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACrE,IAAI,eAAe,CAAC,IAAI,GAAG,uBAAuB;QAAE,eAAe,CAAC,MAAM,CAAC,eAAe,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAM,CAAC,CAAC;IACjH,OAAO,MAAM,CAAC;AAChB,CAAC;AACD;;;GAGG;AACH,MAAM,uBAAuB,GAAG,GAAG,CAAC;AACpC,MAAM,eAAe,GAAG,IAAI,GAAG,EAAkG,CAAC;AAElI,wFAAwF;AACxF,MAAM,MAAM,GAAG,CAAC,GAA2C,EAAE;IAC3D,MAAM,EAAE,GAAG,IAAI,YAAY,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,IAAI,YAAY,CAAC,EAAE,CAAC,CAAC;IAC3D,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACrB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5B,MAAM,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;QACtC,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC,CAAE,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QACzC,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC,CAAE,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3C,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC;AACpB,CAAC,CAAC,EAAE,CAAC;AACL,sGAAsG;AACtG,MAAM,uBAAuB,GAAG,GAAG,CAAC;AAEpC;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,CAAS;IACvC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;QAAE,MAAM,IAAI,yBAAyB,CAAC,4CAA4C,CAAC,CAAC;IACvH,IAAI,CAAC,GAAG,uBAAuB;QAAE,OAAO,2BAA2B,CAAC,CAAC,CAAC,CAAC;IACvE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,MAAM,EAAE,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC;IAC3C,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC;IAC9C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACzC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YAAC,EAAE,IAAI,IAAI,GAAG,EAAE,CAAC,CAAC,CAAE,GAAG,KAAK,CAAC;YAAC,EAAE,IAAI,IAAI,GAAG,EAAE,CAAC,CAAC,CAAE,GAAG,KAAK,CAAC;QAAC,CAAC;aAAM,CAAC;YAAC,EAAE,IAAI,IAAI,GAAG,EAAE,CAAC,CAAC,CAAE,GAAG,KAAK,CAAC;YAAC,EAAE,IAAI,IAAI,GAAG,EAAE,CAAC,CAAC,CAAE,GAAG,KAAK,CAAC;QAAC,CAAC;QACjJ,KAAK,IAAI,OAAO,CAAC;IACnB,CAAC;IACD,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACxE,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IACjD,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC;AACjD,CAAC;AACD;;;;GAIG;AACH,SAAS,2BAA2B,CAAC,CAAS;IAC5C,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9D,IAAI,IAAI,GAAG,CAAC,EAAE,OAAO,GAAG,MAAM,EAAE,GAAG,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;IAChD,KAAK,IAAI,CAAC,GAAG,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAChC,MAAM,QAAQ,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,OAAO,GAAG,IAAI,CAAC;QAC9C,IAAI,GAAG,OAAO,CAAC;QAAC,OAAO,GAAG,QAAQ,CAAC;QACnC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC;YAAE,EAAE,GAAG,OAAO,CAAC;QAC9B,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC;YAAE,GAAG,IAAI,CAAC,GAAG,OAAO,CAAC;QACvD,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,KAAK,EAAE,CAAC;YAAC,OAAO,IAAI,MAAM,CAAC;YAAC,IAAI,IAAI,MAAM,CAAC;YAAC,GAAG,IAAI,MAAM,CAAC;YAAC,EAAE,IAAI,MAAM,CAAC;QAAC,CAAC;IACpG,CAAC;IACD,GAAG,IAAI,OAAO,CAAC;IACf,MAAM,EAAE,GAAG,OAAO,GAAG,GAAG,EAAE,GAAG,GAAG,EAAE,GAAG,GAAG,CAAC;IACzC,OAAO,EAAE,GAAG,EAAE,GAAG,GAAG,GAAG,GAAG,CAAC;AAC7B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oCAAoC,CAAC,IAAY,EAAE,MAAc,EAAE,KAAK,GAAG,CAAC;IAC1F,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,YAAY,CAAC,IAAI,GAAG,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,aAAa,CAAC,EAAE,CAAC,CAAC;IACvG,IAAI,YAAY,GAAG,CAAC,CAAC;IACrB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;QAC9B,MAAM,CAAC,GAAG,CAAC,GAAG,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,EAAE,GAAG,IAAI,GAAG,CAAC,CAAC;QAC1C,IAAI,GAAG,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC;QAChC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;YAC9B,2FAA2F;YAC3F,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;YAC/H,IAAI,CAAC,GAAG,MAAM,CAAC;YACf,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gBACjC,MAAM,CAAC,GAAG,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;gBAC1F,IAAI,KAAK,GAAG,CAAC,CAAC;gBACd,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE;oBAAE,KAAK,IAAI,OAAO,CAAC,CAAC,CAAE,GAAG,eAAe,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC;gBACrG,GAAG,IAAI,KAAK,GAAG,CAAC,CAAC;gBACjB,CAAC,GAAG,CAAC,CAAC;YACR,CAAC;YACD,YAAY,IAAI,EAAE,GAAG,MAAM,CAAC;YAC5B,KAAK,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC;YAC1B,EAAE,GAAG,EAAE,CAAC;YAAC,MAAM,GAAG,MAAM,CAAC;QAC3B,CAAC;IACH,CAAC;IACD,0GAA0G;IAC1G,MAAM,MAAM,GAAG,CAAC,CAAS,EAAsB,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7F,MAAM,KAAK,GAAG,IAAI,YAAY,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IAC5C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE;QAAE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;YAC7D,IAAI,QAAQ,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,CAAC;YAC3B,KAAK,MAAM,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC;gBAAE,KAAK,MAAM,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;oBACjE,QAAQ,IAAI,EAAE,GAAG,EAAE,CAAC;oBACpB,IAAI,IAAI,EAAE,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC,CAAE,GAAG,KAAK,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC,CAAE,CAAC,CAAC;gBAClE,CAAC;YACD,KAAK,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,QAAQ,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC;QAC5D,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC;AACjC,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,0CAA0C,CAAC,IAAY,EAAE,MAAc,EAAE,KAAK,GAAG,CAAC;IAChG,iHAAiH;IACjH,MAAM,CAAC,GAAG,IAAI,CAAC,EAAE,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,MAAM,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;IAC1F,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC;IACnG,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,aAAa,CAAC,WAAW,CAAC,EAAE,IAAI,GAAG,MAAM,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,YAAY,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IAChH,MAAM,EAAE,GAAG,IAAI,YAAY,CAAC,IAAI,CAAC,EAAE,EAAE,GAAG,IAAI,YAAY,CAAC,IAAI,CAAC,EAAE,WAAW,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC;IACtG,mHAAmH;IACnH,MAAM,OAAO,GAAG,IAAI,YAAY,CAAC,OAAO,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,IAAI,YAAY,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC;IACrF,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,OAAO,EAAE,CAAC,EAAE,EAAE,CAAC;QAAC,MAAM,KAAK,GAAG,CAAC,GAAG,WAAW,CAAC;QAAC,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAAC,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAAC,CAAC;IAC/H,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,WAAW,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,CAAE,EAAE,GAAG,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACrC,4FAA4F;QAC5F,MAAM,GAAG,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAC7F,qGAAqG;QACrG,MAAM,YAAY,GAAG,OAAO,CAAC,CAAC,CAAE,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC,GAAG,WAAW,CAAC;QACvE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,OAAO,EAAE,CAAC,EAAE,EAAE,CAAC;YAClC,MAAM,CAAC,GAAG,IAAI,GAAG,GAAG,GAAG,OAAO,CAAC,CAAC,CAAE,EAAE,CAAC,GAAG,IAAI,GAAG,GAAG,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC;YAC/D,MAAM,CAAC,GAAG,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;YAClF,2DAA2D;YAC3D,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;YACrE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;YAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;YACrB,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC;gBAAC,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;gBAAC,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;YAAC,CAAC;YACzC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;gBAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC,CAAE,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC,CAAE,CAAC;gBAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC,CAAE,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC,CAAE,CAAC;YAAC,CAAC;YACtH,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;gBAC9B,MAAM,EAAE,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAE,CAAC;gBACtB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;oBAAC,MAAM,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC;oBAAC,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,CAAE,CAAC;gBAAC,CAAC;YAChG,CAAC;QACH,CAAC;IACH,CAAC;IACD,MAAM,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IACzB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAE,GAAG,IAAI,CAAC;IACnE,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,CAAC,GAAG,OAAO,EAAE,CAAC;AAC3D,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,kCAAkC,CAAC,KAA8C;IAC/F,MAAM,CAAC,GAAG,wBAAwB,CAAC,KAAK,EAAE,CAAC,UAAU,EAAE,eAAe,EAAE,iBAAiB,EAAE,OAAO,EAAE,eAAe,EAAE,wBAAwB;QAC3I,qBAAqB,EAAE,mBAAmB,EAAE,WAAW,CAAC,EAAE,gDAAgD,CAAC,CAAC;IAC9G,IAAI,CAAC,CAAC,QAAQ,KAAK,UAAU;QAAE,MAAM,IAAI,yBAAyB,CAAC,gCAAgC,CAAC,CAAC;IACrG,MAAM,aAAa,GAAG,QAAQ,CAAC,CAAC,CAAC,aAAa,EAAE,eAAe,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC,CAAC,CAAC,eAAe,EAAE,iBAAiB,CAAC,CAAC;IAC3H,MAAM,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,sBAAsB,EAAE,wBAAwB,CAAC,CAAC;IAC3E,MAAM,CAAC,GAAG,sCAAsC,CAAC;IACjD,MAAM,MAAM,GAAG,CAAC,CAAC,mBAAmB,CAAC;IACrC,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,MAAM,GAAG,CAAC,IAAI,MAAM,GAAG,CAAC,CAAC,0BAA0B,EAAE,CAAC;QACvH,MAAM,IAAI,yBAAyB,CAAC,mDAAmD,GAAG,CAAC,CAAC,0BAA0B,GAAG,GAAG,CAAC,CAAC;IAChI,CAAC;IACD,MAAM,KAAK,GAAG,wBAAwB,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,WAAW,CAAC,EAAE,gBAAgB,CAAC,CAAC;IACzF,IAAI,aAAqB,EAAE,UAAkB,CAAC;IAC9C,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QAC9B,IAAI,KAAK,CAAC,SAAS,KAAK,SAAS;YAAE,MAAM,IAAI,yBAAyB,CAAC,qCAAqC,CAAC,CAAC;QAC9G,aAAa,GAAG,CAAC,CAAC;QAAC,UAAU,GAAG,aAAa,CAAC;IAChD,CAAC;SAAM,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QACnC,MAAM,SAAS,GAAG,QAAQ,CAAC,KAAK,CAAC,SAAS,EAAE,iBAAiB,CAAC,CAAC;QAC/D,aAAa,GAAG,CAAC,GAAG,SAAS,CAAC;QAC9B,UAAU,GAAG,8BAA8B,CAAC,EAAE,aAAa,EAAE,eAAe,EAAE,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,eAAe,CAAC;IACnH,CAAC;;QAAM,MAAM,IAAI,yBAAyB,CAAC,wCAAwC,CAAC,CAAC;IACrF,MAAM,KAAK,GAAG,wBAAwB,CAAC,CAAC,CAAC,iBAAiB,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,4BAA4B,CAAC,CAAC;IACtG,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,qBAAqB,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,qBAAqB,CAAC,CAAC;IAC3G,MAAM,IAAI,GAAG,CAAC,CAAC,aAAa,CAAC;IAC7B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,kBAAkB;QAAE,MAAM,IAAI,yBAAyB,CAAC,+BAA+B,GAAG,CAAC,CAAC,kBAAkB,GAAG,eAAe,CAAC,CAAC;IACjM,MAAM,WAAW,GAAI,IAAkB,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,KAAK,EAAE,gBAAgB,GAAG,KAAK,GAAG,GAAG,CAAC,CAAC,CAAC;IAC/G,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,WAAW,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,IAAI,CAAC,CAAC,WAAW,CAAC,CAAC,CAAE,GAAG,WAAW,CAAC,CAAC,GAAG,CAAC,CAAE,CAAC;YAAE,MAAM,IAAI,yBAAyB,CAAC,4CAA4C,CAAC,CAAC;IAC7K,IAAI,OAAO,CAAC,CAAC,SAAS,KAAK,QAAQ,IAAI,CAAC,qCAAqC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;QAAE,MAAM,IAAI,yBAAyB,CAAC,uCAAuC,CAAC,CAAC;IAE9K,MAAM,eAAe,GAAG,aAAa,GAAG,OAAO,EAAE,cAAc,GAAG,UAAU,GAAG,eAAe,EAAE,IAAI,GAAG,CAAC,GAAG,MAAM,GAAG,CAAC,CAAC;IACtH,IAAI,kBAAkB,GAAG,CAAC,EAAE,mBAAmB,GAAG,CAAC,EAAE,gBAAgB,GAAG,CAAC,CAAC;IAC1E,MAAM,OAAO,GAAsC,EAAE,EAAE,OAAO,GAAe,EAAE,EAAE,sBAAsB,GAAa,EAAE,CAAC;IACvH,KAAK,MAAM,EAAE,IAAI,WAAW,EAAE,CAAC;QAC7B,MAAM,OAAO,GAAG,EAAE,GAAG,IAAI,GAAG,cAAc,EAAE,MAAM,GAAG,CAAC,GAAG,OAAO,CAAC;QACjE,MAAM,IAAI,GAAG,yBAAyB,CAAC,KAAK,GAAG,MAAM,EAAE,MAAM,CAAC,CAAC;QAC/D,IAAI,IAAI,CAAC,SAAS;YAAE,sBAAsB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACpD,kBAAkB,GAAG,IAAI,CAAC,GAAG,CAAC,kBAAkB,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;QAAC,mBAAmB,GAAG,IAAI,CAAC,GAAG,CAAC,mBAAmB,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;QAC5I,MAAM,MAAM,GAAa,IAAI,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;QAChD,IAAI,QAAQ,GAAG,CAAC,CAAC;QACjB,KAAK,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,IAAI,EAAE,EAAE,EAAE;YAAE,KAAK,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC;gBACnE,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,MAAM,CAAC,CAAE,CAAC;gBACxF,uGAAuG;gBACvG,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,KAAK,CAAC;oBAAE,MAAM,IAAI,yBAAyB,CAAC,kDAAkD,CAAC,CAAC;gBAC/G,MAAM,CAAC,EAAE,GAAG,IAAI,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;gBAC5C,QAAQ,IAAI,MAAM,CAAC,EAAE,GAAG,IAAI,GAAG,EAAE,CAAE,CAAC;YACtC,CAAC;QACD,IAAI,CAAC,CAAC,QAAQ,GAAG,CAAC,IAAI,QAAQ,IAAI,CAAC,GAAG,IAAI,CAAC;YAAE,MAAM,IAAI,yBAAyB,CAAC,kDAAkD,CAAC,CAAC;QACrI,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,CAAC;QAC5C,gBAAgB,GAAG,IAAI,CAAC,GAAG,CAAC,gBAAgB,EAAE,SAAS,CAAC,CAAC;QACzD,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC;QAC/C,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACrB,OAAO,CAAC,IAAI,CAAC,EAAE,YAAY,EAAE,EAAE,EAAE,kCAAkC,EAAE,MAAM,EAAE,0BAA0B,EAAE,4BAA4B,GAAG,OAAO;YAC7I,sBAAsB,EAAE,QAAQ,EAAE,uBAAuB,EAAE,SAAS,EAAE,oBAAoB,EAAE,MAAM,CAAC,MAAM,GAAG,IAAI,GAAG,MAAM,CAAE,EAAE,CAAC,CAAC;IACnI,CAAC;IACD,MAAM,WAAW,GAAG,EAAE,IAAI,EAAE,gBAAyB,EAAE,UAAU,EAAE,8EAA8E,EAAE,CAAC;IACpJ,MAAM,KAAK,GAAG,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;QAChH,MAAM,EAAE,OAAO,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,GAAG,GAAG,KAAK;QACrF,UAAU,EAAE,EAAE,aAAa,EAAE,aAAa,EAAE,eAAe,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,kCAAkC,EAAE,CAAC,EAAE;QAC3J,MAAM,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,uBAAuB,EAAE,KAAK,EAAE,uBAAuB,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM;YAC7J,mBAAmB,EAAE,OAAO,CAAC,KAAK,CAAE,EAAE;QACxC,oCAAoC,EAAE,CAAC,EAAE,6BAA6B,EAAE,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW;KAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACnH,MAAM,OAAO,GAAG,0BAA0B,CAAC,EAAE,aAAa,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,EAAE,cAAc,EAAE,qCAAqC,CAAC,YAAY;QAC7J,gBAAgB,EAAE,eAAe,EAAE,aAAa,EAAE,gCAAgC,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,SAAS,EAAE,iBAAiB;QACxJ,yBAAyB,EAAE,mBAAmB,EAAE,aAAa,EAAE,kCAAkC,EAAE,mBAAmB,EAAE,2CAA2C;QACnK,gBAAgB,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAE;QACrG,0BAA0B,EAAE,KAAK,EAAE,sBAAsB,EAAE,KAAK,EAAE,sBAAsB,EAAE,KAAK,EAAE,kBAAkB,EAAE,KAAK;QAC1H,IAAI,EAAE,EAAE,aAAa,EAAE,CAAC,aAAa,CAAC,EAAE,aAAa,EAAE,CAAC,aAAa,CAAC,EAAE,eAAe,EAAE,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC;YAC1J,YAAY,EAAE,WAAW,EAAE,kCAAkC,EAAE,CAAC,CAAC,CAAC,EAAE;QACtE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC;IAC9D,OAAO,mBAAmB,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,qCAAqC,EAAE,eAAe,EAAE,sBAAsB,EAAE,UAAU,EAAE,cAAc;QACrJ,mBAAmB,EAAE,MAAM,EAAE,sBAAsB,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,8BAA8B,EAAE,gBAAgB;QAClI,UAAU,EAAE,sBAAsB,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,iDAA0D,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,CAAC,CAAC;YACjK,EAAE,MAAM,EAAE,sBAAsB,CAAC,MAAM,KAAK,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,4CAAqD,CAAC,CAAC,CAAC,iCAA0C;gBACjK,kBAAkB,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,EAAE,EACzE,qCAAqC,CAAC,OAAO,EAAE,qCAAqC,CAAC,YAAY,EAAE,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC;AACvH,CAAC","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Builds a `LensSampledPsfProfile` for an ideal, aberration-free, in-focus lens with a uniformly lit\n * circular pupil: the Airy pattern of scalar Fraunhofer diffraction, one unit-sum kernel per declared\n * wavelength on ONE shared sample grid (2.1.0).\n *\n * **Model.** The pupil diameter is D = f/N (entrance pupil = exit pupil, unity pupil magnification) and\n * the pupil-to-image distance z is the thin-lens image distance for the focus (f at infinity), so the\n * image-side (working) f-number is N_w = z/D. The incoherent optical transfer function (OTF) of a\n * uniform circular pupil is the normalized autocorrelation of the pupil,\n * H(ρ) = (2/π)·(acos ρ − ρ·√(1 − ρ²)), ρ = |f|/f_c ≤ 1, f_c = 1/(λ·N_w),\n * and the intensity PSF is its inverse Fourier transform (the Airy pattern, first zero at 1.21967·λ·N_w)\n * (MIT OpenCourseWare 2.71 Optics, Spring 2009, lectures 21–22, free: the intensity PSF as the transform of\n * the pupil autocorrelation and the circular-pupil MTF; also see: Goodman, Introduction to Fourier Optics,\n * 4th ed., 2017, §7.3).\n *\n * **Cell integration (exact, no Bessel functions).** Kernel sample (i, j) is the PSF energy inside the\n * square cell of side p centred on (i·p, j·p). The cell integral is the PSF convolved with the cell's\n * indicator, evaluated at the cell centre, so in the frequency domain\n * K(i, j) = p² ∫∫ H(|f|/f_c)·sinc(p·f_x)·sinc(p·f_y)·cos(2π·p·i·f_x)·cos(2π·p·j·f_y) d²f,\n * with sinc(x) = sin(πx)/(πx) (the sine terms vanish because H is even). It is evaluated in polar\n * coordinates: a trapezoid rule in angle (exponentially convergent for this smooth periodic integrand) and\n * Gauss–Legendre in t with ρ = 1 − t², which removes the (1 − ρ)^{3/2} edge behaviour of H. The node counts\n * follow from the integrand's bandwidth with a fixed margin; the tests check convergence by doubling them and\n * check the values against an independent real-space integration of `calculateIdealPolygonDiffractionPsf` in\n * its many-blade (circular) limit. The sum of all cells is exactly 1 (Parseval), so the energy captured by\n * the finite kernel is Σ K over its window and the truncated energy is 1 − Σ K.\n *\n * **Cells much larger than the pattern (2.5; threshold lowered to β = 32 after 2.8.1).** The frequency-domain node counts grow with β = p·f_c (the\n * cell side over the cut-off period), so the work grows as β². From β ≥ `AIRY_REAL_SPACE_MINIMUM_BETA` (32) the cells are\n * integrated in real space instead, with the same model and no frequency integral: the Airy encircled energy is\n * EE(r) = 1 − J₀(x)² − J₁(x)², x = π·r·f_c (it follows from J₀′ = −J₁ and J₁′ = J₀ − J₁/x, DLMF §10.6(i), free), so\n * the energy of the rectangle [0, X] × [0, Y] is (1/2π)∫ EE(r_edge(θ)) dθ over its quadrant, a one-dimensional\n * integral in θ. Every cell is a signed sum of such rectangles. J₀² + J₁² comes from the Hankel asymptotic expansion\n * (DLMF §10.17(i), eq. 10.17.3, free) from x = 300, with eight terms in each of P and Q (remainder under 1e-17,\n * DLMF §10.17(iii)), and below it from Miller's backward recurrence normalized by J₀ + 2ΣJ₂ₖ = 1 (DLMF §3.6(iii),\n * 10.12.4, free). The θ integral uses 16-point Gauss–Legendre panels on which the phase 2x changes by at most\n * 2 radians, so the work grows only as β. The tests check the two methods against each other from β = 32 to 400, the\n * real-space cells against the asymptotic leakage 8√2/(π³·β) of the centre cell, and convergence under doubled panel\n * counts. The threshold was 192 in 2.5; it moved to 32 because from there the real-space cells agree with the frequency-domain cells\n * within about 1e-12 and cost a few to a few hundred times less (a 750 µm whole-frame cell at f/8 took 3.4 s for 20\n * wavelengths). Every β a native pitch up to 10 µm reaches (at most 10/(0.38·1.4) ≈ 19) stays in the frequency domain,\n * so captures at sensor pitch are unchanged; larger cells (binned and whole-frame views) change in the last bits only.\n *\n * **Reuse (2.6).** The cells depend only on (β, R) (and the test-only node scale), so `calculateAiryCellEnergies` keeps the\n * 800 most recently used results (256 before 2.15.0; 800 holds two whole 400-node grids) and returns copies: tiles of one frame rendered in the same worker, and captures that\n * change only the exposure, ISO, scene or output, reuse the kernels instead of integrating them again. The frequency-domain\n * loop also takes cos θ and sin θ from a per-call table computed with the same expressions, so the values are unchanged.\n *\n * **Truncation and normalization.** The kernel is the (2R+1)² window of cells. The Airy tails fall only as\n * r⁻³, so a finite window always misses some energy: it is reported per wavelength, and the window is then\n * renormalized to unit sum because the sampled-profile contract requires unit-energy kernels. The missing\n * tail energy is thereby put back inside the window (an approximation that slightly raises low-frequency\n * contrast compared with the untruncated pattern).\n *\n * **Approximations (labelled):** scalar (no polarization), aberration-free, uniform pupil amplitude, exactly in\n * focus (no defocus), thin lens with unity pupil magnification, the on-axis PSF used across the declared field\n * (isoplanatic), PSF energy binned into the sample cells, finite window renormalized. Not a measured lens.\n * @see docs/PSF_FOUNDATION.md, \"Diffraction-limited sampled profile (2.1)\".\n */\n\nimport { approximationResult, type CalculationResult } from \"../core/calculation-result.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { requireAllowlistedRecord } from \"../core/record-validation.js\";\nimport type { EvidenceProvenance } from \"../core/evidence-provenance.js\";\nimport type { FocusPlane } from \"./focus-state.js\";\nimport { calculateThinLensImageDistance } from \"./thin-lens.js\";\nimport { parseLensSampledPsfProfile, type LensSampledPsfProfile } from \"./lens-psf-profile.js\";\n\nexport const DIFFRACTION_LIMITED_PSF_PROFILE_MODEL = /* @__PURE__ */ Object.freeze({\n modelId: \"photivra-diffraction-limited-circular-pupil-psf\" as const, modelVersion: \"1.0.0\" as const });\n/**\n * Input limits: at most 400 wavelengths, kernel radius 0–32 samples (the irradiance-convolution limit). 400 is the finest spectral\n * sampling of `simulateCapture` and the display view (`sensor.spectralStepNanometers` 1 nm over 380–780 nm), so `lens.diffraction`\n * builds one exact kernel per spectral node at every allowed step; it was 64 before 2.15.0, which refused every step under 6.25 nm.\n * The work and memory are linear in the count (measured in docs/PSF_FOUNDATION.md, \"Spectral nodes\").\n */\nexport const DIFFRACTION_LIMITED_PSF_PROFILE_LIMITS = /* @__PURE__ */ Object.freeze({ maximumWavelengths: 400, maximumKernelRadiusSamples: 32 });\n/**\n * Internal: β = p·f_c (cell side over the cut-off period) from which `calculateAiryCellEnergies` integrates the cells in\n * real space (2.5). Above every β of a `simulateCapture` pitch up to 100 µm, whose captures keep the frequency-domain values.\n */\nexport const AIRY_REAL_SPACE_MINIMUM_BETA = 32;\n/** Radius of the first dark ring of the Airy pattern in units of λ·N_w: j₁,₁/π, j₁,₁ = 3.8317059702 (first zero of J₁). */\nconst AIRY_FIRST_ZERO_PER_LAMBDA_N = 3.8317059702075125 / Math.PI;\n\nexport interface CreateDiffractionLimitedPsfProfileInput {\n /** Only `circular` in 2.1.0 (polygon pupils: `calculateIdealPolygonDiffractionPsf` gives point densities). */\n aperture: \"circular\";\n focalLengthMm: number;\n apertureFNumber: number;\n focus: FocusPlane;\n /** Strictly increasing wavelengths, nm, in the medium of propagation (no air/vacuum conversion is applied). 1–400. */\n wavelengthsNm: readonly number[];\n /** Square sample pitch of every kernel, µm. */\n samplePitchMicrometers: number;\n /** Kernel half-width in samples; each kernel is (2R + 1)². 0–32. */\n kernelRadiusSamples: number;\n /** Half-extent of the image field over which the on-axis PSF is declared (profile field axes ±x, ±y), mm. */\n fieldHalfExtentMm: { x: number; y: number };\n profileId: string;\n}\n\nexport interface DiffractionLimitedPsfWavelength {\n wavelengthNm: number;\n /** f_c = 1/(λ·N_w), cycles per µm. */\n cutoffFrequencyCyclesPerMicrometer: number;\n /** 1.21967·λ·N_w, µm. */\n firstZeroRadiusMicrometers: number;\n /** Σ of the cell energies inside the window before renormalization (of a total of 1). */\n capturedEnergyFraction: number;\n /** 1 − capturedEnergyFraction: the Airy tail energy outside the window, put back by renormalization. */\n truncatedEnergyFraction: number;\n /** Fraction of the renormalized kernel in the centre sample. */\n centreSampleFraction: number;\n}\n\nexport interface DiffractionLimitedPsfProfile {\n profile: LensSampledPsfProfile;\n model: typeof DIFFRACTION_LIMITED_PSF_PROFILE_MODEL;\n pupilDiameterMm: number;\n pupilToImageDistanceMm: number;\n workingFNumber: number;\n kernelRadiusSamples: number;\n samplePitchMicrometers: number;\n wavelengths: readonly DiffractionLimitedPsfWavelength[];\n /** Largest truncated energy over the wavelengths. */\n maximumTruncatedEnergyFraction: number;\n /**\n * How the cells were integrated. 2.5: wavelengths with β = p·f_c ≥ `AIRY_REAL_SPACE_MINIMUM_BETA` (192 in 2.5–2.8.1, then 32) use the real-space\n * encircled-energy method; `realSpaceWavelengthsNm` lists them, and `method` is `\"frequency-domain-and-real-space\"` when both ran.\n * `maximumRadialNodes` counts frequency-domain radial nodes (0 when only the real-space method ran); `maximumAngularNodes` the\n * angular nodes of either method (for the real-space method, all θ nodes of one wavelength).\n */\n quadrature: { method: \"polar-frequency-domain-trapezoid-gauss-legendre\" | \"real-space-encircled-energy-gauss-legendre\" | \"frequency-domain-and-real-space\";\n maximumRadialNodes: number; maximumAngularNodes: number; realSpaceWavelengthsNm?: readonly number[] };\n}\n\nconst EVIDENCE: readonly EvidenceProvenance[] = /* @__PURE__ */ Object.freeze([/* @__PURE__ */ Object.freeze({ sourceOrigin: \"photivra\" as const,\n sourceReference: \"photivra:diffraction-limited-psf-profile\", reuseStatus: \"photivra-owned\" as const })]);\nconst LIMITATIONS = /* @__PURE__ */ Object.freeze([\n \"Scalar Fraunhofer diffraction of an ideal, uniformly lit circular pupil (Airy pattern); no aberration, apodization, polarization or stray light.\",\n \"Exactly in focus: no defocus or depth of field. Thin lens with unity pupil magnification; the pupil-to-image distance is the thin-lens image distance.\",\n \"The on-axis PSF is declared unchanged across the field (isoplanatic); no field-dependent aberration or pupil clipping.\",\n \"PSF energy is binned into square sample cells; the finite window misses the Airy tail energy listed per wavelength and is renormalized to unit sum.\",\n \"Wavelengths are used as given, in the declared basis, without air/vacuum conversion.\"\n]);\n\nfunction positive(value: unknown, name: string): number {\n if (typeof value !== \"number\" || !Number.isFinite(value) || value <= 0) throw new InvalidConfigurationError(name + \" must be a positive finite number.\");\n return value;\n}\n\n/** Gauss–Legendre nodes and weights on [0, 1] (Newton iteration on Pₙ; DLMF §3.5(v)). Cached per n. */\nconst GAUSS_LEGENDRE = new Map<number, { nodes: Float64Array; weights: Float64Array }>();\nfunction gaussLegendre(n: number): { nodes: Float64Array; weights: Float64Array } {\n const cached = GAUSS_LEGENDRE.get(n);\n if (cached) return cached;\n const nodes = new Float64Array(n), weights = new Float64Array(n);\n for (let k = 0; k < Math.ceil(n / 2); k++) {\n let x = Math.cos(Math.PI * (k + 0.75) / (n + 0.5));\n for (let iteration = 0; iteration < 100; iteration++) {\n let p0 = 1, p1 = x;\n for (let m = 2; m <= n; m++) { const p2 = ((2 * m - 1) * x * p1 - (m - 1) * p0) / m; p0 = p1; p1 = p2; }\n const step = p1 / (n * (x * p1 - p0) / (x * x - 1));\n x -= step;\n if (Math.abs(step) < 1e-16) break;\n }\n let p0 = 1, p1 = x;\n for (let m = 2; m <= n; m++) { const p2 = ((2 * m - 1) * x * p1 - (m - 1) * p0) / m; p0 = p1; p1 = p2; }\n const derivative = n * (x * p1 - p0) / (x * x - 1);\n const w = 1 / ((1 - x * x) * derivative * derivative);\n // Map [-1, 1] to [0, 1]: t = (1 ± x)/2, weight w (2/(1-x²)P'² halved).\n nodes[k] = (1 - x) / 2; weights[k] = w;\n nodes[n - 1 - k] = (1 + x) / 2; weights[n - 1 - k] = w;\n }\n const result = { nodes, weights };\n if (GAUSS_LEGENDRE.size < 64) GAUSS_LEGENDRE.set(n, result);\n return result;\n}\nfunction sinc(x: number): number {\n if (x === 0) return 1;\n const a = Math.PI * x;\n return Math.sin(a) / a;\n}\n\n/**\n * Cell energies K(i, j), 0 ≤ i, j ≤ R, of the Airy pattern for β = p·f_c (cell side in units of the cut-off\n * period): in the frequency domain below `AIRY_REAL_SPACE_MINIMUM_BETA`, in real space from it (2.5). `scale`\n * multiplies the node or panel counts (1 in production; the tests use 2 to check convergence).\n */\nexport function calculateAiryCellEnergies(beta: number, radius: number, scale = 1): { cells: Float64Array; radialNodes: number; angularNodes: number; realSpace: boolean } {\n // 2.6: a pure function of (β, R, scale), so results are memoized (most recently used first) and handed out as copies.\n // Tiles of one frame, and captures that change only the exposure or the scene, rebuild the same kernels.\n const key = beta + \"|\" + radius + \"|\" + scale, cached = AIRY_CELL_CACHE.get(key);\n if (cached) {\n AIRY_CELL_CACHE.delete(key); AIRY_CELL_CACHE.set(key, cached);\n return { ...cached, cells: cached.cells.slice() };\n }\n const result = beta >= AIRY_REAL_SPACE_MINIMUM_BETA ? { ...calculateAiryCellEnergiesInRealSpace(beta, radius, scale), radialNodes: 0, realSpace: true } :\n { ...calculateAiryCellEnergiesInFrequencyDomain(beta, radius, scale), realSpace: false };\n AIRY_CELL_CACHE.set(key, { ...result, cells: result.cells.slice() });\n if (AIRY_CELL_CACHE.size > AIRY_CELL_CACHE_ENTRIES) AIRY_CELL_CACHE.delete(AIRY_CELL_CACHE.keys().next().value!);\n return result;\n}\n/**\n * Memoized cell energies (2.6): 800 entries of (R + 1)² values, at most 800 × 33² × 8 bytes ≈ 7.0 MB. 2.15.0: 800 (was 256), so the\n * least-recently-used order still reuses a whole grid of the finest spectral sampling (400 nodes), which 256 entries never did.\n */\nconst AIRY_CELL_CACHE_ENTRIES = 800;\nconst AIRY_CELL_CACHE = new Map<string, { cells: Float64Array; radialNodes: number; angularNodes: number; realSpace: boolean }>();\n\n/** Hankel asymptotic coefficients a_k(ν) of DLMF 10.17.1 for ν = 0 and 1, k = 0..15. */\nconst HANKEL = ((): { a0: Float64Array; a1: Float64Array } => {\n const a0 = new Float64Array(16), a1 = new Float64Array(16);\n a0[0] = 1; a1[0] = 1;\n for (let k = 1; k < 16; k++) {\n const odd = (2 * k - 1) * (2 * k - 1);\n a0[k] = a0[k - 1]! * (0 - odd) / (8 * k);\n a1[k] = a1[k - 1]! * (4 - odd) / (8 * k);\n }\n return { a0, a1 };\n})();\n/** Smallest argument of `besselSquareSum`: there the 16-term expansion's remainder is below 1e-17. */\nconst HANKEL_MINIMUM_ARGUMENT = 300;\n\n/**\n * J₀(x)² + J₁(x)² for x > 0. From x ≥ 300 by the Hankel expansion J_ν(x) = √(2/(πx))·(P_ν cos ω_ν − Q_ν sin ω_ν), ω_ν = x − νπ/2 − π/4,\n * P_ν = Σ(−1)^k a_2k(ν)/x^2k, Q_ν = Σ(−1)^k a_2k+1(ν)/x^2k+1 (DLMF 10.17.3), eight terms each. With ω₁ = ω₀ − π/2,\n * J₁ = √(2/(πx))·(P₁ sin ω₀ + Q₁ cos ω₀). Below 300 by Miller's backward recurrence J_{n−1} = (2n/x)·J_n − J_{n+1} (DLMF 10.6.1;\n * the algorithm, DLMF §3.6(iii)), started at an order above x where J_n is negligible, normalized by J₀ + 2·Σ J_2k = 1\n * (DLMF 10.12.4). Internal.\n */\nexport function besselSquareSum(x: number): number {\n if (!(x > 0) || !Number.isFinite(x)) throw new InvalidConfigurationError(\"J0 and J1 need a positive finite argument.\");\n if (x < HANKEL_MINIMUM_ARGUMENT) return besselSquareSumByRecurrence(x);\n const { a0, a1 } = HANKEL, inverse = 1 / x;\n let p0 = 0, q0 = 0, p1 = 0, q1 = 0, power = 1;\n for (let k = 0; k < 16; k++) {\n const sign = (k >> 1) % 2 === 0 ? 1 : -1;\n if (k % 2 === 0) { p0 += sign * a0[k]! * power; p1 += sign * a1[k]! * power; } else { q0 += sign * a0[k]! * power; q1 += sign * a1[k]! * power; }\n power *= inverse;\n }\n const omega = x - Math.PI / 4, c = Math.cos(omega), s = Math.sin(omega);\n const j0 = p0 * c - q0 * s, j1 = p1 * s + q1 * c;\n return 2 / (Math.PI * x) * (j0 * j0 + j1 * j1);\n}\n/**\n * Miller's backward recurrence for 0 < x < 300 (see `besselSquareSum`). The start order x + 10·∛x + 40 (even) lies far enough in the\n * region where J_n(x) falls faster than exponentially that the start values' error is below double precision at J₀ and J₁; the\n * values are rescaled when they grow large, and normalized before squaring.\n */\nfunction besselSquareSumByRecurrence(x: number): number {\n const start = 2 * Math.ceil((x + 10 * Math.cbrt(x) + 40) / 2);\n let next = 0, current = 1e-100, sum = 0, j1 = 0;\n for (let n = start; n >= 1; n--) {\n const previous = (2 * n / x) * current - next;\n next = current; current = previous;\n if (n - 1 === 1) j1 = current;\n if (n - 1 > 0 && (n - 1) % 2 === 0) sum += 2 * current;\n if (Math.abs(current) > 1e100) { current *= 1e-100; next *= 1e-100; sum *= 1e-100; j1 *= 1e-100; }\n }\n sum += current;\n const j0 = current / sum, j1n = j1 / sum;\n return j0 * j0 + j1n * j1n;\n}\n\n/**\n * Real-space cell energies (2.5): see the module comment. G(Z, φ) = ∫₀^φ [J₀² + J₁²](π·β·Z/cos θ) dθ for cell boundaries at\n * half-integer distances Z (in cells), accumulated along θ for each Z with every needed φ = atan(W/Z) as a panel boundary.\n * The rectangle [0, X] × [0, Y] holds 1/4 − (G(X, atan(Y/X)) + G(Y, atan(X/Y)))/(2π) of the energy. Internal.\n */\nexport function calculateAiryCellEnergiesInRealSpace(beta: number, radius: number, scale = 1): { cells: Float64Array; angularNodes: number } {\n const size = radius + 1, table = new Float64Array(size * size), { nodes, weights } = gaussLegendre(16);\n let angularNodes = 0;\n for (let z = 0; z < size; z++) {\n const Z = z + 0.5, k = Math.PI * beta * Z;\n let sum = 0, t0 = 1, theta0 = 0;\n for (let w = 0; w < size; w++) {\n // Panels equal in t = 1/cos θ, so the phase 2x = 2kt changes by at most 2 radians on each.\n const t1 = Math.hypot(Z, w + 0.5) / Z, theta1 = Math.atan2(w + 0.5, Z), panels = Math.max(1, Math.ceil(scale * k * (t1 - t0)));\n let a = theta0;\n for (let n = 1; n <= panels; n++) {\n const b = n === panels ? theta1 : Math.acos(1 / (t0 + (t1 - t0) * n / panels)), h = b - a;\n let panel = 0;\n for (let g = 0; g < 16; g++) panel += weights[g]! * besselSquareSum(k / Math.cos(a + h * nodes[g]!));\n sum += panel * h;\n a = b;\n }\n angularNodes += 16 * panels;\n table[z * size + w] = sum;\n t0 = t1; theta0 = theta1;\n }\n }\n // Boundaries of cell i along one axis as (half-integer index, weight): the centre cell is twice [0, 1/2].\n const bounds = (i: number): [number, number][] => i === 0 ? [[0, 2]] : [[i, 1], [i - 1, -1]];\n const cells = new Float64Array(size * size);\n for (let j = 0; j < size; j++) for (let i = 0; i < size; i++) {\n let constant = 0, leak = 0;\n for (const [a, wa] of bounds(i)) for (const [b, wb] of bounds(j)) {\n constant += wa * wb;\n leak += wa * wb * (table[a * size + b]! + table[b * size + a]!);\n }\n cells[j * size + i] = constant / 4 - leak / (2 * Math.PI);\n }\n return { cells, angularNodes };\n}\n\n/** Frequency-domain cell energies (2.1; see the module comment). Internal. */\nexport function calculateAiryCellEnergiesInFrequencyDomain(beta: number, radius: number, scale = 1): { cells: Float64Array; radialNodes: number; angularNodes: number } {\n // Bandwidth of the angular and radial integrands: c = π·β·(2R + 1)·√2 (the sinc factors and the highest cosine).\n const c = Math.PI * beta * (2 * radius + 1) * Math.SQRT2, margin = 10 * Math.cbrt(c) + 24;\n const quarter = Math.ceil(scale * (c + margin) / 4), radialNodes = Math.ceil(scale * (c + margin));\n const { nodes, weights } = gaussLegendre(radialNodes), size = radius + 1, cells = new Float64Array(size * size);\n const ca = new Float64Array(size), cb = new Float64Array(size), angularStep = (Math.PI / 2) / quarter;\n // cos θ and sin θ of every angular node, computed once with the same expressions as inside the loop (same values).\n const cosines = new Float64Array(quarter + 1), sines = new Float64Array(quarter + 1);\n for (let k = 0; k <= quarter; k++) { const theta = k * angularStep; cosines[k] = Math.cos(theta); sines[k] = Math.sin(theta); }\n for (let r = 0; r < radialNodes; r++) {\n const t = nodes[r]!, rho = 1 - t * t;\n // H(1 − t²) without cancellation: acos(1 − t²) = 2·asin(t/√2), ρ·√(1 − ρ²) = ρ·t·√(2 − t²).\n const otf = (2 / Math.PI) * (2 * Math.asin(t / Math.SQRT2) - rho * t * Math.sqrt(2 - t * t));\n // ∫₀¹ g(ρ) dρ = ∫₀¹ g(1 − t²)·2t dt; the 4 is the four symmetric quadrants of the angular trapezoid.\n const radialWeight = weights[r]! * 2 * t * rho * otf * 4 * angularStep;\n for (let k = 0; k <= quarter; k++) {\n const x = beta * rho * cosines[k]!, y = beta * rho * sines[k]!;\n const w = radialWeight * (k === 0 || k === quarter ? 0.5 : 1) * sinc(x) * sinc(y);\n // cos(2π·i·x) and cos(2π·j·y) by the Chebyshev recurrence.\n const cx = Math.cos(2 * Math.PI * x), cy = Math.cos(2 * Math.PI * y);\n ca[0] = 1; cb[0] = 1;\n if (size > 1) { ca[1] = cx; cb[1] = cy; }\n for (let i = 2; i < size; i++) { ca[i] = 2 * cx * ca[i - 1]! - ca[i - 2]!; cb[i] = 2 * cy * cb[i - 1]! - cb[i - 2]!; }\n for (let j = 0; j < size; j++) {\n const wj = w * cb[j]!;\n for (let i = 0; i < size; i++) { const o = j * size + i; cells[o] = cells[o]! + wj * ca[i]!; }\n }\n }\n }\n const area = beta * beta;\n for (let i = 0; i < cells.length; i++) cells[i] = cells[i]! * area;\n return { cells, radialNodes, angularNodes: 4 * quarter };\n}\n\n/**\n * Creates a sampled lens PSF profile for an ideal diffraction-limited lens with a circular pupil: one\n * unit-sum Airy kernel per wavelength on one shared (2R+1)² grid, with the truncated energy reported per\n * wavelength. The profile declares the given focal length, f-number and focus exactly, and the field axes\n * ±`fieldHalfExtentMm` with the same kernel at every field node.\n * @param input - See `CreateDiffractionLimitedPsfProfileInput`.\n * @returns The profile and its diffraction record. An approximation; not a measured lens.\n * @see docs/PSF_FOUNDATION.md, \"Diffraction-limited sampled profile (2.1)\".\n */\nexport function createDiffractionLimitedPsfProfile(input: CreateDiffractionLimitedPsfProfileInput): CalculationResult<DiffractionLimitedPsfProfile> {\n const r = requireAllowlistedRecord(input, [\"aperture\", \"focalLengthMm\", \"apertureFNumber\", \"focus\", \"wavelengthsNm\", \"samplePitchMicrometers\",\n \"kernelRadiusSamples\", \"fieldHalfExtentMm\", \"profileId\"], \"Invalid diffraction-limited PSF profile input.\");\n if (r.aperture !== \"circular\") throw new InvalidConfigurationError(\"aperture must be \\\"circular\\\".\");\n const focalLengthMm = positive(r.focalLengthMm, \"focalLengthMm\"), fNumber = positive(r.apertureFNumber, \"apertureFNumber\");\n const pitch = positive(r.samplePitchMicrometers, \"samplePitchMicrometers\");\n const L = DIFFRACTION_LIMITED_PSF_PROFILE_LIMITS;\n const radius = r.kernelRadiusSamples;\n if (typeof radius !== \"number\" || !Number.isSafeInteger(radius) || radius < 0 || radius > L.maximumKernelRadiusSamples) {\n throw new InvalidConfigurationError(\"kernelRadiusSamples must be an integer from 0 to \" + L.maximumKernelRadiusSamples + \".\");\n }\n const focus = requireAllowlistedRecord(r.focus, [\"kind\", \"distanceM\"], \"Invalid focus.\");\n let focusDiopters: number, distanceMm: number;\n if (focus.kind === \"infinity\") {\n if (focus.distanceM !== undefined) throw new InvalidConfigurationError(\"An infinity focus has no distanceM.\");\n focusDiopters = 0; distanceMm = focalLengthMm;\n } else if (focus.kind === \"finite\") {\n const distanceM = positive(focus.distanceM, \"focus.distanceM\");\n focusDiopters = 1 / distanceM;\n distanceMm = calculateThinLensImageDistance({ focalLengthMm, objectDistanceM: distanceM }).value.imageDistanceMm;\n } else throw new InvalidConfigurationError(\"focus.kind must be finite or infinity.\");\n const field = requireAllowlistedRecord(r.fieldHalfExtentMm, [\"x\", \"y\"], \"Invalid fieldHalfExtentMm.\");\n const fieldX = positive(field.x, \"fieldHalfExtentMm.x\"), fieldY = positive(field.y, \"fieldHalfExtentMm.y\");\n const list = r.wavelengthsNm;\n if (!Array.isArray(list) || list.length < 1 || list.length > L.maximumWavelengths) throw new InvalidConfigurationError(\"wavelengthsNm must list 1 to \" + L.maximumWavelengths + \" wavelengths.\");\n const wavelengths = (list as unknown[]).map((value, index) => positive(value, \"wavelengthsNm[\" + index + \"]\"));\n for (let i = 1; i < wavelengths.length; i++) if (!(wavelengths[i]! > wavelengths[i - 1]!)) throw new InvalidConfigurationError(\"wavelengthsNm must be strictly increasing.\");\n if (typeof r.profileId !== \"string\" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(r.profileId)) throw new InvalidConfigurationError(\"profileId must be a public opaque ID.\");\n\n const pupilDiameterMm = focalLengthMm / fNumber, workingFNumber = distanceMm / pupilDiameterMm, size = 2 * radius + 1;\n let maximumRadialNodes = 0, maximumAngularNodes = 0, maximumTruncated = 0;\n const records: DiffractionLimitedPsfWavelength[] = [], kernels: number[][] = [], realSpaceWavelengthsNm: number[] = [];\n for (const nm of wavelengths) {\n const lambdaN = nm / 1000 * workingFNumber, cutoff = 1 / lambdaN;\n const airy = calculateAiryCellEnergies(pitch * cutoff, radius);\n if (airy.realSpace) realSpaceWavelengthsNm.push(nm);\n maximumRadialNodes = Math.max(maximumRadialNodes, airy.radialNodes); maximumAngularNodes = Math.max(maximumAngularNodes, airy.angularNodes);\n const values: number[] = new Array(size * size);\n let captured = 0;\n for (let ky = 0; ky < size; ky++) for (let kx = 0; kx < size; kx++) {\n const value = airy.cells[Math.abs(ky - radius) * (radius + 1) + Math.abs(kx - radius)]!;\n // Every cell energy of a non-negative PSF is positive; a negative value would be a quadrature failure.\n if (!(value > -1e-13)) throw new InvalidConfigurationError(\"Diffraction cell integration failed to converge.\");\n values[ky * size + kx] = Math.max(0, value);\n captured += values[ky * size + kx]!;\n }\n if (!(captured > 0 && captured <= 1 + 1e-9)) throw new InvalidConfigurationError(\"Diffraction cell integration failed to converge.\");\n const truncated = Math.max(0, 1 - captured);\n maximumTruncated = Math.max(maximumTruncated, truncated);\n const kernel = values.map((v) => v / captured);\n kernels.push(kernel);\n records.push({ wavelengthNm: nm, cutoffFrequencyCyclesPerMicrometer: cutoff, firstZeroRadiusMicrometers: AIRY_FIRST_ZERO_PER_LAMBDA_N * lambdaN,\n capturedEnergyFraction: captured, truncatedEnergyFraction: truncated, centreSampleFraction: kernel[radius * size + radius]! });\n }\n const uncertainty = { kind: \"not-quantified\" as const, limitation: \"Ideal diffraction-limited model; no measured lens or quadrature error bound.\" };\n const nodes = [-fieldX, fieldX].flatMap((fx) => [-fieldY, fieldY].flatMap((fy) => wavelengths.map((nm, index) => ({\n nodeId: \"airy-\" + (fx < 0 ? \"xm\" : \"xp\") + \"-\" + (fy < 0 ? \"ym\" : \"yp\") + \"-\" + index,\n coordinate: { focalLengthMm, focusDiopters, apertureFNumber: fNumber, fieldXmm: fx, fieldYmm: fy, wavelengthNm: nm, signedDefocusImagePlaneMicrometers: 0 },\n kernel: { widthSamples: size, heightSamples: size, samplePitchMicrometersX: pitch, samplePitchMicrometersY: pitch, centerSampleX: radius, centerSampleY: radius,\n normalizedIntensity: kernels[index]! },\n bestFocusImagePlaneOffsetMicrometers: 0, relativePupilThroughputFactor: 1, evidence: EVIDENCE, uncertainty }))));\n const profile = parseLensSampledPsfProfile({ schemaVersion: \"0.1.0\", profileId: r.profileId, profileVersion: DIFFRACTION_LIMITED_PSF_PROFILE_MODEL.modelVersion,\n scientificStatus: \"approximation\", opticalDomain: \"lens-primary-optical-path-only\", coordinateSystem: \"image-plane-metric\", fieldAxes: \"+X right, +Y up\",\n kernelEnergyNormalization: \"unit-energy-shape\", interpolation: \"bounded-regular-grid-multilinear\", throughputOwnership: \"separate-relative-pupil-throughput-factor\",\n responseIncludes: { diffraction: true, aberration: false, defocus: false, pupilClippingShape: false },\n sensorOpticalStackIncluded: false, sensorSamplingIncluded: false, reconstructionIncluded: false, strayLightIncluded: false,\n axes: { focalLengthMm: [focalLengthMm], focusDiopters: [focusDiopters], apertureFNumber: [fNumber], fieldXmm: [-fieldX, fieldX], fieldYmm: [-fieldY, fieldY],\n wavelengthNm: wavelengths, signedDefocusImagePlaneMicrometers: [0] },\n nodes, evidence: EVIDENCE, limitations: [...LIMITATIONS] });\n return approximationResult({ profile, model: DIFFRACTION_LIMITED_PSF_PROFILE_MODEL, pupilDiameterMm, pupilToImageDistanceMm: distanceMm, workingFNumber,\n kernelRadiusSamples: radius, samplePitchMicrometers: pitch, wavelengths: records, maximumTruncatedEnergyFraction: maximumTruncated,\n quadrature: realSpaceWavelengthsNm.length === 0 ? { method: \"polar-frequency-domain-trapezoid-gauss-legendre\" as const, maximumRadialNodes, maximumAngularNodes } :\n { method: realSpaceWavelengthsNm.length === wavelengths.length ? \"real-space-encircled-energy-gauss-legendre\" as const : \"frequency-domain-and-real-space\" as const,\n maximumRadialNodes, maximumAngularNodes, realSpaceWavelengthsNm } },\n DIFFRACTION_LIMITED_PSF_PROFILE_MODEL.modelId, DIFFRACTION_LIMITED_PSF_PROFILE_MODEL.modelVersion, [...LIMITATIONS]);\n}\n"]}
@@ -30,8 +30,11 @@
30
30
  import { type CalculationResult } from "../core/calculation-result.js";
31
31
  import { type ResolveLensSampledPsfInput } from "./lens-psf-profile.js";
32
32
  export declare const LENS_PSF_CONVOLUTION_KERNELS_SCHEMA_VERSION: "0.1.0";
33
- /** At most this many wavelengths per call. */
34
- export declare const LENS_PSF_CONVOLUTION_KERNELS_MAXIMUM_WAVELENGTHS = 64;
33
+ /**
34
+ * At most this many wavelengths per call: 400, the finest spectral sampling of `simulateCapture` (1 nm over 380–780 nm),
35
+ * as `DIFFRACTION_LIMITED_PSF_PROFILE_LIMITS.maximumWavelengths` (64 before 2.15.0). The work is linear in the count.
36
+ */
37
+ export declare const LENS_PSF_CONVOLUTION_KERNELS_MAXIMUM_WAVELENGTHS = 400;
35
38
  export interface LensPsfConvolutionKernelsInput {
36
39
  schemaVersion: typeof LENS_PSF_CONVOLUTION_KERNELS_SCHEMA_VERSION;
37
40
  /** The optical state, as for `resolveLensSampledPsf`, without the field point and wavelength. */
@@ -89,3 +92,11 @@ export interface LensPsfConvolutionKernels {
89
92
  * @see docs/WEBGPU.md, "Kernels from a lens PSF profile".
90
93
  */
91
94
  export declare function resolveLensPsfConvolutionKernels(input: LensPsfConvolutionKernelsInput): CalculationResult<LensPsfConvolutionKernels>;
95
+ /**
96
+ * Internal (not exported from the package): `resolveLensPsfConvolutionKernels` with `ownedProfile` used in place of a fresh
97
+ * private clone of `input.psf.profile`. `ownedProfile` must be a structured clone of that profile that no caller code can
98
+ * reach, already passed to `markEngineOwnedLensSampledPsfProfile`; repeated calls then share one parse (the clone is frozen
99
+ * and its parse cached on the first resolve), so N one-wavelength calls cost O(N) instead of O(N²) (2.15.0).
100
+ * The values are the same: the public function parses its own clone the same way.
101
+ */
102
+ export declare function resolveLensPsfConvolutionKernelsWithOwnedProfile(input: LensPsfConvolutionKernelsInput, ownedProfile: object): CalculationResult<LensPsfConvolutionKernels>;