@photivra/engine 2.19.0 → 2.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +289 -1
- package/CITATION.cff +1 -1
- package/NODE_SUPPORT.md +7 -5
- package/README.md +7 -6
- package/SECURITY.md +1 -1
- package/THIRD_PARTY.md +21 -10
- package/dist/capture/dng-lens-correction-opcodes.d.ts +32 -0
- package/dist/capture/dng-lens-correction-opcodes.js +119 -0
- package/dist/capture/dng-lens-correction-opcodes.js.map +1 -0
- package/dist/capture/exposure-build-up-stage.d.ts +16 -0
- package/dist/capture/exposure-build-up-stage.js +4 -0
- package/dist/capture/exposure-build-up-stage.js.map +1 -0
- package/dist/capture/native-environment-lean.d.ts +2 -1
- package/dist/capture/native-environment-lean.js +42 -78
- package/dist/capture/native-environment-lean.js.map +1 -1
- package/dist/capture/native-environment-tabulated.js +4 -3
- package/dist/capture/native-environment-tabulated.js.map +1 -1
- package/dist/capture/native-raw-dng.d.ts +6 -0
- package/dist/capture/native-raw-dng.js +16 -2
- package/dist/capture/native-raw-dng.js.map +1 -1
- package/dist/capture/native-raw.d.ts +3 -2
- package/dist/capture/native-raw.js +5 -4
- package/dist/capture/native-raw.js.map +1 -1
- package/dist/capture/sensor-raw-producer.d.ts +4 -1
- package/dist/capture/sensor-raw-producer.js +15 -8
- package/dist/capture/sensor-raw-producer.js.map +1 -1
- package/dist/core/shared-helpers.d.ts +2 -0
- package/dist/core/shared-helpers.js +4 -0
- package/dist/core/shared-helpers.js.map +1 -1
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/core/version.js.map +1 -1
- package/dist/equipment/generic-lens-catalogue.d.ts +11 -4
- package/dist/equipment/generic-lens-catalogue.js +49 -7
- package/dist/equipment/generic-lens-catalogue.js.map +1 -1
- package/dist/equipment/generic-tier-assets.d.ts +32 -0
- package/dist/equipment/generic-tier-assets.js +18 -14
- package/dist/equipment/generic-tier-assets.js.map +1 -1
- package/dist/equipment/generic-tier-presets.js +2 -3
- package/dist/equipment/generic-tier-presets.js.map +1 -1
- package/dist/equipment/sensor-profile-body.d.ts +78 -0
- package/dist/equipment/sensor-profile-body.js +77 -0
- package/dist/equipment/sensor-profile-body.js.map +1 -0
- package/dist/equipment/sensor-profiles.d.ts +8 -0
- package/dist/equipment/sensor-profiles.js +82 -0
- package/dist/equipment/sensor-profiles.js.map +1 -0
- package/dist/facade/catalogue-lens-signal.d.ts +2 -2
- package/dist/facade/catalogue-lens-signal.js +58 -71
- package/dist/facade/catalogue-lens-signal.js.map +1 -1
- package/dist/facade/chart-radiance-batch.js +2 -1
- package/dist/facade/chart-radiance-batch.js.map +1 -1
- package/dist/facade/defocus-image-space-provider.d.ts +7 -0
- package/dist/facade/defocus-image-space-provider.js +64 -76
- package/dist/facade/defocus-image-space-provider.js.map +1 -1
- package/dist/facade/display-view-gpu-session.js +2 -0
- package/dist/facade/display-view-gpu-session.js.map +1 -1
- package/dist/facade/display-view-gpu.d.ts +13 -5
- package/dist/facade/display-view-gpu.js +4 -4
- package/dist/facade/display-view-gpu.js.map +1 -1
- package/dist/facade/display-view-lens-signal.d.ts +1 -1
- package/dist/facade/display-view-lens-signal.js +30 -26
- package/dist/facade/display-view-lens-signal.js.map +1 -1
- package/dist/facade/display-view.d.ts +3 -1
- package/dist/facade/display-view.js +6 -24
- package/dist/facade/display-view.js.map +1 -1
- package/dist/facade/expected-signal-setup.d.ts +3 -1
- package/dist/facade/expected-signal-setup.js +1 -1
- package/dist/facade/expected-signal-setup.js.map +1 -1
- package/dist/facade/expected-signal.d.ts +47 -14
- package/dist/facade/expected-signal.js +117 -112
- package/dist/facade/expected-signal.js.map +1 -1
- package/dist/facade/exposure-build-up.d.ts +70 -0
- package/dist/facade/exposure-build-up.js +164 -0
- package/dist/facade/exposure-build-up.js.map +1 -0
- package/dist/facade/fast-methods.d.ts +50 -0
- package/dist/facade/fast-methods.js +58 -0
- package/dist/facade/fast-methods.js.map +1 -0
- package/dist/facade/fast-resolution.d.ts +12 -0
- package/dist/facade/fast-resolution.js +18 -0
- package/dist/facade/fast-resolution.js.map +1 -0
- package/dist/facade/generic-lens-capture.d.ts +5 -4
- package/dist/facade/generic-lens-capture.js +5 -4
- package/dist/facade/generic-lens-capture.js.map +1 -1
- package/dist/facade/image-plane-enable.d.ts +7 -0
- package/dist/facade/image-plane-enable.js +17 -0
- package/dist/facade/image-plane-enable.js.map +1 -0
- package/dist/facade/image-plane-lattice-data.d.ts +6 -0
- package/dist/facade/image-plane-lattice-data.js +10 -0
- package/dist/facade/image-plane-lattice-data.js.map +1 -0
- package/dist/facade/image-plane-support.d.ts +33 -0
- package/dist/facade/image-plane-support.js +11 -0
- package/dist/facade/image-plane-support.js.map +1 -0
- package/dist/facade/image-plane.d.ts +36 -0
- package/dist/facade/image-plane.js +353 -0
- package/dist/facade/image-plane.js.map +1 -0
- package/dist/facade/mixed-illumination.d.ts +108 -0
- package/dist/facade/mixed-illumination.js +174 -0
- package/dist/facade/mixed-illumination.js.map +1 -0
- package/dist/facade/representative-light-sources.d.ts +72 -0
- package/dist/facade/representative-light-sources.js +124 -0
- package/dist/facade/representative-light-sources.js.map +1 -0
- package/dist/facade/scene-radiance.d.ts +129 -4
- package/dist/facade/scene-radiance.js +48 -13
- package/dist/facade/scene-radiance.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +88 -19
- package/dist/facade/simulate-capture.js +374 -145
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.d.ts +3 -2
- package/dist/facade/simulate-display-view.js +36 -18
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/dist/gpu/chart-composite-gpu.d.ts +13 -1
- package/dist/gpu/chart-composite-gpu.js +48 -9
- package/dist/gpu/chart-composite-gpu.js.map +1 -1
- package/dist/gpu/chart-radiance-gpu.js +4 -30
- package/dist/gpu/chart-radiance-gpu.js.map +1 -1
- package/dist/gpu/display-view-composite-material-module.d.ts +10 -4
- package/dist/gpu/display-view-composite-material-module.js +39 -13
- package/dist/gpu/display-view-composite-material-module.js.map +1 -1
- package/dist/gpu/display-view-image-material-module.d.ts +86 -0
- package/dist/gpu/display-view-image-material-module.js +339 -0
- package/dist/gpu/display-view-image-material-module.js.map +1 -0
- package/dist/gpu/display-view-image-quality.d.ts +45 -0
- package/dist/gpu/display-view-image-quality.js +380 -0
- package/dist/gpu/display-view-image-quality.js.map +1 -0
- package/dist/gpu/display-view-quality-gpu.d.ts +67 -1
- package/dist/gpu/display-view-quality-gpu.js +136 -205
- package/dist/gpu/display-view-quality-gpu.js.map +1 -1
- package/dist/gpu/display-view-speed-gpu.d.ts +51 -3
- package/dist/gpu/display-view-speed-gpu.js +131 -93
- package/dist/gpu/display-view-speed-gpu.js.map +1 -1
- package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +29 -15
- package/dist/gpu/wgsl-display-view-quality-kernel.js +58 -36
- package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
- package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +21 -6
- package/dist/gpu/wgsl-display-view-speed-kernel.js +46 -11
- package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
- package/dist/gpu/wgsl-exact-sum.d.ts +20 -0
- package/dist/gpu/wgsl-exact-sum.js +42 -0
- package/dist/gpu/wgsl-exact-sum.js.map +1 -0
- package/dist/index.d.ts +18 -3
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/dist/motion/subject-motion.d.ts +66 -0
- package/dist/motion/subject-motion.js +52 -0
- package/dist/motion/subject-motion.js.map +1 -0
- package/dist/optics/aperture-diameter.d.ts +47 -0
- package/dist/optics/aperture-diameter.js +50 -0
- package/dist/optics/aperture-diameter.js.map +1 -0
- package/dist/optics/detail-limit.d.ts +182 -0
- package/dist/optics/detail-limit.js +336 -0
- package/dist/optics/detail-limit.js.map +1 -0
- package/dist/optics/generic-lens-lateral-color.d.ts +83 -0
- package/dist/optics/generic-lens-lateral-color.js +77 -0
- package/dist/optics/generic-lens-lateral-color.js.map +1 -0
- package/dist/optics/generic-lens-psf.d.ts +18 -2
- package/dist/optics/generic-lens-psf.js +79 -48
- package/dist/optics/generic-lens-psf.js.map +1 -1
- package/dist/optics/lens-psf-profile.d.ts +7 -0
- package/dist/optics/lens-psf-profile.js +2 -1
- package/dist/optics/lens-psf-profile.js.map +1 -1
- package/dist/optics/scene-to-sensor-irradiance.d.ts +13 -0
- package/dist/optics/scene-to-sensor-irradiance.js +7 -0
- package/dist/optics/scene-to-sensor-irradiance.js.map +1 -1
- package/dist/output/crop-to-match-focal-length.d.ts +62 -0
- package/dist/output/crop-to-match-focal-length.js +63 -0
- package/dist/output/crop-to-match-focal-length.js.map +1 -0
- package/dist/output/developed-lens-corrections.d.ts +19 -5
- package/dist/output/developed-lens-corrections.js +30 -11
- package/dist/output/developed-lens-corrections.js.map +1 -1
- package/dist/output/generic-lens-corrections.d.ts +41 -3
- package/dist/output/generic-lens-corrections.js +57 -9
- package/dist/output/generic-lens-corrections.js.map +1 -1
- package/dist/output/lens-corrections.d.ts +12 -2
- package/dist/output/lens-corrections.js +22 -7
- package/dist/output/lens-corrections.js.map +1 -1
- package/dist/output/print-contrast-reference.js +1 -1
- package/dist/output/print-contrast-reference.js.map +1 -1
- package/dist/output/print-plan.d.ts +41 -5
- package/dist/output/print-plan.js +59 -14
- package/dist/output/print-plan.js.map +1 -1
- package/dist/output/print-size-blur-noise.d.ts +110 -0
- package/dist/output/print-size-blur-noise.js +138 -0
- package/dist/output/print-size-blur-noise.js.map +1 -0
- package/dist/sensor/binomial-sample.d.ts +3 -0
- package/dist/sensor/binomial-sample.js +86 -0
- package/dist/sensor/binomial-sample.js.map +1 -0
- package/dist/sensor/eqe-electron-rate.js +1 -7
- package/dist/sensor/eqe-electron-rate.js.map +1 -1
- package/dist/sensor/exposure-window.d.ts +2 -0
- package/dist/sensor/exposure-window.js +2 -2
- package/dist/sensor/exposure-window.js.map +1 -1
- package/dist/sensor/readout-timing.js +2 -23
- package/dist/sensor/readout-timing.js.map +1 -1
- package/dist/sensor/response-application-compatibility.js +1 -6
- package/dist/sensor/response-application-compatibility.js.map +1 -1
- package/dist/sensor/responsivity-photocurrent.js +1 -7
- package/dist/sensor/responsivity-photocurrent.js.map +1 -1
- package/dist/sensor/sensor-characteristics.d.ts +147 -0
- package/dist/sensor/sensor-characteristics.js +126 -0
- package/dist/sensor/sensor-characteristics.js.map +1 -0
- package/dist/sensor/spatial-sample-reduction.js +1 -5
- package/dist/sensor/spatial-sample-reduction.js.map +1 -1
- package/dist/sensor/spectral-quadrature.js +1 -6
- package/dist/sensor/spectral-quadrature.js.map +1 -1
- package/dist/sensor/temporal-photo-signal.d.ts +3 -0
- package/dist/sensor/temporal-photo-signal.js +5 -2
- package/dist/sensor/temporal-photo-signal.js.map +1 -1
- package/dist/stabilization/system.d.ts +9 -0
- package/dist/stabilization/system.js +5 -6
- package/dist/stabilization/system.js.map +1 -1
- package/docs/AGENT_GUIDE.md +5 -0
- package/docs/API_REFERENCE.md +173 -2
- package/docs/QUICK_START.md +213 -6
- package/docs/USAGE.md +157 -3
- package/docs/api/capture-native-raw-dng.md +6 -0
- package/docs/api/core-version.md +1 -1
- package/docs/api/equipment-generic-lens-catalogue.md +17 -7
- package/docs/api/equipment-sensor-profile-body.md +64 -0
- package/docs/api/equipment-sensor-profiles.md +86 -0
- package/docs/api/exports.json +357 -2
- package/docs/api/facade-display-view-gpu.md +5 -2
- package/docs/api/facade-exposure-build-up.md +105 -0
- package/docs/api/facade-fast-methods.md +99 -0
- package/docs/api/facade-image-plane-enable.md +16 -0
- package/docs/api/facade-mixed-illumination.md +67 -0
- package/docs/api/facade-representative-light-sources.md +131 -0
- package/docs/api/facade-scene-radiance.md +54 -3
- package/docs/api/facade-simulate-capture.md +60 -15
- package/docs/api/facade-simulate-display-view.md +3 -2
- package/docs/api/motion-subject-motion.md +23 -0
- package/docs/api/optics-aperture-diameter.md +51 -0
- package/docs/api/optics-detail-limit.md +203 -0
- package/docs/api/optics-generic-lens-lateral-color.md +79 -0
- package/docs/api/output-crop-to-match-focal-length.md +67 -0
- package/docs/api/output-developed-lens-corrections.md +10 -1
- package/docs/api/output-generic-lens-corrections.md +40 -2
- package/docs/api/output-lens-corrections.md +11 -1
- package/docs/api/output-print-plan.md +35 -1
- package/docs/api/output-print-size-blur-noise.md +115 -0
- package/docs/api/sensor-sensor-characteristics.md +105 -0
- package/llms.txt +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Which limits detail for given settings: the lens (diffraction at this f-number, aberrations, defocus) or the sensor's pixel
|
|
4
|
+
* pitch (#575, for the app's Learn section).
|
|
5
|
+
*
|
|
6
|
+
* Model (`detail-limit` 1.0.0; the decision rule is an approximation, see below):
|
|
7
|
+
* - **Optics.** The incoherent optical transfer function of a uniformly lit circular pupil carrying the wavefront W (in waves)
|
|
8
|
+
* is the normalized autocorrelation of the pupil function P = exp(i·2π·W):
|
|
9
|
+
* OTF(s) = (1/π)∫∫ P(ρ + s·e)·P*(ρ − s·e) d²ρ over the overlap of the two unit discs, s = ν/ν_c, ν_c = 1/(λ·N_w), N_w the
|
|
10
|
+
* working f-number (thin-lens image distance / pupil diameter). With W = 0 it is the diffraction-limited
|
|
11
|
+
* (2/π)(acos s − s√(1 − s²)) (MIT OpenCourseWare 2.71 Optics, Spring 2009, lectures 21–22, free; also see Goodman,
|
|
12
|
+
* Introduction to Fourier Optics, 4th ed., §7.3–7.4). The integral is evaluated by Gauss–Legendre quadrature on a smooth
|
|
13
|
+
* parametrisation of the overlap (√ edge removed), with node counts that grow with the phase variation; MTF = |OTF|.
|
|
14
|
+
* - **Wavefront.** `lens.ideal`: none. `lens.catalogue`: the lens's resolved Seidel terms (`resolveGenericLens`, the same
|
|
15
|
+
* expansion as its PSF: Hopkins 1950; Wyant & Creath 1992, Applied Optics and Optical Engineering XI ch. 1, free author copy)
|
|
16
|
+
* at the field height H, with axial color at the wavelength. Defocus of a subject away from the focus distance adds
|
|
17
|
+
* W020 = dz/(8·N_w²), dz = sensor minus image distance (thin lens; `calculateDefocusWavefront`, Wyant & Creath eq. 18).
|
|
18
|
+
* - **Sampling.** Nyquist frequency 1/(2p) for pixel pitch p (exact for the lattice); a Bayer filter's red and blue sites
|
|
19
|
+
* repeat every 2p, so their Nyquist frequency is 1/(4p), while its green sites still sample rows and columns up to 1/(2p).
|
|
20
|
+
* Pixel aperture MTF |sinc(ν·p)| for a square, fully sensitive site (fill factor 1, an idealisation).
|
|
21
|
+
* - **Decision (approximation).** The sensor limits when the optics still pass at least 50 % contrast at the Nyquist
|
|
22
|
+
* frequency (optics MTF50 ≥ 1/(2p)): a denser sensor would record finer detail. Otherwise the lens limits, and the cause is
|
|
23
|
+
* diffraction when the wavefront error is within the Maréchal criterion (RMS ≤ λ/14, Strehl ≈ 0.8; Wyant & Creath 1992
|
|
24
|
+
* eq. 67), else aberrations or defocus, whichever has the larger RMS wavefront error.
|
|
25
|
+
* @see docs/PHYSICS_FOUNDATION.md "Which limits detail: the lens or the sensor" for the equations, exactness and limits.
|
|
26
|
+
*/
|
|
27
|
+
import { approximationResult } from "../core/calculation-result.js";
|
|
28
|
+
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
29
|
+
import { InvalidScientificInputError, requirePositiveFinite, requirePositiveInteger } from "../core/validation.js";
|
|
30
|
+
import { requireInputKeys, requireInputObject } from "../core/record-validation.js";
|
|
31
|
+
import { genericLensDispersion, resolveGenericLens } from "../equipment/generic-lens-catalogue.js";
|
|
32
|
+
/** Version of the `detail-limit` model. */
|
|
33
|
+
const DETAIL_LIMIT_MODEL_VERSION = "1.0.0";
|
|
34
|
+
/**
|
|
35
|
+
* Limits of `calculateDetailLimit`: the wavelength range and the largest wavefront (sum of the magnitudes of its terms, in
|
|
36
|
+
* waves) whose transfer function is integrated. Beyond it the blur is geometric and the quadrature would grow without
|
|
37
|
+
* bound; use `calculateDefocusCircle` for such a blur.
|
|
38
|
+
*/
|
|
39
|
+
export const DETAIL_LIMIT_LIMITS = /* @__PURE__ */ Object.freeze({ minimumWavelengthNm: 380, maximumWavelengthNm: 780, maximumWavefrontWaves: 64 });
|
|
40
|
+
/** The Maréchal criterion: RMS wavefront error λ/14 (Strehl ratio ≈ 0.8), below which a lens counts as diffraction-limited. */
|
|
41
|
+
const MARECHAL_RMS_WAVES = 1 / 14;
|
|
42
|
+
const BASIS = /* @__PURE__ */ Object.freeze({
|
|
43
|
+
optics: Object.freeze({ kind: "exact", statement: "Scalar incoherent OTF of a circular pupil carrying the stated wavefront at one wavelength " +
|
|
44
|
+
"(numerical quadrature, converged to about 1e-9). The wavefront itself is exact for lens.ideal (none) and an approximation for lens.catalogue." }),
|
|
45
|
+
sampling: Object.freeze({ kind: "exact", statement: "Nyquist frequencies of the pixel lattice and of a Bayer filter's color lattices; pixel aperture " +
|
|
46
|
+
"MTF of an ideal square site with fill factor 1 (an idealisation: no microlens, crosstalk or optical low-pass filter)." }),
|
|
47
|
+
system: Object.freeze({ kind: "approximation", statement: "Optics MTF × pixel aperture MTF. Demosaicing, sharpening, noise, motion and aliasing are excluded." }),
|
|
48
|
+
limitedBy: Object.freeze({ kind: "approximation", statement: "The sensor limits when the optics MTF50 reaches the Nyquist frequency 1/(2p); otherwise " +
|
|
49
|
+
"the lens, through diffraction when the RMS wavefront error is within λ/14 (Maréchal), else aberrations or defocus, whichever RMS is larger." })
|
|
50
|
+
});
|
|
51
|
+
/** Gauss–Legendre nodes and weights on [0, 1] (Golub–Welsch by Newton on the Legendre recurrence), cached by count. */
|
|
52
|
+
const GL_CACHE = new Map();
|
|
53
|
+
function gaussLegendreUnit(n) {
|
|
54
|
+
let rule = GL_CACHE.get(n);
|
|
55
|
+
if (rule)
|
|
56
|
+
return rule;
|
|
57
|
+
const x = new Float64Array(n), w = new Float64Array(n);
|
|
58
|
+
for (let i = 0; i < n; i++) {
|
|
59
|
+
let z = Math.cos(Math.PI * (i + 0.75) / (n + 0.5)), dp = 0;
|
|
60
|
+
for (let it = 0; it < 100; it++) {
|
|
61
|
+
let p0 = 1, p1 = z;
|
|
62
|
+
for (let k = 2; k <= n; k++) {
|
|
63
|
+
const p2 = ((2 * k - 1) * z * p1 - (k - 1) * p0) / k;
|
|
64
|
+
p0 = p1;
|
|
65
|
+
p1 = p2;
|
|
66
|
+
}
|
|
67
|
+
const pn = n === 1 ? z : p1, pm = n === 1 ? 1 : p0;
|
|
68
|
+
dp = n * (z * pn - pm) / (z * z - 1);
|
|
69
|
+
const step = pn / dp;
|
|
70
|
+
z -= step;
|
|
71
|
+
if (Math.abs(step) < 1e-15)
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
x[i] = (1 - z) / 2;
|
|
75
|
+
w[i] = 1 / ((1 - z * z) * dp * dp);
|
|
76
|
+
}
|
|
77
|
+
rule = { x, w };
|
|
78
|
+
GL_CACHE.set(n, rule);
|
|
79
|
+
return rule;
|
|
80
|
+
}
|
|
81
|
+
function wavefrontWaves(w, px, py) {
|
|
82
|
+
const r2 = px * px + py * py;
|
|
83
|
+
return (w.defocus + w.fieldCurvature) * r2 + w.spherical * r2 * r2 + w.coma * r2 * py + w.astigmatism * py * py;
|
|
84
|
+
}
|
|
85
|
+
function wavefrontMagnitude(w) {
|
|
86
|
+
return Math.abs(w.defocus) + Math.abs(w.spherical) + Math.abs(w.coma) + Math.abs(w.astigmatism) + Math.abs(w.fieldCurvature);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* RMS of the wavefront over the unit disc with piston and tilt removed (tilt only moves the image, so the MTF and the Strehl
|
|
90
|
+
* ratio do not depend on it; Wyant & Creath 1992, sec. on RMS wavefront error). Gauss–Legendre in ρ² and the midpoint rule in
|
|
91
|
+
* angle, exact for these polynomials.
|
|
92
|
+
*/
|
|
93
|
+
function rmsWaves(w) {
|
|
94
|
+
const radial = gaussLegendreUnit(8), angles = 16;
|
|
95
|
+
let mean = 0, square = 0, tiltX = 0, tiltY = 0;
|
|
96
|
+
for (let i = 0; i < radial.x.length; i++) {
|
|
97
|
+
const rho = Math.sqrt(radial.x[i]);
|
|
98
|
+
for (let j = 0; j < angles; j++) {
|
|
99
|
+
const t = 2 * Math.PI * (j + 0.5) / angles, x = rho * Math.cos(t), y = rho * Math.sin(t);
|
|
100
|
+
const value = wavefrontWaves(w, x, y), weight = radial.w[i] / angles;
|
|
101
|
+
mean += weight * value;
|
|
102
|
+
square += weight * value * value;
|
|
103
|
+
tiltX += weight * value * x;
|
|
104
|
+
tiltY += weight * value * y;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Least-squares tilt c·x has c = <W x>/<x²> with <x²> = 1/4, and removes c²/4 of the variance (x is orthogonal to piston).
|
|
108
|
+
return Math.sqrt(Math.max(0, square - mean * mean - 4 * (tiltX * tiltX + tiltY * tiltY)));
|
|
109
|
+
}
|
|
110
|
+
/** Diffraction-limited MTF (2/π)(acos s − s√(1 − s²)), 0 from s = 1. */
|
|
111
|
+
function diffractionMtf(s) {
|
|
112
|
+
return s >= 1 ? 0 : (2 / Math.PI) * (Math.acos(s) - s * Math.sqrt(1 - s * s));
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* @internal |OTF| at normalized frequency s (0 ≤ s < 1) along the pupil's x axis (`tangential` false) or y axis (true). The overlap
|
|
116
|
+
* |ρ ± s·e| ≤ 1 is |a| ≤ 1 − s, |b| ≤ √(1 − (|a| + s)²) in coordinates (a along e, b across); |a| = (1 − s)(1 − τ²) removes
|
|
117
|
+
* the square-root edge, so the integrand is smooth in τ and in b/b_max.
|
|
118
|
+
*/
|
|
119
|
+
export function detailLimitOpticsMtf(w, s, tangential, nodeScale = 1) {
|
|
120
|
+
const magnitude = wavefrontMagnitude(w);
|
|
121
|
+
if (s >= 1)
|
|
122
|
+
return 0;
|
|
123
|
+
if (magnitude === 0)
|
|
124
|
+
return diffractionMtf(s);
|
|
125
|
+
// The phase difference W(ρ + s e) − W(ρ − s e) varies by at most about 8·s·magnitude waves over the overlap.
|
|
126
|
+
const cycles = 8 * s * magnitude, n = Math.ceil(nodeScale * Math.min(1200, 12 + Math.ceil(2.2 * cycles)));
|
|
127
|
+
const rule = gaussLegendreUnit(n), half = 1 - s;
|
|
128
|
+
let re = 0, im = 0;
|
|
129
|
+
for (let i = 0; i < n; i++) {
|
|
130
|
+
const tau = rule.x[i], a = half * (1 - tau * tau), da = 2 * half * tau * rule.w[i];
|
|
131
|
+
const bMax = Math.sqrt(Math.max(0, (1 - a - s) * (1 + a + s)));
|
|
132
|
+
for (const sign of [1, -1]) {
|
|
133
|
+
const along = sign * a;
|
|
134
|
+
for (let j = 0; j < n; j++) {
|
|
135
|
+
const b = bMax * (2 * rule.x[j] - 1), weight = da * 2 * bMax * rule.w[j];
|
|
136
|
+
const phase = tangential
|
|
137
|
+
? wavefrontWaves(w, b, along + s) - wavefrontWaves(w, b, along - s)
|
|
138
|
+
: wavefrontWaves(w, along + s, b) - wavefrontWaves(w, along - s, b);
|
|
139
|
+
re += weight * Math.cos(2 * Math.PI * phase);
|
|
140
|
+
im += weight * Math.sin(2 * Math.PI * phase);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return Math.min(1, Math.hypot(re, im) / Math.PI);
|
|
145
|
+
}
|
|
146
|
+
/** Smallest s in (0, 1) where f(s) ≤ 0.5: a scan from 0 with the given step, then bisection. */
|
|
147
|
+
function firstHalfCrossing(f, step) {
|
|
148
|
+
let lo = 0;
|
|
149
|
+
for (let s = step;; s += step) {
|
|
150
|
+
const hi = Math.min(s, 1);
|
|
151
|
+
if (f(hi) <= 0.5) {
|
|
152
|
+
let a = lo, b = hi;
|
|
153
|
+
for (let it = 0; it < 60 && b - a > 1e-13 * b; it++) {
|
|
154
|
+
const m = (a + b) / 2;
|
|
155
|
+
if (f(m) <= 0.5)
|
|
156
|
+
b = m;
|
|
157
|
+
else
|
|
158
|
+
a = m;
|
|
159
|
+
}
|
|
160
|
+
return (a + b) / 2;
|
|
161
|
+
}
|
|
162
|
+
if (hi >= 1)
|
|
163
|
+
return 1;
|
|
164
|
+
lo = hi;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/** The diffraction-limited MTF50 as a fraction of the cut-off: (2/π)(acos s − s√(1 − s²)) = 0.5, s ≈ 0.40397. */
|
|
168
|
+
function diffractionMtf50Fraction() {
|
|
169
|
+
let a = 0, b = 1;
|
|
170
|
+
for (let it = 0; it < 60; it++) {
|
|
171
|
+
const m = (a + b) / 2;
|
|
172
|
+
if (diffractionMtf(m) <= 0.5)
|
|
173
|
+
b = m;
|
|
174
|
+
else
|
|
175
|
+
a = m;
|
|
176
|
+
}
|
|
177
|
+
return (a + b) / 2;
|
|
178
|
+
}
|
|
179
|
+
function sinc(x) {
|
|
180
|
+
return x === 0 ? 1 : Math.abs(Math.sin(Math.PI * x) / (Math.PI * x));
|
|
181
|
+
}
|
|
182
|
+
/** Thin-lens image distance in mm of an object at d meters, or f at infinity (d = null). */
|
|
183
|
+
function imageDistanceMm(focalLengthMm, distanceM, label) {
|
|
184
|
+
if (distanceM === null)
|
|
185
|
+
return focalLengthMm;
|
|
186
|
+
const d = distanceM * 1000;
|
|
187
|
+
if (!(d > focalLengthMm))
|
|
188
|
+
throw new InvalidScientificInputError(`${label} must be greater than the focal length (${focalLengthMm} mm).`);
|
|
189
|
+
return (focalLengthMm * d) / (d - focalLengthMm);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Which limits detail for given settings: the lens (diffraction at this f-number, aberrations, defocus) or the sensor's pixel
|
|
193
|
+
* pitch. Returns the optics' resolution (MTF50 and the diffraction cut-off, cycles/mm), the sampling's (Nyquist frequency
|
|
194
|
+
* and, with a Bayer filter, the red and blue sites'), the system MTF50 (optics × pixel aperture), the f-number from which
|
|
195
|
+
* diffraction limits on this sensor, and which one limits.
|
|
196
|
+
*
|
|
197
|
+
* The sensor limits when the optics still pass 50 % contrast at the Nyquist frequency 1/(2p); otherwise the lens does,
|
|
198
|
+
* through diffraction when its wavefront error is within the Maréchal criterion (RMS ≤ λ/14), else through aberrations or
|
|
199
|
+
* defocus. The optics MTF is the exact scalar OTF of the stated wavefront at one wavelength; the decision rule and the
|
|
200
|
+
* catalogue lenses' wavefronts are approximations.
|
|
201
|
+
*
|
|
202
|
+
* Example: an ideal 50 mm lens at f/22 on a 36 × 24 mm, 6000-pixel-wide sensor (6 µm) is limited by diffraction (optics
|
|
203
|
+
* MTF50 ≈ 33 cycles/mm against a Nyquist frequency of ≈ 83); at f/4 the pixel pitch limits.
|
|
204
|
+
*
|
|
205
|
+
* @param input Sensor, lens, f-number, and optional focus, field position and wavelength.
|
|
206
|
+
* @returns The resolution of the optics and of the sampling, and which limits detail.
|
|
207
|
+
*/
|
|
208
|
+
export function calculateDetailLimit(input) {
|
|
209
|
+
requireInputKeys(input, "calculateDetailLimit", ["sensor", "lens", "fNumber", "focus", "fieldPosition", "wavelengthNm"]);
|
|
210
|
+
requireInputObject(input.sensor, "sensor");
|
|
211
|
+
requireInputKeys(input.sensor, "sensor", ["widthMm", "heightMm", "pixelWidth", "colorFilterArray"]);
|
|
212
|
+
requirePositiveFinite("sensor.widthMm", input.sensor.widthMm);
|
|
213
|
+
requirePositiveFinite("sensor.heightMm", input.sensor.heightMm);
|
|
214
|
+
requirePositiveInteger("sensor.pixelWidth", input.sensor.pixelWidth);
|
|
215
|
+
const cfa = input.sensor.colorFilterArray ?? "bayer";
|
|
216
|
+
if (cfa !== "bayer" && cfa !== "none")
|
|
217
|
+
throw new InvalidConfigurationError("sensor.colorFilterArray must be \"bayer\" or \"none\".");
|
|
218
|
+
requirePositiveFinite("fNumber", input.fNumber);
|
|
219
|
+
const wavelengthNm = input.wavelengthNm ?? 550;
|
|
220
|
+
if (typeof wavelengthNm !== "number" || !(wavelengthNm >= DETAIL_LIMIT_LIMITS.minimumWavelengthNm && wavelengthNm <= DETAIL_LIMIT_LIMITS.maximumWavelengthNm)) {
|
|
221
|
+
throw new InvalidScientificInputError("wavelengthNm must be a number from 380 through 780.");
|
|
222
|
+
}
|
|
223
|
+
const fieldPosition = input.fieldPosition ?? 0;
|
|
224
|
+
if (typeof fieldPosition !== "number" || !(fieldPosition >= 0 && fieldPosition <= 1)) {
|
|
225
|
+
throw new InvalidScientificInputError("fieldPosition must be a number from 0 (center) through 1 (corner).");
|
|
226
|
+
}
|
|
227
|
+
let focusDistanceM = null, subjectDistanceM = null;
|
|
228
|
+
if (input.focus !== undefined) {
|
|
229
|
+
requireInputObject(input.focus, "focus");
|
|
230
|
+
requireInputKeys(input.focus, "focus", ["focusDistanceM", "subjectDistanceM"]);
|
|
231
|
+
requirePositiveFinite("focus.focusDistanceM", input.focus.focusDistanceM);
|
|
232
|
+
focusDistanceM = input.focus.focusDistanceM;
|
|
233
|
+
if (input.focus.subjectDistanceM !== undefined)
|
|
234
|
+
requirePositiveFinite("focus.subjectDistanceM", input.focus.subjectDistanceM);
|
|
235
|
+
subjectDistanceM = input.focus.subjectDistanceM ?? focusDistanceM;
|
|
236
|
+
}
|
|
237
|
+
const lens = input.lens;
|
|
238
|
+
requireInputObject(lens, "lens");
|
|
239
|
+
const wavefront = { defocus: 0, spherical: 0, coma: 0, astigmatism: 0, fieldCurvature: 0 };
|
|
240
|
+
let focalLengthMm;
|
|
241
|
+
if (lens.kind === "ideal") {
|
|
242
|
+
requireInputKeys(lens, "lens", ["kind", "focalLengthMm"]);
|
|
243
|
+
requirePositiveFinite("lens.focalLengthMm", lens.focalLengthMm);
|
|
244
|
+
focalLengthMm = lens.focalLengthMm;
|
|
245
|
+
}
|
|
246
|
+
else if (lens.kind === "catalogue") {
|
|
247
|
+
requireInputKeys(lens, "lens", ["kind", "lensId"]);
|
|
248
|
+
const resolved = resolveGenericLens({ lensId: lens.lensId, activeImagingArea: { widthMm: input.sensor.widthMm, heightMm: input.sensor.heightMm },
|
|
249
|
+
fNumber: input.fNumber, ...(focusDistanceM === null ? {} : { focusDistanceM }) });
|
|
250
|
+
focalLengthMm = resolved.focalLengthMm;
|
|
251
|
+
// Waves at 550 nm (the catalogue's design wavelength) → waves at λ: OPD is fixed, so scale by 550/λ.
|
|
252
|
+
const a = resolved.character.aberrations, H = fieldPosition, scale = 550 / wavelengthNm;
|
|
253
|
+
wavefront.defocus = scale * (a.balancingDefocusWaves + a.axialColorWaves * genericLensDispersion(wavelengthNm));
|
|
254
|
+
wavefront.spherical = scale * a.sphericalWaves;
|
|
255
|
+
wavefront.coma = scale * a.comaWaves * H;
|
|
256
|
+
wavefront.astigmatism = scale * a.astigmatismWaves * H * H;
|
|
257
|
+
wavefront.fieldCurvature = scale * a.fieldCurvatureWaves * H * H;
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
throw new InvalidConfigurationError("lens.kind must be \"ideal\" or \"catalogue\".");
|
|
261
|
+
}
|
|
262
|
+
const lambdaMm = wavelengthNm * 1e-6;
|
|
263
|
+
const sensorImageMm = imageDistanceMm(focalLengthMm, focusDistanceM, "focus.focusDistanceM");
|
|
264
|
+
const subjectImageMm = imageDistanceMm(focalLengthMm, subjectDistanceM, "focus.subjectDistanceM");
|
|
265
|
+
const pupilMm = focalLengthMm / input.fNumber, workingFNumber = sensorImageMm / pupilMm;
|
|
266
|
+
// W020 = dz/(8 N_w²), dz = sensor minus image distance (calculateDefocusWavefront's convention), in waves.
|
|
267
|
+
const defocusDz = sensorImageMm - subjectImageMm, defocusWaves = defocusDz / (8 * workingFNumber * workingFNumber) / lambdaMm;
|
|
268
|
+
const defocusBlurDiameterMm = Math.abs(defocusDz) * pupilMm / subjectImageMm;
|
|
269
|
+
const aberrationRms = rmsWaves(wavefront);
|
|
270
|
+
wavefront.defocus += defocusWaves;
|
|
271
|
+
const totalRms = rmsWaves(wavefront), defocusRms = Math.abs(defocusWaves) / (2 * Math.sqrt(3));
|
|
272
|
+
const magnitude = wavefrontMagnitude(wavefront);
|
|
273
|
+
if (magnitude > DETAIL_LIMIT_LIMITS.maximumWavefrontWaves) {
|
|
274
|
+
throw new InvalidScientificInputError(`The wavefront (${magnitude.toPrecision(3)} waves, defocus blur ${defocusBlurDiameterMm.toPrecision(3)} mm) exceeds ` +
|
|
275
|
+
`${DETAIL_LIMIT_LIMITS.maximumWavefrontWaves} waves: the blur is geometric; use calculateDefocusCircle for it.`);
|
|
276
|
+
}
|
|
277
|
+
const pitchMm = input.sensor.widthMm / input.sensor.pixelWidth, cutoff = 1 / (lambdaMm * workingFNumber), nyquist = 1 / (2 * pitchMm);
|
|
278
|
+
const step = Math.min(1 / 128, 1 / (32 * Math.max(1, magnitude)));
|
|
279
|
+
const symmetric = wavefront.coma === 0 && wavefront.astigmatism === 0;
|
|
280
|
+
const opticsAt = (s, tangential) => detailLimitOpticsMtf(wavefront, s, tangential);
|
|
281
|
+
const tangential50 = firstHalfCrossing((s) => opticsAt(s, true), step) * cutoff;
|
|
282
|
+
const sagittal50 = symmetric ? tangential50 : firstHalfCrossing((s) => opticsAt(s, false), step) * cutoff;
|
|
283
|
+
const systemAt = (s, t) => opticsAt(s, t) * sinc(s * cutoff * pitchMm);
|
|
284
|
+
const systemTangential50 = firstHalfCrossing((s) => systemAt(s, true), step) * cutoff;
|
|
285
|
+
const system50 = symmetric ? systemTangential50 : Math.min(systemTangential50, firstHalfCrossing((s) => systemAt(s, false), step) * cutoff);
|
|
286
|
+
const sNyquist = nyquist / cutoff;
|
|
287
|
+
const opticsAtNyquist = symmetric ? opticsAt(sNyquist, true) : Math.min(opticsAt(sNyquist, true), opticsAt(sNyquist, false));
|
|
288
|
+
const mtf50 = Math.min(tangential50, sagittal50), fraction50 = diffractionMtf50Fraction();
|
|
289
|
+
const limitedBy = mtf50 >= nyquist ? "sensor" : "lens";
|
|
290
|
+
const limitingFactor = limitedBy === "sensor" ? "pixel-pitch"
|
|
291
|
+
: totalRms <= MARECHAL_RMS_WAVES ? "diffraction" : defocusRms > aberrationRms ? "defocus" : "aberrations";
|
|
292
|
+
return approximationResult({
|
|
293
|
+
limitedBy,
|
|
294
|
+
limitingFactor,
|
|
295
|
+
opticsToNyquistRatio: mtf50 / nyquist,
|
|
296
|
+
fNumber: input.fNumber,
|
|
297
|
+
wavelengthNm,
|
|
298
|
+
focalLengthMm,
|
|
299
|
+
fieldPosition,
|
|
300
|
+
optics: {
|
|
301
|
+
workingFNumber,
|
|
302
|
+
cutoffCyclesPerMm: cutoff,
|
|
303
|
+
mtf50CyclesPerMm: mtf50,
|
|
304
|
+
tangentialMtf50CyclesPerMm: tangential50,
|
|
305
|
+
sagittalMtf50CyclesPerMm: sagittal50,
|
|
306
|
+
diffractionMtf50CyclesPerMm: fraction50 * cutoff,
|
|
307
|
+
mtfAtNyquist: opticsAtNyquist,
|
|
308
|
+
rmsWavefrontErrorWaves: { aberrations: aberrationRms, defocus: defocusRms, total: totalRms },
|
|
309
|
+
withinMarechalCriterion: totalRms <= MARECHAL_RMS_WAVES,
|
|
310
|
+
defocusBlurDiameterMm
|
|
311
|
+
},
|
|
312
|
+
sampling: {
|
|
313
|
+
colorFilterArray: cfa,
|
|
314
|
+
pixelPitchMicrometers: pitchMm * 1000,
|
|
315
|
+
nyquistCyclesPerMm: nyquist,
|
|
316
|
+
redBlueNyquistCyclesPerMm: cfa === "bayer" ? nyquist / 2 : null,
|
|
317
|
+
pixelApertureMtfAtNyquist: 2 / Math.PI,
|
|
318
|
+
pixelApertureMtf50CyclesPerMm: 0.6033545923 / pitchMm
|
|
319
|
+
},
|
|
320
|
+
system: {
|
|
321
|
+
mtf50CyclesPerMm: system50,
|
|
322
|
+
mtf50LinePairsPerPictureHeight: system50 * input.sensor.heightMm,
|
|
323
|
+
mtfAtNyquist: opticsAtNyquist * (2 / Math.PI)
|
|
324
|
+
},
|
|
325
|
+
diffractionLimitedFNumber: (2 * pitchMm * fraction50 / lambdaMm) * focalLengthMm / sensorImageMm,
|
|
326
|
+
basis: BASIS
|
|
327
|
+
}, "detail-limit", DETAIL_LIMIT_MODEL_VERSION, [
|
|
328
|
+
"Scalar, incoherent, monochromatic imaging at one wavelength (default 550 nm); lateral color excluded.",
|
|
329
|
+
"Uniformly lit circular pupil; thin-lens working f-number and defocus.",
|
|
330
|
+
"Pixel aperture: ideal square site with fill factor 1; no optical low-pass filter, microlens or crosstalk.",
|
|
331
|
+
"A Bayer filter's red and blue sites have half the Nyquist frequency; demosaicing is not modeled.",
|
|
332
|
+
"Decision rule: the sensor limits when the optics MTF50 reaches the Nyquist frequency; diffraction when the RMS wavefront error is within λ/14 (Maréchal).",
|
|
333
|
+
"Catalogue lenses are generic Photivra designs (labeled approximations), not measured products."
|
|
334
|
+
]);
|
|
335
|
+
}
|
|
336
|
+
//# sourceMappingURL=detail-limit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detail-limit.js","sourceRoot":"","sources":["../../src/optics/detail-limit.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Which limits detail for given settings: the lens (diffraction at this f-number, aberrations, defocus) or the sensor's pixel\n * pitch (#575, for the app's Learn section).\n *\n * Model (`detail-limit` 1.0.0; the decision rule is an approximation, see below):\n * - **Optics.** The incoherent optical transfer function of a uniformly lit circular pupil carrying the wavefront W (in waves)\n * is the normalized autocorrelation of the pupil function P = exp(i·2π·W):\n * OTF(s) = (1/π)∫∫ P(ρ + s·e)·P*(ρ − s·e) d²ρ over the overlap of the two unit discs, s = ν/ν_c, ν_c = 1/(λ·N_w), N_w the\n * working f-number (thin-lens image distance / pupil diameter). With W = 0 it is the diffraction-limited\n * (2/π)(acos s − s√(1 − s²)) (MIT OpenCourseWare 2.71 Optics, Spring 2009, lectures 21–22, free; also see Goodman,\n * Introduction to Fourier Optics, 4th ed., §7.3–7.4). The integral is evaluated by Gauss–Legendre quadrature on a smooth\n * parametrisation of the overlap (√ edge removed), with node counts that grow with the phase variation; MTF = |OTF|.\n * - **Wavefront.** `lens.ideal`: none. `lens.catalogue`: the lens's resolved Seidel terms (`resolveGenericLens`, the same\n * expansion as its PSF: Hopkins 1950; Wyant & Creath 1992, Applied Optics and Optical Engineering XI ch. 1, free author copy)\n * at the field height H, with axial color at the wavelength. Defocus of a subject away from the focus distance adds\n * W020 = dz/(8·N_w²), dz = sensor minus image distance (thin lens; `calculateDefocusWavefront`, Wyant & Creath eq. 18).\n * - **Sampling.** Nyquist frequency 1/(2p) for pixel pitch p (exact for the lattice); a Bayer filter's red and blue sites\n * repeat every 2p, so their Nyquist frequency is 1/(4p), while its green sites still sample rows and columns up to 1/(2p).\n * Pixel aperture MTF |sinc(ν·p)| for a square, fully sensitive site (fill factor 1, an idealisation).\n * - **Decision (approximation).** The sensor limits when the optics still pass at least 50 % contrast at the Nyquist\n * frequency (optics MTF50 ≥ 1/(2p)): a denser sensor would record finer detail. Otherwise the lens limits, and the cause is\n * diffraction when the wavefront error is within the Maréchal criterion (RMS ≤ λ/14, Strehl ≈ 0.8; Wyant & Creath 1992\n * eq. 67), else aberrations or defocus, whichever has the larger RMS wavefront error.\n * @see docs/PHYSICS_FOUNDATION.md \"Which limits detail: the lens or the sensor\" for the equations, exactness and limits.\n */\n\nimport { approximationResult, type CalculationResult } from \"../core/calculation-result.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { InvalidScientificInputError, requirePositiveFinite, requirePositiveInteger } from \"../core/validation.js\";\nimport { requireInputKeys, requireInputObject } from \"../core/record-validation.js\";\nimport { genericLensDispersion, resolveGenericLens } from \"../equipment/generic-lens-catalogue.js\";\n\n/** Version of the `detail-limit` model. */\nconst DETAIL_LIMIT_MODEL_VERSION = \"1.0.0\" as const;\n\n/**\n * Limits of `calculateDetailLimit`: the wavelength range and the largest wavefront (sum of the magnitudes of its terms, in\n * waves) whose transfer function is integrated. Beyond it the blur is geometric and the quadrature would grow without\n * bound; use `calculateDefocusCircle` for such a blur.\n */\nexport const DETAIL_LIMIT_LIMITS = /* @__PURE__ */ Object.freeze({ minimumWavelengthNm: 380, maximumWavelengthNm: 780, maximumWavefrontWaves: 64 });\n\n/** The Maréchal criterion: RMS wavefront error λ/14 (Strehl ratio ≈ 0.8), below which a lens counts as diffraction-limited. */\nconst MARECHAL_RMS_WAVES = 1 / 14;\n\n/** A lens with no aberrations (diffraction only). */\nexport interface DetailLimitIdealLens {\n kind: \"ideal\";\n /** Physical focal length in millimeters (for the working f-number and the defocus of a subject off the focus distance). */\n focalLengthMm: number;\n}\n\n/** A generic catalogue lens (`GENERIC_LENS_IDS`), resolved for the sensor as `resolveGenericLens` does. */\nexport interface DetailLimitCatalogueLens {\n kind: \"catalogue\";\n lensId: string;\n}\n\nexport interface CalculateDetailLimitInput {\n /** Active imaging area and horizontal pixel count; pitch = widthMm / pixelWidth. */\n sensor: {\n widthMm: number;\n heightMm: number;\n pixelWidth: number;\n /** Default `\"bayer\"`. `\"none\"`: every site records full detail (a monochrome sensor). */\n colorFilterArray?: \"bayer\" | \"none\";\n };\n lens: DetailLimitIdealLens | DetailLimitCatalogueLens;\n /** f-number set on the lens. */\n fNumber: number;\n /**\n * Focus and subject distances from the lens in meters. Omitted: focused at infinity, subject in focus. `subjectDistanceM`\n * omitted: the subject is at the focus distance.\n */\n focus?: { focusDistanceM: number; subjectDistanceM?: number };\n /** Field height as a fraction of the half-diagonal, 0 (center, default) to 1 (corner). Only aberrations depend on it. */\n fieldPosition?: number;\n /** Wavelength in nanometres, 380–780. Default 550. */\n wavelengthNm?: number;\n}\n\n/** Which component limits detail. */\nexport type DetailLimitedBy = \"lens\" | \"sensor\";\n/** What limits it: the lens's diffraction, aberrations or defocus, or the sensor's pixel pitch. */\nexport type DetailLimitingFactor = \"diffraction\" | \"aberrations\" | \"defocus\" | \"pixel-pitch\";\n\nexport interface DetailLimitOptics {\n /** Working f-number N_w = N·v/f (thin-lens image distance v; N at infinity focus). */\n workingFNumber: number;\n /** Diffraction cut-off 1/(λ·N_w) in cycles/mm: no optics passes detail beyond it. */\n cutoffCyclesPerMm: number;\n /** Frequency where the optics' MTF falls to 0.5, the lower of the two directions, cycles/mm. */\n mtf50CyclesPerMm: number;\n /** MTF50 for detail along the radius from the image center (tangential), cycles/mm. */\n tangentialMtf50CyclesPerMm: number;\n /** MTF50 for detail across the radius (sagittal), cycles/mm. Equal to the tangential value at the center. */\n sagittalMtf50CyclesPerMm: number;\n /** MTF50 of an aberration-free lens in focus at the same working f-number (0.40397·cut-off), cycles/mm. */\n diffractionMtf50CyclesPerMm: number;\n /** Optics MTF at the sensor's Nyquist frequency (lower direction). */\n mtfAtNyquist: number;\n /** RMS wavefront error in waves at the wavelength: of the aberrations, of the defocus, and of both together. */\n rmsWavefrontErrorWaves: { aberrations: number; defocus: number; total: number };\n /** Whether the total RMS wavefront error is within the Maréchal criterion λ/14. */\n withinMarechalCriterion: boolean;\n /** Geometric blur-circle diameter of the defocus on the sensor, mm (0 in focus). */\n defocusBlurDiameterMm: number;\n}\n\nexport interface DetailLimitSampling {\n colorFilterArray: \"bayer\" | \"none\";\n /** Pixel pitch in micrometres: widthMm / pixelWidth. */\n pixelPitchMicrometers: number;\n /** 1/(2p), cycles/mm: the finest detail the pixel grid records without aliasing (Bayer green sites too, along rows and columns). */\n nyquistCyclesPerMm: number;\n /** Bayer only: 1/(4p), the Nyquist frequency of the red and blue sites. Null without a color filter array. */\n redBlueNyquistCyclesPerMm: number | null;\n /** Pixel aperture MTF at the Nyquist frequency: sinc(1/2) = 2/π. */\n pixelApertureMtfAtNyquist: number;\n /** Frequency where the pixel aperture MTF falls to 0.5 (0.60335/p), cycles/mm: beyond the Nyquist frequency. */\n pixelApertureMtf50CyclesPerMm: number;\n}\n\nexport interface DetailLimitSystem {\n /** MTF50 of optics × pixel aperture (lower direction), cycles/mm. Detail above the Nyquist frequency aliases. */\n mtf50CyclesPerMm: number;\n /** The same in line pairs per picture height (cycles/mm × heightMm). */\n mtf50LinePairsPerPictureHeight: number;\n /** System MTF at the Nyquist frequency (lower direction). */\n mtfAtNyquist: number;\n}\n\n/** Whether a quantity is exact under its stated conditions or an approximation. */\nexport interface DetailLimitBasis { kind: \"exact\" | \"approximation\"; statement: string }\n\nexport interface DetailLimit {\n /** The lens or the sensor. */\n limitedBy: DetailLimitedBy;\n /** Diffraction, aberrations or defocus (the lens), or the pixel pitch (the sensor). */\n limitingFactor: DetailLimitingFactor;\n /** Optics MTF50 / Nyquist frequency: at least 1 when the sensor limits. */\n opticsToNyquistRatio: number;\n fNumber: number;\n wavelengthNm: number;\n /** Physical focal length used (the catalogue lens's on this sensor), mm. */\n focalLengthMm: number;\n fieldPosition: number;\n optics: DetailLimitOptics;\n sampling: DetailLimitSampling;\n system: DetailLimitSystem;\n /**\n * The f-number at which an aberration-free lens in focus has its MTF50 at the Nyquist frequency (2·p·0.40397/λ at\n * infinity focus): from it on, diffraction limits detail on this sensor whatever the lens.\n */\n diffractionLimitedFNumber: number;\n basis: Readonly<Record<\"optics\" | \"sampling\" | \"system\" | \"limitedBy\", DetailLimitBasis>>;\n}\n\nconst BASIS: DetailLimit[\"basis\"] = /* @__PURE__ */ Object.freeze({\n optics: Object.freeze({ kind: \"exact\", statement: \"Scalar incoherent OTF of a circular pupil carrying the stated wavefront at one wavelength \" +\n \"(numerical quadrature, converged to about 1e-9). The wavefront itself is exact for lens.ideal (none) and an approximation for lens.catalogue.\" }),\n sampling: Object.freeze({ kind: \"exact\", statement: \"Nyquist frequencies of the pixel lattice and of a Bayer filter's color lattices; pixel aperture \" +\n \"MTF of an ideal square site with fill factor 1 (an idealisation: no microlens, crosstalk or optical low-pass filter).\" }),\n system: Object.freeze({ kind: \"approximation\", statement: \"Optics MTF × pixel aperture MTF. Demosaicing, sharpening, noise, motion and aliasing are excluded.\" }),\n limitedBy: Object.freeze({ kind: \"approximation\", statement: \"The sensor limits when the optics MTF50 reaches the Nyquist frequency 1/(2p); otherwise \" +\n \"the lens, through diffraction when the RMS wavefront error is within λ/14 (Maréchal), else aberrations or defocus, whichever RMS is larger.\" })\n});\n\n/** Gauss–Legendre nodes and weights on [0, 1] (Golub–Welsch by Newton on the Legendre recurrence), cached by count. */\nconst GL_CACHE = new Map<number, { x: Float64Array; w: Float64Array }>();\nfunction gaussLegendreUnit(n: number): { x: Float64Array; w: Float64Array } {\n let rule = GL_CACHE.get(n);\n if (rule) return rule;\n const x = new Float64Array(n), w = new Float64Array(n);\n for (let i = 0; i < n; i++) {\n let z = Math.cos(Math.PI * (i + 0.75) / (n + 0.5)), dp = 0;\n for (let it = 0; it < 100; it++) {\n let p0 = 1, p1 = z;\n for (let k = 2; k <= n; k++) { const p2 = ((2 * k - 1) * z * p1 - (k - 1) * p0) / k; p0 = p1; p1 = p2; }\n const pn = n === 1 ? z : p1, pm = n === 1 ? 1 : p0;\n dp = n * (z * pn - pm) / (z * z - 1);\n const step = pn / dp;\n z -= step;\n if (Math.abs(step) < 1e-15) break;\n }\n x[i] = (1 - z) / 2;\n w[i] = 1 / ((1 - z * z) * dp * dp);\n }\n rule = { x, w };\n GL_CACHE.set(n, rule);\n return rule;\n}\n\n/** @internal Seidel and defocus terms of the wavefront in waves at the wavelength, field along +y. */\nexport interface DetailLimitWavefront { defocus: number; spherical: number; coma: number; astigmatism: number; fieldCurvature: number }\n\nfunction wavefrontWaves(w: DetailLimitWavefront, px: number, py: number): number {\n const r2 = px * px + py * py;\n return (w.defocus + w.fieldCurvature) * r2 + w.spherical * r2 * r2 + w.coma * r2 * py + w.astigmatism * py * py;\n}\n\nfunction wavefrontMagnitude(w: DetailLimitWavefront): number {\n return Math.abs(w.defocus) + Math.abs(w.spherical) + Math.abs(w.coma) + Math.abs(w.astigmatism) + Math.abs(w.fieldCurvature);\n}\n\n/**\n * RMS of the wavefront over the unit disc with piston and tilt removed (tilt only moves the image, so the MTF and the Strehl\n * ratio do not depend on it; Wyant & Creath 1992, sec. on RMS wavefront error). Gauss–Legendre in ρ² and the midpoint rule in\n * angle, exact for these polynomials.\n */\nfunction rmsWaves(w: DetailLimitWavefront): number {\n const radial = gaussLegendreUnit(8), angles = 16;\n let mean = 0, square = 0, tiltX = 0, tiltY = 0;\n for (let i = 0; i < radial.x.length; i++) {\n const rho = Math.sqrt(radial.x[i]!);\n for (let j = 0; j < angles; j++) {\n const t = 2 * Math.PI * (j + 0.5) / angles, x = rho * Math.cos(t), y = rho * Math.sin(t);\n const value = wavefrontWaves(w, x, y), weight = radial.w[i]! / angles;\n mean += weight * value;\n square += weight * value * value;\n tiltX += weight * value * x;\n tiltY += weight * value * y;\n }\n }\n // Least-squares tilt c·x has c = <W x>/<x²> with <x²> = 1/4, and removes c²/4 of the variance (x is orthogonal to piston).\n return Math.sqrt(Math.max(0, square - mean * mean - 4 * (tiltX * tiltX + tiltY * tiltY)));\n}\n\n/** Diffraction-limited MTF (2/π)(acos s − s√(1 − s²)), 0 from s = 1. */\nfunction diffractionMtf(s: number): number {\n return s >= 1 ? 0 : (2 / Math.PI) * (Math.acos(s) - s * Math.sqrt(1 - s * s));\n}\n\n/**\n * @internal |OTF| at normalized frequency s (0 ≤ s < 1) along the pupil's x axis (`tangential` false) or y axis (true). The overlap\n * |ρ ± s·e| ≤ 1 is |a| ≤ 1 − s, |b| ≤ √(1 − (|a| + s)²) in coordinates (a along e, b across); |a| = (1 − s)(1 − τ²) removes\n * the square-root edge, so the integrand is smooth in τ and in b/b_max.\n */\nexport function detailLimitOpticsMtf(w: DetailLimitWavefront, s: number, tangential: boolean, nodeScale = 1): number {\n const magnitude = wavefrontMagnitude(w);\n if (s >= 1) return 0;\n if (magnitude === 0) return diffractionMtf(s);\n // The phase difference W(ρ + s e) − W(ρ − s e) varies by at most about 8·s·magnitude waves over the overlap.\n const cycles = 8 * s * magnitude, n = Math.ceil(nodeScale * Math.min(1200, 12 + Math.ceil(2.2 * cycles)));\n const rule = gaussLegendreUnit(n), half = 1 - s;\n let re = 0, im = 0;\n for (let i = 0; i < n; i++) {\n const tau = rule.x[i]!, a = half * (1 - tau * tau), da = 2 * half * tau * rule.w[i]!;\n const bMax = Math.sqrt(Math.max(0, (1 - a - s) * (1 + a + s)));\n for (const sign of [1, -1]) {\n const along = sign * a;\n for (let j = 0; j < n; j++) {\n const b = bMax * (2 * rule.x[j]! - 1), weight = da * 2 * bMax * rule.w[j]!;\n const phase = tangential\n ? wavefrontWaves(w, b, along + s) - wavefrontWaves(w, b, along - s)\n : wavefrontWaves(w, along + s, b) - wavefrontWaves(w, along - s, b);\n re += weight * Math.cos(2 * Math.PI * phase);\n im += weight * Math.sin(2 * Math.PI * phase);\n }\n }\n }\n return Math.min(1, Math.hypot(re, im) / Math.PI);\n}\n\n/** Smallest s in (0, 1) where f(s) ≤ 0.5: a scan from 0 with the given step, then bisection. */\nfunction firstHalfCrossing(f: (s: number) => number, step: number): number {\n let lo = 0;\n for (let s = step; ; s += step) {\n const hi = Math.min(s, 1);\n if (f(hi) <= 0.5) {\n let a = lo, b = hi;\n for (let it = 0; it < 60 && b - a > 1e-13 * b; it++) { const m = (a + b) / 2; if (f(m) <= 0.5) b = m; else a = m; }\n return (a + b) / 2;\n }\n if (hi >= 1) return 1;\n lo = hi;\n }\n}\n\n/** The diffraction-limited MTF50 as a fraction of the cut-off: (2/π)(acos s − s√(1 − s²)) = 0.5, s ≈ 0.40397. */\nfunction diffractionMtf50Fraction(): number {\n let a = 0, b = 1;\n for (let it = 0; it < 60; it++) { const m = (a + b) / 2; if (diffractionMtf(m) <= 0.5) b = m; else a = m; }\n return (a + b) / 2;\n}\n\nfunction sinc(x: number): number {\n return x === 0 ? 1 : Math.abs(Math.sin(Math.PI * x) / (Math.PI * x));\n}\n\n/** Thin-lens image distance in mm of an object at d meters, or f at infinity (d = null). */\nfunction imageDistanceMm(focalLengthMm: number, distanceM: number | null, label: string): number {\n if (distanceM === null) return focalLengthMm;\n const d = distanceM * 1000;\n if (!(d > focalLengthMm)) throw new InvalidScientificInputError(`${label} must be greater than the focal length (${focalLengthMm} mm).`);\n return (focalLengthMm * d) / (d - focalLengthMm);\n}\n\n/**\n * Which limits detail for given settings: the lens (diffraction at this f-number, aberrations, defocus) or the sensor's pixel\n * pitch. Returns the optics' resolution (MTF50 and the diffraction cut-off, cycles/mm), the sampling's (Nyquist frequency\n * and, with a Bayer filter, the red and blue sites'), the system MTF50 (optics × pixel aperture), the f-number from which\n * diffraction limits on this sensor, and which one limits.\n *\n * The sensor limits when the optics still pass 50 % contrast at the Nyquist frequency 1/(2p); otherwise the lens does,\n * through diffraction when its wavefront error is within the Maréchal criterion (RMS ≤ λ/14), else through aberrations or\n * defocus. The optics MTF is the exact scalar OTF of the stated wavefront at one wavelength; the decision rule and the\n * catalogue lenses' wavefronts are approximations.\n *\n * Example: an ideal 50 mm lens at f/22 on a 36 × 24 mm, 6000-pixel-wide sensor (6 µm) is limited by diffraction (optics\n * MTF50 ≈ 33 cycles/mm against a Nyquist frequency of ≈ 83); at f/4 the pixel pitch limits.\n *\n * @param input Sensor, lens, f-number, and optional focus, field position and wavelength.\n * @returns The resolution of the optics and of the sampling, and which limits detail.\n */\nexport function calculateDetailLimit(input: CalculateDetailLimitInput): CalculationResult<DetailLimit> {\n requireInputKeys(input, \"calculateDetailLimit\", [\"sensor\", \"lens\", \"fNumber\", \"focus\", \"fieldPosition\", \"wavelengthNm\"]);\n requireInputObject(input.sensor, \"sensor\");\n requireInputKeys(input.sensor, \"sensor\", [\"widthMm\", \"heightMm\", \"pixelWidth\", \"colorFilterArray\"]);\n requirePositiveFinite(\"sensor.widthMm\", input.sensor.widthMm);\n requirePositiveFinite(\"sensor.heightMm\", input.sensor.heightMm);\n requirePositiveInteger(\"sensor.pixelWidth\", input.sensor.pixelWidth);\n const cfa = input.sensor.colorFilterArray ?? \"bayer\";\n if (cfa !== \"bayer\" && cfa !== \"none\") throw new InvalidConfigurationError(\"sensor.colorFilterArray must be \\\"bayer\\\" or \\\"none\\\".\");\n requirePositiveFinite(\"fNumber\", input.fNumber);\n const wavelengthNm = input.wavelengthNm ?? 550;\n if (typeof wavelengthNm !== \"number\" || !(wavelengthNm >= DETAIL_LIMIT_LIMITS.minimumWavelengthNm && wavelengthNm <= DETAIL_LIMIT_LIMITS.maximumWavelengthNm)) {\n throw new InvalidScientificInputError(\"wavelengthNm must be a number from 380 through 780.\");\n }\n const fieldPosition = input.fieldPosition ?? 0;\n if (typeof fieldPosition !== \"number\" || !(fieldPosition >= 0 && fieldPosition <= 1)) {\n throw new InvalidScientificInputError(\"fieldPosition must be a number from 0 (center) through 1 (corner).\");\n }\n let focusDistanceM: number | null = null, subjectDistanceM: number | null = null;\n if (input.focus !== undefined) {\n requireInputObject(input.focus, \"focus\");\n requireInputKeys(input.focus, \"focus\", [\"focusDistanceM\", \"subjectDistanceM\"]);\n requirePositiveFinite(\"focus.focusDistanceM\", input.focus.focusDistanceM);\n focusDistanceM = input.focus.focusDistanceM;\n if (input.focus.subjectDistanceM !== undefined) requirePositiveFinite(\"focus.subjectDistanceM\", input.focus.subjectDistanceM);\n subjectDistanceM = input.focus.subjectDistanceM ?? focusDistanceM;\n }\n\n const lens = input.lens;\n requireInputObject(lens, \"lens\");\n const wavefront: DetailLimitWavefront = { defocus: 0, spherical: 0, coma: 0, astigmatism: 0, fieldCurvature: 0 };\n let focalLengthMm: number;\n if (lens.kind === \"ideal\") {\n requireInputKeys(lens, \"lens\", [\"kind\", \"focalLengthMm\"]);\n requirePositiveFinite(\"lens.focalLengthMm\", lens.focalLengthMm);\n focalLengthMm = lens.focalLengthMm;\n } else if (lens.kind === \"catalogue\") {\n requireInputKeys(lens, \"lens\", [\"kind\", \"lensId\"]);\n const resolved = resolveGenericLens({ lensId: lens.lensId, activeImagingArea: { widthMm: input.sensor.widthMm, heightMm: input.sensor.heightMm },\n fNumber: input.fNumber, ...(focusDistanceM === null ? {} : { focusDistanceM }) });\n focalLengthMm = resolved.focalLengthMm;\n // Waves at 550 nm (the catalogue's design wavelength) → waves at λ: OPD is fixed, so scale by 550/λ.\n const a = resolved.character.aberrations, H = fieldPosition, scale = 550 / wavelengthNm;\n wavefront.defocus = scale * (a.balancingDefocusWaves + a.axialColorWaves * genericLensDispersion(wavelengthNm));\n wavefront.spherical = scale * a.sphericalWaves;\n wavefront.coma = scale * a.comaWaves * H;\n wavefront.astigmatism = scale * a.astigmatismWaves * H * H;\n wavefront.fieldCurvature = scale * a.fieldCurvatureWaves * H * H;\n } else {\n throw new InvalidConfigurationError(\"lens.kind must be \\\"ideal\\\" or \\\"catalogue\\\".\");\n }\n\n const lambdaMm = wavelengthNm * 1e-6;\n const sensorImageMm = imageDistanceMm(focalLengthMm, focusDistanceM, \"focus.focusDistanceM\");\n const subjectImageMm = imageDistanceMm(focalLengthMm, subjectDistanceM, \"focus.subjectDistanceM\");\n const pupilMm = focalLengthMm / input.fNumber, workingFNumber = sensorImageMm / pupilMm;\n // W020 = dz/(8 N_w²), dz = sensor minus image distance (calculateDefocusWavefront's convention), in waves.\n const defocusDz = sensorImageMm - subjectImageMm, defocusWaves = defocusDz / (8 * workingFNumber * workingFNumber) / lambdaMm;\n const defocusBlurDiameterMm = Math.abs(defocusDz) * pupilMm / subjectImageMm;\n const aberrationRms = rmsWaves(wavefront);\n wavefront.defocus += defocusWaves;\n const totalRms = rmsWaves(wavefront), defocusRms = Math.abs(defocusWaves) / (2 * Math.sqrt(3));\n const magnitude = wavefrontMagnitude(wavefront);\n if (magnitude > DETAIL_LIMIT_LIMITS.maximumWavefrontWaves) {\n throw new InvalidScientificInputError(`The wavefront (${magnitude.toPrecision(3)} waves, defocus blur ${defocusBlurDiameterMm.toPrecision(3)} mm) exceeds ` +\n `${DETAIL_LIMIT_LIMITS.maximumWavefrontWaves} waves: the blur is geometric; use calculateDefocusCircle for it.`);\n }\n\n const pitchMm = input.sensor.widthMm / input.sensor.pixelWidth, cutoff = 1 / (lambdaMm * workingFNumber), nyquist = 1 / (2 * pitchMm);\n const step = Math.min(1 / 128, 1 / (32 * Math.max(1, magnitude)));\n const symmetric = wavefront.coma === 0 && wavefront.astigmatism === 0;\n const opticsAt = (s: number, tangential: boolean): number => detailLimitOpticsMtf(wavefront, s, tangential);\n const tangential50 = firstHalfCrossing((s) => opticsAt(s, true), step) * cutoff;\n const sagittal50 = symmetric ? tangential50 : firstHalfCrossing((s) => opticsAt(s, false), step) * cutoff;\n const systemAt = (s: number, t: boolean): number => opticsAt(s, t) * sinc(s * cutoff * pitchMm);\n const systemTangential50 = firstHalfCrossing((s) => systemAt(s, true), step) * cutoff;\n const system50 = symmetric ? systemTangential50 : Math.min(systemTangential50, firstHalfCrossing((s) => systemAt(s, false), step) * cutoff);\n const sNyquist = nyquist / cutoff;\n const opticsAtNyquist = symmetric ? opticsAt(sNyquist, true) : Math.min(opticsAt(sNyquist, true), opticsAt(sNyquist, false));\n const mtf50 = Math.min(tangential50, sagittal50), fraction50 = diffractionMtf50Fraction();\n\n const limitedBy: DetailLimitedBy = mtf50 >= nyquist ? \"sensor\" : \"lens\";\n const limitingFactor: DetailLimitingFactor = limitedBy === \"sensor\" ? \"pixel-pitch\"\n : totalRms <= MARECHAL_RMS_WAVES ? \"diffraction\" : defocusRms > aberrationRms ? \"defocus\" : \"aberrations\";\n\n return approximationResult(\n {\n limitedBy,\n limitingFactor,\n opticsToNyquistRatio: mtf50 / nyquist,\n fNumber: input.fNumber,\n wavelengthNm,\n focalLengthMm,\n fieldPosition,\n optics: {\n workingFNumber,\n cutoffCyclesPerMm: cutoff,\n mtf50CyclesPerMm: mtf50,\n tangentialMtf50CyclesPerMm: tangential50,\n sagittalMtf50CyclesPerMm: sagittal50,\n diffractionMtf50CyclesPerMm: fraction50 * cutoff,\n mtfAtNyquist: opticsAtNyquist,\n rmsWavefrontErrorWaves: { aberrations: aberrationRms, defocus: defocusRms, total: totalRms },\n withinMarechalCriterion: totalRms <= MARECHAL_RMS_WAVES,\n defocusBlurDiameterMm\n },\n sampling: {\n colorFilterArray: cfa,\n pixelPitchMicrometers: pitchMm * 1000,\n nyquistCyclesPerMm: nyquist,\n redBlueNyquistCyclesPerMm: cfa === \"bayer\" ? nyquist / 2 : null,\n pixelApertureMtfAtNyquist: 2 / Math.PI,\n pixelApertureMtf50CyclesPerMm: 0.6033545923 / pitchMm\n },\n system: {\n mtf50CyclesPerMm: system50,\n mtf50LinePairsPerPictureHeight: system50 * input.sensor.heightMm,\n mtfAtNyquist: opticsAtNyquist * (2 / Math.PI)\n },\n diffractionLimitedFNumber: (2 * pitchMm * fraction50 / lambdaMm) * focalLengthMm / sensorImageMm,\n basis: BASIS\n },\n \"detail-limit\",\n DETAIL_LIMIT_MODEL_VERSION,\n [\n \"Scalar, incoherent, monochromatic imaging at one wavelength (default 550 nm); lateral color excluded.\",\n \"Uniformly lit circular pupil; thin-lens working f-number and defocus.\",\n \"Pixel aperture: ideal square site with fill factor 1; no optical low-pass filter, microlens or crosstalk.\",\n \"A Bayer filter's red and blue sites have half the Nyquist frequency; demosaicing is not modeled.\",\n \"Decision rule: the sensor limits when the optics MTF50 reaches the Nyquist frequency; diffraction when the RMS wavefront error is within λ/14 (Maréchal).\",\n \"Catalogue lenses are generic Photivra designs (labeled approximations), not measured products.\"\n ]\n );\n}\n"]}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lateral colour of a catalogue lens as a fringe width (#580): the red–blue image displacement in micrometres and in pixels at
|
|
3
|
+
* one field position, for the app's Guide page on color fringing.
|
|
4
|
+
*
|
|
5
|
+
* It reads the lens's own lateral-colour model (`docs/LENS_CATALOGUE.md`, "Models"), the one `simulateCapture` renders and
|
|
6
|
+
* `createGenericLensCorrectionProfile` corrects: a radial image displacement Δr(λ) = L·R·H·D(λ), with L the lens's
|
|
7
|
+
* `lateralColor.fraction`, R the sensor's half-diagonal, H the field height (fraction of R) and D(λ) the normalised Cauchy
|
|
8
|
+
* dispersion (0 at the Fraunhofer d line, F minus C = 1). The red and blue channels are taken at the effective wavelengths of
|
|
9
|
+
* the lens corrections, `GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM` (610 and 465 nm), so the blue-minus-red displacement
|
|
10
|
+
* is L·R·H·(D(465) − D(610)). With `channelDispersion` (2.21.0, #589's `calculateChannelEffectiveDispersion` for the camera's
|
|
11
|
+
* channels and the light) it is L·R·H·(D̄_blue − D̄_red), the displacement `simulateCapture` renders in RAW. The lens's lateral
|
|
12
|
+
* colour does not change with the f-number or the focus distance.
|
|
13
|
+
*
|
|
14
|
+
* Every value is an approximation: the lens is a generic Photivra design, not a measured product, and a real channel spans a
|
|
15
|
+
* band of wavelengths, not one.
|
|
16
|
+
*/
|
|
17
|
+
import { type CalculationResult } from "../core/calculation-result.js";
|
|
18
|
+
import { type ChannelDispersion } from "../output/developed-lens-corrections.js";
|
|
19
|
+
export interface CalculateLateralColorFringeInput {
|
|
20
|
+
/** A catalogue id (`GENERIC_LENS_IDS`). */
|
|
21
|
+
lensId: string;
|
|
22
|
+
/** The sensor's active capture area, mm (the lens is scaled to it as in `resolveGenericLens`). */
|
|
23
|
+
activeImagingArea: {
|
|
24
|
+
widthMm: number;
|
|
25
|
+
heightMm: number;
|
|
26
|
+
};
|
|
27
|
+
/** The sensor's pixel pitch, µm. */
|
|
28
|
+
pixelPitchMicrometers: number;
|
|
29
|
+
/**
|
|
30
|
+
* The field position as fractions of the frame's half-width (x, +right) and half-height (y, +up) from the optical axis, each
|
|
31
|
+
* in [−1, 1]. Default `{ x: 1, y: 1 }`: the top-right corner.
|
|
32
|
+
*/
|
|
33
|
+
fieldPosition?: {
|
|
34
|
+
x: number;
|
|
35
|
+
y: number;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* 2.21.0: each channel's effective dispersion (`calculateChannelEffectiveDispersion` for the camera's channels and the light;
|
|
39
|
+
* only `red` and `blue` are read). Default: D at the single wavelengths 610 and 465 nm.
|
|
40
|
+
*/
|
|
41
|
+
channelDispersion?: ChannelDispersion;
|
|
42
|
+
}
|
|
43
|
+
/** A signed displacement of the blue image from the red one: radial (positive: blue farther from the axis) and its x and y parts. */
|
|
44
|
+
export interface LateralColorDisplacement {
|
|
45
|
+
radial: number;
|
|
46
|
+
x: number;
|
|
47
|
+
y: number;
|
|
48
|
+
}
|
|
49
|
+
export interface LateralColorFringe {
|
|
50
|
+
lensId: string;
|
|
51
|
+
/** The image-plane point, mm from the optical axis (+x right, +y up). */
|
|
52
|
+
imagePointMm: {
|
|
53
|
+
x: number;
|
|
54
|
+
y: number;
|
|
55
|
+
};
|
|
56
|
+
/** Field height H: the point's distance from the axis as a fraction of the half-diagonal (1 at the corner). */
|
|
57
|
+
fieldHeight: number;
|
|
58
|
+
/** The channel wavelengths the displacement is taken at, nm; null with a given `channelDispersion`. */
|
|
59
|
+
channelWavelengthsNm: {
|
|
60
|
+
red: number;
|
|
61
|
+
blue: number;
|
|
62
|
+
} | null;
|
|
63
|
+
/** The red and blue dispersions D used (normalized: 0 at the d line, F minus C = 1). */
|
|
64
|
+
channelDispersion: {
|
|
65
|
+
red: number;
|
|
66
|
+
blue: number;
|
|
67
|
+
};
|
|
68
|
+
/** Blue-minus-red image displacement, µm. */
|
|
69
|
+
blueMinusRedMicrometers: LateralColorDisplacement;
|
|
70
|
+
/** The same in pixels of the given pitch. */
|
|
71
|
+
blueMinusRedPixels: LateralColorDisplacement;
|
|
72
|
+
/** The fringe width: |radial| displacement, µm and pixels. */
|
|
73
|
+
fringeWidthMicrometers: number;
|
|
74
|
+
fringeWidthPixels: number;
|
|
75
|
+
/** The lens's own F-to-C (486 to 656 nm) displacement at this point, L·R·H, µm. */
|
|
76
|
+
fToCMicrometers: number;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The red–blue image displacement of a catalogue lens at a field position on a sensor, in micrometres and pixels, from the
|
|
80
|
+
* lens's own lateral-colour model Δr(λ) = L·R·H·D(λ) at the lens corrections' channel wavelengths (610 and 465 nm), or at the
|
|
81
|
+
* given `channelDispersion`. Exact for the model (no fast mode); an approximation of a generic lens, not a measured product.
|
|
82
|
+
*/
|
|
83
|
+
export declare function calculateLateralColorFringe(input: CalculateLateralColorFringeInput): CalculationResult<LateralColorFringe>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Lateral colour of a catalogue lens as a fringe width (#580): the red–blue image displacement in micrometres and in pixels at
|
|
4
|
+
* one field position, for the app's Guide page on color fringing.
|
|
5
|
+
*
|
|
6
|
+
* It reads the lens's own lateral-colour model (`docs/LENS_CATALOGUE.md`, "Models"), the one `simulateCapture` renders and
|
|
7
|
+
* `createGenericLensCorrectionProfile` corrects: a radial image displacement Δr(λ) = L·R·H·D(λ), with L the lens's
|
|
8
|
+
* `lateralColor.fraction`, R the sensor's half-diagonal, H the field height (fraction of R) and D(λ) the normalised Cauchy
|
|
9
|
+
* dispersion (0 at the Fraunhofer d line, F minus C = 1). The red and blue channels are taken at the effective wavelengths of
|
|
10
|
+
* the lens corrections, `GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM` (610 and 465 nm), so the blue-minus-red displacement
|
|
11
|
+
* is L·R·H·(D(465) − D(610)). With `channelDispersion` (2.21.0, #589's `calculateChannelEffectiveDispersion` for the camera's
|
|
12
|
+
* channels and the light) it is L·R·H·(D̄_blue − D̄_red), the displacement `simulateCapture` renders in RAW. The lens's lateral
|
|
13
|
+
* colour does not change with the f-number or the focus distance.
|
|
14
|
+
*
|
|
15
|
+
* Every value is an approximation: the lens is a generic Photivra design, not a measured product, and a real channel spans a
|
|
16
|
+
* band of wavelengths, not one.
|
|
17
|
+
*/
|
|
18
|
+
import { approximationResult } from "../core/calculation-result.js";
|
|
19
|
+
import { InvalidScientificInputError } from "../core/validation.js";
|
|
20
|
+
import { requireInputKeys, requireInputObject } from "../core/record-validation.js";
|
|
21
|
+
import { genericLensDispersion, resolveGenericLens } from "../equipment/generic-lens-catalogue.js";
|
|
22
|
+
import { GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM } from "../output/developed-lens-corrections.js";
|
|
23
|
+
function positiveFinite(value, name) {
|
|
24
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0)
|
|
25
|
+
throw new InvalidScientificInputError(name + " must be a positive finite number.");
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
function fieldCoordinate(value, name) {
|
|
29
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < -1 || value > 1)
|
|
30
|
+
throw new InvalidScientificInputError(name + " must be a finite number in [-1, 1].");
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The red–blue image displacement of a catalogue lens at a field position on a sensor, in micrometres and pixels, from the
|
|
35
|
+
* lens's own lateral-colour model Δr(λ) = L·R·H·D(λ) at the lens corrections' channel wavelengths (610 and 465 nm), or at the
|
|
36
|
+
* given `channelDispersion`. Exact for the model (no fast mode); an approximation of a generic lens, not a measured product.
|
|
37
|
+
*/
|
|
38
|
+
export function calculateLateralColorFringe(input) {
|
|
39
|
+
requireInputObject(input, "calculateLateralColorFringe input");
|
|
40
|
+
requireInputKeys(input, "calculateLateralColorFringe", ["lensId", "activeImagingArea", "pixelPitchMicrometers", "fieldPosition", "channelDispersion"]);
|
|
41
|
+
const lens = resolveGenericLens({ lensId: input.lensId, activeImagingArea: input.activeImagingArea });
|
|
42
|
+
const pitch = positiveFinite(input.pixelPitchMicrometers, "pixelPitchMicrometers");
|
|
43
|
+
let u = 1, v = 1;
|
|
44
|
+
if (input.fieldPosition !== undefined) {
|
|
45
|
+
requireInputObject(input.fieldPosition, "fieldPosition");
|
|
46
|
+
requireInputKeys(input.fieldPosition, "fieldPosition", ["x", "y"]);
|
|
47
|
+
u = fieldCoordinate(input.fieldPosition.x, "fieldPosition.x");
|
|
48
|
+
v = fieldCoordinate(input.fieldPosition.y, "fieldPosition.y");
|
|
49
|
+
}
|
|
50
|
+
const x = u * input.activeImagingArea.widthMm / 2, y = v * input.activeImagingArea.heightMm / 2, r = Math.hypot(x, y);
|
|
51
|
+
const fieldHeight = r / lens.halfDiagonalMm, { red, blue } = GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM, given = input.channelDispersion;
|
|
52
|
+
let dispersion = { red: genericLensDispersion(red), blue: genericLensDispersion(blue) };
|
|
53
|
+
if (given !== undefined) {
|
|
54
|
+
requireInputObject(given, "channelDispersion");
|
|
55
|
+
requireInputKeys(given, "channelDispersion", ["red", "green", "blue"]);
|
|
56
|
+
// D spans −1.08 (1100 nm) to 4.30 (300 nm), as createGenericLensCorrectionProfile checks.
|
|
57
|
+
for (const c of ["red", "blue"])
|
|
58
|
+
if (typeof given[c] !== "number" || !(given[c] >= -1.1 && given[c] <= 4.4))
|
|
59
|
+
throw new InvalidScientificInputError("channelDispersion." + c + " must be a number in [−1.1, 4.4], D between 1100 and 300 nm.");
|
|
60
|
+
dispersion = { red: given.red, blue: given.blue };
|
|
61
|
+
}
|
|
62
|
+
const fToCMicrometers = lens.character.lateralColor.fraction * lens.halfDiagonalMm * 1000 * fieldHeight;
|
|
63
|
+
const radial = fToCMicrometers * (dispersion.blue - dispersion.red);
|
|
64
|
+
const cos = r > 0 ? x / r : 0, sin = r > 0 ? y / r : 0;
|
|
65
|
+
// `+ 0` turns a −0 (from a negative zero cosine or sine) into 0.
|
|
66
|
+
const um = { radial, x: radial * cos + 0, y: radial * sin + 0 };
|
|
67
|
+
return approximationResult({ lensId: lens.lensId, imagePointMm: { x, y }, fieldHeight, channelWavelengthsNm: given ? null : { red, blue }, channelDispersion: dispersion,
|
|
68
|
+
blueMinusRedMicrometers: um, blueMinusRedPixels: { radial: um.radial / pitch, x: um.x / pitch, y: um.y / pitch },
|
|
69
|
+
fringeWidthMicrometers: Math.abs(radial), fringeWidthPixels: Math.abs(radial) / pitch, fToCMicrometers }, "generic-lens-lateral-color-fringe", "1.0.0", [
|
|
70
|
+
"The catalogue lens's lateral-colour model: radial displacement L·R·H·D(λ), D the normalised Cauchy dispersion (Fraunhofer d, F and C lines).",
|
|
71
|
+
given ? "Red and blue at the given channel dispersions (a neutral surface's weighting of each channel's band)." :
|
|
72
|
+
"Red and blue at single effective wavelengths of 610 and 465 nm (GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM); a real channel spans a band.",
|
|
73
|
+
"A generic Photivra lens, not a measured product; lateral colour does not change with the f-number or the focus distance in this model.",
|
|
74
|
+
"Geometric displacement only: the rendered fringe also depends on the lens's blur, the demosaic and the colour conversion."
|
|
75
|
+
]);
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=generic-lens-lateral-color.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generic-lens-lateral-color.js","sourceRoot":"","sources":["../../src/optics/generic-lens-lateral-color.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Lateral colour of a catalogue lens as a fringe width (#580): the red–blue image displacement in micrometres and in pixels at\n * one field position, for the app's Guide page on color fringing.\n *\n * It reads the lens's own lateral-colour model (`docs/LENS_CATALOGUE.md`, \"Models\"), the one `simulateCapture` renders and\n * `createGenericLensCorrectionProfile` corrects: a radial image displacement Δr(λ) = L·R·H·D(λ), with L the lens's\n * `lateralColor.fraction`, R the sensor's half-diagonal, H the field height (fraction of R) and D(λ) the normalised Cauchy\n * dispersion (0 at the Fraunhofer d line, F minus C = 1). The red and blue channels are taken at the effective wavelengths of\n * the lens corrections, `GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM` (610 and 465 nm), so the blue-minus-red displacement\n * is L·R·H·(D(465) − D(610)). With `channelDispersion` (2.21.0, #589's `calculateChannelEffectiveDispersion` for the camera's\n * channels and the light) it is L·R·H·(D̄_blue − D̄_red), the displacement `simulateCapture` renders in RAW. The lens's lateral\n * colour does not change with the f-number or the focus distance.\n *\n * Every value is an approximation: the lens is a generic Photivra design, not a measured product, and a real channel spans a\n * band of wavelengths, not one.\n */\n\nimport { approximationResult, type CalculationResult } from \"../core/calculation-result.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport { requireInputKeys, requireInputObject } from \"../core/record-validation.js\";\nimport { genericLensDispersion, resolveGenericLens } from \"../equipment/generic-lens-catalogue.js\";\nimport { GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM, type ChannelDispersion } from \"../output/developed-lens-corrections.js\";\n\nexport interface CalculateLateralColorFringeInput {\n /** A catalogue id (`GENERIC_LENS_IDS`). */\n lensId: string;\n /** The sensor's active capture area, mm (the lens is scaled to it as in `resolveGenericLens`). */\n activeImagingArea: { widthMm: number; heightMm: number };\n /** The sensor's pixel pitch, µm. */\n pixelPitchMicrometers: number;\n /**\n * The field position as fractions of the frame's half-width (x, +right) and half-height (y, +up) from the optical axis, each\n * in [−1, 1]. Default `{ x: 1, y: 1 }`: the top-right corner.\n */\n fieldPosition?: { x: number; y: number };\n /**\n * 2.21.0: each channel's effective dispersion (`calculateChannelEffectiveDispersion` for the camera's channels and the light;\n * only `red` and `blue` are read). Default: D at the single wavelengths 610 and 465 nm.\n */\n channelDispersion?: ChannelDispersion;\n}\n\n/** A signed displacement of the blue image from the red one: radial (positive: blue farther from the axis) and its x and y parts. */\nexport interface LateralColorDisplacement {\n radial: number;\n x: number;\n y: number;\n}\n\nexport interface LateralColorFringe {\n lensId: string;\n /** The image-plane point, mm from the optical axis (+x right, +y up). */\n imagePointMm: { x: number; y: number };\n /** Field height H: the point's distance from the axis as a fraction of the half-diagonal (1 at the corner). */\n fieldHeight: number;\n /** The channel wavelengths the displacement is taken at, nm; null with a given `channelDispersion`. */\n channelWavelengthsNm: { red: number; blue: number } | null;\n /** The red and blue dispersions D used (normalized: 0 at the d line, F minus C = 1). */\n channelDispersion: { red: number; blue: number };\n /** Blue-minus-red image displacement, µm. */\n blueMinusRedMicrometers: LateralColorDisplacement;\n /** The same in pixels of the given pitch. */\n blueMinusRedPixels: LateralColorDisplacement;\n /** The fringe width: |radial| displacement, µm and pixels. */\n fringeWidthMicrometers: number;\n fringeWidthPixels: number;\n /** The lens's own F-to-C (486 to 656 nm) displacement at this point, L·R·H, µm. */\n fToCMicrometers: number;\n}\n\nfunction positiveFinite(value: unknown, name: string): number {\n if (typeof value !== \"number\" || !Number.isFinite(value) || value <= 0) throw new InvalidScientificInputError(name + \" must be a positive finite number.\");\n return value;\n}\nfunction fieldCoordinate(value: unknown, name: string): number {\n if (typeof value !== \"number\" || !Number.isFinite(value) || value < -1 || value > 1) throw new InvalidScientificInputError(name + \" must be a finite number in [-1, 1].\");\n return value;\n}\n\n/**\n * The red–blue image displacement of a catalogue lens at a field position on a sensor, in micrometres and pixels, from the\n * lens's own lateral-colour model Δr(λ) = L·R·H·D(λ) at the lens corrections' channel wavelengths (610 and 465 nm), or at the\n * given `channelDispersion`. Exact for the model (no fast mode); an approximation of a generic lens, not a measured product.\n */\nexport function calculateLateralColorFringe(input: CalculateLateralColorFringeInput): CalculationResult<LateralColorFringe> {\n requireInputObject(input, \"calculateLateralColorFringe input\");\n requireInputKeys(input, \"calculateLateralColorFringe\", [\"lensId\", \"activeImagingArea\", \"pixelPitchMicrometers\", \"fieldPosition\", \"channelDispersion\"]);\n const lens = resolveGenericLens({ lensId: input.lensId, activeImagingArea: input.activeImagingArea });\n const pitch = positiveFinite(input.pixelPitchMicrometers, \"pixelPitchMicrometers\");\n let u = 1, v = 1;\n if (input.fieldPosition !== undefined) {\n requireInputObject(input.fieldPosition, \"fieldPosition\");\n requireInputKeys(input.fieldPosition, \"fieldPosition\", [\"x\", \"y\"]);\n u = fieldCoordinate(input.fieldPosition.x, \"fieldPosition.x\"); v = fieldCoordinate(input.fieldPosition.y, \"fieldPosition.y\");\n }\n const x = u * input.activeImagingArea.widthMm / 2, y = v * input.activeImagingArea.heightMm / 2, r = Math.hypot(x, y);\n const fieldHeight = r / lens.halfDiagonalMm, { red, blue } = GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM, given = input.channelDispersion;\n let dispersion = { red: genericLensDispersion(red), blue: genericLensDispersion(blue) };\n if (given !== undefined) {\n requireInputObject(given, \"channelDispersion\");\n requireInputKeys(given, \"channelDispersion\", [\"red\", \"green\", \"blue\"]);\n // D spans −1.08 (1100 nm) to 4.30 (300 nm), as createGenericLensCorrectionProfile checks.\n for (const c of [\"red\", \"blue\"] as const) if (typeof given[c] !== \"number\" || !(given[c] >= -1.1 && given[c] <= 4.4))\n throw new InvalidScientificInputError(\"channelDispersion.\" + c + \" must be a number in [−1.1, 4.4], D between 1100 and 300 nm.\");\n dispersion = { red: given.red, blue: given.blue };\n }\n const fToCMicrometers = lens.character.lateralColor.fraction * lens.halfDiagonalMm * 1000 * fieldHeight;\n const radial = fToCMicrometers * (dispersion.blue - dispersion.red);\n const cos = r > 0 ? x / r : 0, sin = r > 0 ? y / r : 0;\n // `+ 0` turns a −0 (from a negative zero cosine or sine) into 0.\n const um: LateralColorDisplacement = { radial, x: radial * cos + 0, y: radial * sin + 0 };\n return approximationResult({ lensId: lens.lensId, imagePointMm: { x, y }, fieldHeight, channelWavelengthsNm: given ? null : { red, blue }, channelDispersion: dispersion,\n blueMinusRedMicrometers: um, blueMinusRedPixels: { radial: um.radial / pitch, x: um.x / pitch, y: um.y / pitch },\n fringeWidthMicrometers: Math.abs(radial), fringeWidthPixels: Math.abs(radial) / pitch, fToCMicrometers },\n \"generic-lens-lateral-color-fringe\", \"1.0.0\", [\n \"The catalogue lens's lateral-colour model: radial displacement L·R·H·D(λ), D the normalised Cauchy dispersion (Fraunhofer d, F and C lines).\",\n given ? \"Red and blue at the given channel dispersions (a neutral surface's weighting of each channel's band).\" :\n \"Red and blue at single effective wavelengths of 610 and 465 nm (GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM); a real channel spans a band.\",\n \"A generic Photivra lens, not a measured product; lateral colour does not change with the f-number or the focus distance in this model.\",\n \"Geometric displacement only: the rendered fringe also depends on the lens's blur, the demosaic and the colour conversion.\"]);\n}\n"]}
|