@photivra/engine 2.18.0 → 2.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (335) hide show
  1. package/CHANGELOG.md +179 -1
  2. package/CITATION.cff +1 -1
  3. package/NODE_SUPPORT.md +7 -5
  4. package/README.md +6 -6
  5. package/SECURITY.md +1 -1
  6. package/THIRD_PARTY.md +14 -10
  7. package/dist/capture/dng-lens-correction-opcodes.d.ts +32 -0
  8. package/dist/capture/dng-lens-correction-opcodes.js +119 -0
  9. package/dist/capture/dng-lens-correction-opcodes.js.map +1 -0
  10. package/dist/capture/focus-control.js +2 -75
  11. package/dist/capture/focus-control.js.map +1 -1
  12. package/dist/capture/native-environment-lean.d.ts +1 -0
  13. package/dist/capture/native-environment-lean.js +2 -6
  14. package/dist/capture/native-environment-lean.js.map +1 -1
  15. package/dist/capture/native-environment-optical-stage.d.ts +1 -0
  16. package/dist/capture/native-environment-optical-stage.js +2 -5
  17. package/dist/capture/native-environment-optical-stage.js.map +1 -1
  18. package/dist/capture/native-environment-raw.js +1 -5
  19. package/dist/capture/native-environment-raw.js.map +1 -1
  20. package/dist/capture/native-raw-dng.d.ts +6 -0
  21. package/dist/capture/native-raw-dng.js +16 -2
  22. package/dist/capture/native-raw-dng.js.map +1 -1
  23. package/dist/capture/release-sequence.js +2 -47
  24. package/dist/capture/release-sequence.js.map +1 -1
  25. package/dist/capture/sensor-raw-producer.js +3 -5
  26. package/dist/capture/sensor-raw-producer.js.map +1 -1
  27. package/dist/color/white-balance.d.ts +2 -0
  28. package/dist/color/white-balance.js +3 -56
  29. package/dist/color/white-balance.js.map +1 -1
  30. package/dist/composition/image-formation-plan.d.ts +1 -0
  31. package/dist/composition/image-formation-plan.js +3 -49
  32. package/dist/composition/image-formation-plan.js.map +1 -1
  33. package/dist/core/evidence-provenance.js +1 -16
  34. package/dist/core/evidence-provenance.js.map +1 -1
  35. package/dist/core/record-validation.d.ts +54 -0
  36. package/dist/core/record-validation.js +176 -0
  37. package/dist/core/record-validation.js.map +1 -1
  38. package/dist/core/scientific-assurance.js +1 -18
  39. package/dist/core/scientific-assurance.js.map +1 -1
  40. package/dist/core/shared-helpers.d.ts +35 -0
  41. package/dist/core/shared-helpers.js +38 -0
  42. package/dist/core/shared-helpers.js.map +1 -0
  43. package/dist/core/spectral-composition.js +1 -36
  44. package/dist/core/spectral-composition.js.map +1 -1
  45. package/dist/core/spectral.js +1 -19
  46. package/dist/core/spectral.js.map +1 -1
  47. package/dist/core/version.d.ts +1 -1
  48. package/dist/core/version.js +1 -1
  49. package/dist/core/version.js.map +1 -1
  50. package/dist/equipment/exposure-capabilities.d.ts +1 -0
  51. package/dist/equipment/exposure-capabilities.js +2 -36
  52. package/dist/equipment/exposure-capabilities.js.map +1 -1
  53. package/dist/equipment/generic-lens-catalogue.d.ts +8 -2
  54. package/dist/equipment/generic-lens-catalogue.js +31 -2
  55. package/dist/equipment/generic-lens-catalogue.js.map +1 -1
  56. package/dist/equipment/generic-tier-assets.js +2 -3
  57. package/dist/equipment/generic-tier-assets.js.map +1 -1
  58. package/dist/equipment/generic-tier-presets.js +2 -3
  59. package/dist/equipment/generic-tier-presets.js.map +1 -1
  60. package/dist/equipment/iso-capabilities.js +2 -49
  61. package/dist/equipment/iso-capabilities.js.map +1 -1
  62. package/dist/equipment/metering-capabilities.js +1 -26
  63. package/dist/equipment/metering-capabilities.js.map +1 -1
  64. package/dist/equipment/release-capabilities.js +1 -53
  65. package/dist/equipment/release-capabilities.js.map +1 -1
  66. package/dist/exposure/auto-iso-slowest-shutter.js +1 -7
  67. package/dist/exposure/auto-iso-slowest-shutter.js.map +1 -1
  68. package/dist/exposure/exposure-duration-control.js +1 -16
  69. package/dist/exposure/exposure-duration-control.js.map +1 -1
  70. package/dist/exposure/exposure-mode-resolver.js +1 -17
  71. package/dist/exposure/exposure-mode-resolver.js.map +1 -1
  72. package/dist/exposure/filtered-exposure-time.d.ts +93 -0
  73. package/dist/exposure/filtered-exposure-time.js +129 -0
  74. package/dist/exposure/filtered-exposure-time.js.map +1 -0
  75. package/dist/exposure/flash-ttl.d.ts +1 -0
  76. package/dist/exposure/flash-ttl.js +1 -1
  77. package/dist/exposure/flash-ttl.js.map +1 -1
  78. package/dist/exposure/flash.js +1 -49
  79. package/dist/exposure/flash.js.map +1 -1
  80. package/dist/exposure/metering-scene-radiance.js +1 -25
  81. package/dist/exposure/metering-scene-radiance.js.map +1 -1
  82. package/dist/exposure/metering-target.js +1 -30
  83. package/dist/exposure/metering-target.js.map +1 -1
  84. package/dist/exposure/metering-temporal.js +1 -8
  85. package/dist/exposure/metering-temporal.js.map +1 -1
  86. package/dist/exposure/metering.js +1 -72
  87. package/dist/exposure/metering.js.map +1 -1
  88. package/dist/exposure/program-line.js +1 -45
  89. package/dist/exposure/program-line.js.map +1 -1
  90. package/dist/facade/develop-joined-parts.d.ts +29 -0
  91. package/dist/facade/develop-joined-parts.js +33 -0
  92. package/dist/facade/develop-joined-parts.js.map +1 -0
  93. package/dist/facade/display-view-gpu.d.ts +7 -4
  94. package/dist/facade/display-view-gpu.js +4 -4
  95. package/dist/facade/display-view-gpu.js.map +1 -1
  96. package/dist/facade/display-view-lens-signal.d.ts +1 -1
  97. package/dist/facade/display-view-lens-signal.js +5 -3
  98. package/dist/facade/display-view-lens-signal.js.map +1 -1
  99. package/dist/facade/display-view.d.ts +3 -1
  100. package/dist/facade/display-view.js +1 -1
  101. package/dist/facade/display-view.js.map +1 -1
  102. package/dist/facade/fast-methods.d.ts +50 -0
  103. package/dist/facade/fast-methods.js +50 -0
  104. package/dist/facade/fast-methods.js.map +1 -0
  105. package/dist/facade/fast-resolution.d.ts +12 -0
  106. package/dist/facade/fast-resolution.js +18 -0
  107. package/dist/facade/fast-resolution.js.map +1 -0
  108. package/dist/facade/generic-lens-capture.d.ts +6 -3
  109. package/dist/facade/generic-lens-capture.js +6 -3
  110. package/dist/facade/generic-lens-capture.js.map +1 -1
  111. package/dist/facade/simulate-capture.d.ts +88 -16
  112. package/dist/facade/simulate-capture.js +279 -153
  113. package/dist/facade/simulate-capture.js.map +1 -1
  114. package/dist/facade/simulate-display-view.d.ts +3 -2
  115. package/dist/facade/simulate-display-view.js +22 -12
  116. package/dist/facade/simulate-display-view.js.map +1 -1
  117. package/dist/gpu/chart-composite-gpu.js +1 -3
  118. package/dist/gpu/chart-composite-gpu.js.map +1 -1
  119. package/dist/gpu/chart-radiance-gpu.js +1 -5
  120. package/dist/gpu/chart-radiance-gpu.js.map +1 -1
  121. package/dist/gpu/display-view-quality-gpu.d.ts +13 -1
  122. package/dist/gpu/display-view-quality-gpu.js +4 -8
  123. package/dist/gpu/display-view-quality-gpu.js.map +1 -1
  124. package/dist/gpu/display-view-speed-gpu.js +3 -25
  125. package/dist/gpu/display-view-speed-gpu.js.map +1 -1
  126. package/dist/gpu/sensor-stage-gpu.js +1 -5
  127. package/dist/gpu/sensor-stage-gpu.js.map +1 -1
  128. package/dist/gpu/webgpu-runtime.d.ts +2 -0
  129. package/dist/gpu/webgpu-runtime.js +3 -0
  130. package/dist/gpu/webgpu-runtime.js.map +1 -1
  131. package/dist/index.d.ts +11 -1
  132. package/dist/index.js +8 -0
  133. package/dist/index.js.map +1 -1
  134. package/dist/motion/camera-rotation.js +1 -6
  135. package/dist/motion/camera-rotation.js.map +1 -1
  136. package/dist/motion/capture-rotation-inverse-mapping.d.ts +12 -0
  137. package/dist/motion/capture-rotation-inverse-mapping.js +2 -2
  138. package/dist/motion/capture-rotation-inverse-mapping.js.map +1 -1
  139. package/dist/motion/capture-rotation-temporal-quadrature.d.ts +1 -0
  140. package/dist/motion/capture-rotation-temporal-quadrature.js +1 -1
  141. package/dist/motion/capture-rotation-temporal-quadrature.js.map +1 -1
  142. package/dist/motion/capture-rotation-trajectory.js +2 -17
  143. package/dist/motion/capture-rotation-trajectory.js.map +1 -1
  144. package/dist/motion/capture-translation-parallax-temporal-quadrature.d.ts +1 -0
  145. package/dist/motion/capture-translation-parallax-temporal-quadrature.js +3 -14
  146. package/dist/motion/capture-translation-parallax-temporal-quadrature.js.map +1 -1
  147. package/dist/motion/extended-object-projection.js +5 -40
  148. package/dist/motion/extended-object-projection.js.map +1 -1
  149. package/dist/optics/aperture-diameter.d.ts +47 -0
  150. package/dist/optics/aperture-diameter.js +50 -0
  151. package/dist/optics/aperture-diameter.js.map +1 -0
  152. package/dist/optics/atmospheric-haze.js +1 -7
  153. package/dist/optics/atmospheric-haze.js.map +1 -1
  154. package/dist/optics/brown-conrady-distortion.js +1 -7
  155. package/dist/optics/brown-conrady-distortion.js.map +1 -1
  156. package/dist/optics/complex-pupil-psf.d.ts +1 -0
  157. package/dist/optics/complex-pupil-psf.js +2 -84
  158. package/dist/optics/complex-pupil-psf.js.map +1 -1
  159. package/dist/optics/detail-limit.d.ts +182 -0
  160. package/dist/optics/detail-limit.js +336 -0
  161. package/dist/optics/detail-limit.js.map +1 -0
  162. package/dist/optics/focus-state.js +1 -11
  163. package/dist/optics/focus-state.js.map +1 -1
  164. package/dist/optics/front-of-lens-filter.d.ts +5 -0
  165. package/dist/optics/front-of-lens-filter.js +3 -65
  166. package/dist/optics/front-of-lens-filter.js.map +1 -1
  167. package/dist/optics/generic-lens-psf.d.ts +18 -2
  168. package/dist/optics/generic-lens-psf.js +25 -2
  169. package/dist/optics/generic-lens-psf.js.map +1 -1
  170. package/dist/optics/generic-nd-filter-presets.d.ts +12 -0
  171. package/dist/optics/generic-nd-filter-presets.js +48 -0
  172. package/dist/optics/generic-nd-filter-presets.js.map +1 -0
  173. package/dist/optics/illumination-vignetting.js +1 -6
  174. package/dist/optics/illumination-vignetting.js.map +1 -1
  175. package/dist/optics/lens-psf-profile.d.ts +4 -1
  176. package/dist/optics/lens-psf-profile.js +12 -111
  177. package/dist/optics/lens-psf-profile.js.map +1 -1
  178. package/dist/optics/longitudinal-chromatic-aberration.js +1 -7
  179. package/dist/optics/longitudinal-chromatic-aberration.js.map +1 -1
  180. package/dist/optics/pupil-vignetting.js +1 -7
  181. package/dist/optics/pupil-vignetting.js.map +1 -1
  182. package/dist/optics/radial-distortion.js +1 -6
  183. package/dist/optics/radial-distortion.js.map +1 -1
  184. package/dist/optics/scene-to-sensor-irradiance.d.ts +13 -0
  185. package/dist/optics/scene-to-sensor-irradiance.js +11 -96
  186. package/dist/optics/scene-to-sensor-irradiance.js.map +1 -1
  187. package/dist/optics/stray-light.d.ts +1 -0
  188. package/dist/optics/stray-light.js +1 -1
  189. package/dist/optics/stray-light.js.map +1 -1
  190. package/dist/optics/zernike-wavefront.js +1 -7
  191. package/dist/optics/zernike-wavefront.js.map +1 -1
  192. package/dist/output/capture-geometry.js +1 -8
  193. package/dist/output/capture-geometry.js.map +1 -1
  194. package/dist/output/crop-to-match-focal-length.d.ts +62 -0
  195. package/dist/output/crop-to-match-focal-length.js +63 -0
  196. package/dist/output/crop-to-match-focal-length.js.map +1 -0
  197. package/dist/output/developed-lens-corrections.d.ts +62 -0
  198. package/dist/output/developed-lens-corrections.js +135 -0
  199. package/dist/output/developed-lens-corrections.js.map +1 -0
  200. package/dist/output/generic-lens-corrections.d.ts +5 -7
  201. package/dist/output/generic-lens-corrections.js +11 -10
  202. package/dist/output/generic-lens-corrections.js.map +1 -1
  203. package/dist/output/geometric-transforms.js +1 -4
  204. package/dist/output/geometric-transforms.js.map +1 -1
  205. package/dist/output/lens-corrections.d.ts +12 -2
  206. package/dist/output/lens-corrections.js +22 -7
  207. package/dist/output/lens-corrections.js.map +1 -1
  208. package/dist/output/native-capture-sdr.d.ts +2 -2
  209. package/dist/output/native-capture-sdr.js +3 -2
  210. package/dist/output/native-capture-sdr.js.map +1 -1
  211. package/dist/output/print-contrast-reference.js +1 -1
  212. package/dist/output/print-contrast-reference.js.map +1 -1
  213. package/dist/output/print-size-blur-noise.d.ts +110 -0
  214. package/dist/output/print-size-blur-noise.js +138 -0
  215. package/dist/output/print-size-blur-noise.js.map +1 -0
  216. package/dist/processing/development-detail.d.ts +29 -0
  217. package/dist/processing/development-detail.js +128 -107
  218. package/dist/processing/development-detail.js.map +1 -1
  219. package/dist/schema/illumination-temporal.js +3 -72
  220. package/dist/schema/illumination-temporal.js.map +1 -1
  221. package/dist/schema/illumination.d.ts +1 -0
  222. package/dist/schema/illumination.js +2 -65
  223. package/dist/schema/illumination.js.map +1 -1
  224. package/dist/schema/scene-radiance-temporal.js +2 -14
  225. package/dist/schema/scene-radiance-temporal.js.map +1 -1
  226. package/dist/schema/scene-radiance.js +2 -60
  227. package/dist/schema/scene-radiance.js.map +1 -1
  228. package/dist/schema/validation.d.ts +2 -0
  229. package/dist/schema/validation.js +2 -17
  230. package/dist/schema/validation.js.map +1 -1
  231. package/dist/sensor/accumulated-charge.js +2 -59
  232. package/dist/sensor/accumulated-charge.js.map +1 -1
  233. package/dist/sensor/architecture.js +1 -16
  234. package/dist/sensor/architecture.js.map +1 -1
  235. package/dist/sensor/camera-saturation-capacity.d.ts +1 -0
  236. package/dist/sensor/camera-saturation-capacity.js +3 -55
  237. package/dist/sensor/camera-saturation-capacity.js.map +1 -1
  238. package/dist/sensor/capture-color-sampling-binding.js +1 -26
  239. package/dist/sensor/capture-color-sampling-binding.js.map +1 -1
  240. package/dist/sensor/capture-mode-timing.js +1 -39
  241. package/dist/sensor/capture-mode-timing.js.map +1 -1
  242. package/dist/sensor/capture-mode.js +1 -23
  243. package/dist/sensor/capture-mode.js.map +1 -1
  244. package/dist/sensor/color-sampling.js +1 -26
  245. package/dist/sensor/color-sampling.js.map +1 -1
  246. package/dist/sensor/constant-rate-temporal-integration.js +2 -46
  247. package/dist/sensor/constant-rate-temporal-integration.js.map +1 -1
  248. package/dist/sensor/dark-current.js +2 -45
  249. package/dist/sensor/dark-current.js.map +1 -1
  250. package/dist/sensor/eqe-electron-rate.js +2 -41
  251. package/dist/sensor/eqe-electron-rate.js.map +1 -1
  252. package/dist/sensor/exposure-window.d.ts +7 -0
  253. package/dist/sensor/exposure-window.js +9 -24
  254. package/dist/sensor/exposure-window.js.map +1 -1
  255. package/dist/sensor/iso-gain.js +1 -7
  256. package/dist/sensor/iso-gain.js.map +1 -1
  257. package/dist/sensor/iso-signal-chain.js +1 -43
  258. package/dist/sensor/iso-signal-chain.js.map +1 -1
  259. package/dist/sensor/optical-stack.js +1 -25
  260. package/dist/sensor/optical-stack.js.map +1 -1
  261. package/dist/sensor/photon-energy.js +1 -26
  262. package/dist/sensor/photon-energy.js.map +1 -1
  263. package/dist/sensor/physical-charge-capacity.js +3 -77
  264. package/dist/sensor/physical-charge-capacity.js.map +1 -1
  265. package/dist/sensor/radiometry-readiness.js +2 -23
  266. package/dist/sensor/radiometry-readiness.js.map +1 -1
  267. package/dist/sensor/raw-readout.d.ts +3 -0
  268. package/dist/sensor/raw-readout.js +4 -66
  269. package/dist/sensor/raw-readout.js.map +1 -1
  270. package/dist/sensor/raw-reconstruction.js +2 -64
  271. package/dist/sensor/raw-reconstruction.js.map +1 -1
  272. package/dist/sensor/readout-exposure-linkage.js +1 -9
  273. package/dist/sensor/readout-exposure-linkage.js.map +1 -1
  274. package/dist/sensor/readout-timing.js +4 -79
  275. package/dist/sensor/readout-timing.js.map +1 -1
  276. package/dist/sensor/response-application-compatibility.d.ts +2 -0
  277. package/dist/sensor/response-application-compatibility.js +4 -35
  278. package/dist/sensor/response-application-compatibility.js.map +1 -1
  279. package/dist/sensor/response-operating-range.d.ts +1 -0
  280. package/dist/sensor/response-operating-range.js +3 -79
  281. package/dist/sensor/response-operating-range.js.map +1 -1
  282. package/dist/sensor/responsivity-photocurrent.js +4 -114
  283. package/dist/sensor/responsivity-photocurrent.js.map +1 -1
  284. package/dist/sensor/sampling-aperture.js +1 -25
  285. package/dist/sensor/sampling-aperture.js.map +1 -1
  286. package/dist/sensor/sensor-characteristics.d.ts +137 -0
  287. package/dist/sensor/sensor-characteristics.js +115 -0
  288. package/dist/sensor/sensor-characteristics.js.map +1 -0
  289. package/dist/sensor/spatial-sample-reduction.js +3 -24
  290. package/dist/sensor/spatial-sample-reduction.js.map +1 -1
  291. package/dist/sensor/spatial-sampling-quadrature.js +1 -3
  292. package/dist/sensor/spatial-sampling-quadrature.js.map +1 -1
  293. package/dist/sensor/spatio-spectral-reduction.js +1 -18
  294. package/dist/sensor/spatio-spectral-reduction.js.map +1 -1
  295. package/dist/sensor/spectral-coverage.d.ts +10 -1
  296. package/dist/sensor/spectral-coverage.js +1 -1
  297. package/dist/sensor/spectral-coverage.js.map +1 -1
  298. package/dist/sensor/spectral-quadrature.js +3 -41
  299. package/dist/sensor/spectral-quadrature.js.map +1 -1
  300. package/dist/sensor/spectral-response.js +1 -47
  301. package/dist/sensor/spectral-response.js.map +1 -1
  302. package/dist/sensor/temporal-photo-signal.d.ts +3 -0
  303. package/dist/sensor/temporal-photo-signal.js +5 -2
  304. package/dist/sensor/temporal-photo-signal.js.map +1 -1
  305. package/dist/stabilization/camera-shake.js +1 -6
  306. package/dist/stabilization/camera-shake.js.map +1 -1
  307. package/dist/stabilization/system.d.ts +9 -0
  308. package/dist/stabilization/system.js +7 -86
  309. package/dist/stabilization/system.js.map +1 -1
  310. package/docs/AGENT_GUIDE.md +5 -0
  311. package/docs/API_REFERENCE.md +116 -3
  312. package/docs/QUICK_START.md +178 -14
  313. package/docs/USAGE.md +111 -3
  314. package/docs/api/capture-native-raw-dng.md +6 -0
  315. package/docs/api/core-version.md +1 -1
  316. package/docs/api/equipment-generic-lens-catalogue.md +7 -2
  317. package/docs/api/exports.json +203 -3
  318. package/docs/api/exposure-filtered-exposure-time.md +105 -0
  319. package/docs/api/facade-develop-joined-parts.md +32 -0
  320. package/docs/api/facade-display-view-gpu.md +5 -2
  321. package/docs/api/facade-fast-methods.md +91 -0
  322. package/docs/api/facade-simulate-capture.md +72 -16
  323. package/docs/api/facade-simulate-display-view.md +3 -2
  324. package/docs/api/optics-aperture-diameter.md +51 -0
  325. package/docs/api/optics-detail-limit.md +203 -0
  326. package/docs/api/optics-generic-nd-filter-presets.md +34 -0
  327. package/docs/api/optics-lens-psf-profile.md +3 -1
  328. package/docs/api/output-crop-to-match-focal-length.md +67 -0
  329. package/docs/api/output-developed-lens-corrections.md +12 -0
  330. package/docs/api/output-generic-lens-corrections.md +3 -10
  331. package/docs/api/output-lens-corrections.md +11 -1
  332. package/docs/api/output-print-size-blur-noise.md +115 -0
  333. package/docs/api/sensor-sensor-characteristics.md +105 -0
  334. package/llms.txt +5 -0
  335. package/package.json +1 -1
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Which limits detail for given settings: the lens (diffraction at this f-number, aberrations, defocus) or the sensor's pixel
3
+ * pitch (#575, for the app's Learn section).
4
+ *
5
+ * Model (`detail-limit` 1.0.0; the decision rule is an approximation, see below):
6
+ * - **Optics.** The incoherent optical transfer function of a uniformly lit circular pupil carrying the wavefront W (in waves)
7
+ * is the normalized autocorrelation of the pupil function P = exp(i·2π·W):
8
+ * 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
9
+ * working f-number (thin-lens image distance / pupil diameter). With W = 0 it is the diffraction-limited
10
+ * (2/π)(acos s − s√(1 − s²)) (MIT OpenCourseWare 2.71 Optics, Spring 2009, lectures 21–22, free; also see Goodman,
11
+ * Introduction to Fourier Optics, 4th ed., §7.3–7.4). The integral is evaluated by Gauss–Legendre quadrature on a smooth
12
+ * parametrisation of the overlap (√ edge removed), with node counts that grow with the phase variation; MTF = |OTF|.
13
+ * - **Wavefront.** `lens.ideal`: none. `lens.catalogue`: the lens's resolved Seidel terms (`resolveGenericLens`, the same
14
+ * expansion as its PSF: Hopkins 1950; Wyant & Creath 1992, Applied Optics and Optical Engineering XI ch. 1, free author copy)
15
+ * at the field height H, with axial color at the wavelength. Defocus of a subject away from the focus distance adds
16
+ * W020 = dz/(8·N_w²), dz = sensor minus image distance (thin lens; `calculateDefocusWavefront`, Wyant & Creath eq. 18).
17
+ * - **Sampling.** Nyquist frequency 1/(2p) for pixel pitch p (exact for the lattice); a Bayer filter's red and blue sites
18
+ * repeat every 2p, so their Nyquist frequency is 1/(4p), while its green sites still sample rows and columns up to 1/(2p).
19
+ * Pixel aperture MTF |sinc(ν·p)| for a square, fully sensitive site (fill factor 1, an idealisation).
20
+ * - **Decision (approximation).** The sensor limits when the optics still pass at least 50 % contrast at the Nyquist
21
+ * frequency (optics MTF50 ≥ 1/(2p)): a denser sensor would record finer detail. Otherwise the lens limits, and the cause is
22
+ * diffraction when the wavefront error is within the Maréchal criterion (RMS ≤ λ/14, Strehl ≈ 0.8; Wyant & Creath 1992
23
+ * eq. 67), else aberrations or defocus, whichever has the larger RMS wavefront error.
24
+ * @see docs/PHYSICS_FOUNDATION.md "Which limits detail: the lens or the sensor" for the equations, exactness and limits.
25
+ */
26
+ import { type CalculationResult } from "../core/calculation-result.js";
27
+ /**
28
+ * Limits of `calculateDetailLimit`: the wavelength range and the largest wavefront (sum of the magnitudes of its terms, in
29
+ * waves) whose transfer function is integrated. Beyond it the blur is geometric and the quadrature would grow without
30
+ * bound; use `calculateDefocusCircle` for such a blur.
31
+ */
32
+ export declare const DETAIL_LIMIT_LIMITS: Readonly<{
33
+ minimumWavelengthNm: 380;
34
+ maximumWavelengthNm: 780;
35
+ maximumWavefrontWaves: 64;
36
+ }>;
37
+ /** A lens with no aberrations (diffraction only). */
38
+ export interface DetailLimitIdealLens {
39
+ kind: "ideal";
40
+ /** Physical focal length in millimeters (for the working f-number and the defocus of a subject off the focus distance). */
41
+ focalLengthMm: number;
42
+ }
43
+ /** A generic catalogue lens (`GENERIC_LENS_IDS`), resolved for the sensor as `resolveGenericLens` does. */
44
+ export interface DetailLimitCatalogueLens {
45
+ kind: "catalogue";
46
+ lensId: string;
47
+ }
48
+ export interface CalculateDetailLimitInput {
49
+ /** Active imaging area and horizontal pixel count; pitch = widthMm / pixelWidth. */
50
+ sensor: {
51
+ widthMm: number;
52
+ heightMm: number;
53
+ pixelWidth: number;
54
+ /** Default `"bayer"`. `"none"`: every site records full detail (a monochrome sensor). */
55
+ colorFilterArray?: "bayer" | "none";
56
+ };
57
+ lens: DetailLimitIdealLens | DetailLimitCatalogueLens;
58
+ /** f-number set on the lens. */
59
+ fNumber: number;
60
+ /**
61
+ * Focus and subject distances from the lens in meters. Omitted: focused at infinity, subject in focus. `subjectDistanceM`
62
+ * omitted: the subject is at the focus distance.
63
+ */
64
+ focus?: {
65
+ focusDistanceM: number;
66
+ subjectDistanceM?: number;
67
+ };
68
+ /** Field height as a fraction of the half-diagonal, 0 (center, default) to 1 (corner). Only aberrations depend on it. */
69
+ fieldPosition?: number;
70
+ /** Wavelength in nanometres, 380–780. Default 550. */
71
+ wavelengthNm?: number;
72
+ }
73
+ /** Which component limits detail. */
74
+ export type DetailLimitedBy = "lens" | "sensor";
75
+ /** What limits it: the lens's diffraction, aberrations or defocus, or the sensor's pixel pitch. */
76
+ export type DetailLimitingFactor = "diffraction" | "aberrations" | "defocus" | "pixel-pitch";
77
+ export interface DetailLimitOptics {
78
+ /** Working f-number N_w = N·v/f (thin-lens image distance v; N at infinity focus). */
79
+ workingFNumber: number;
80
+ /** Diffraction cut-off 1/(λ·N_w) in cycles/mm: no optics passes detail beyond it. */
81
+ cutoffCyclesPerMm: number;
82
+ /** Frequency where the optics' MTF falls to 0.5, the lower of the two directions, cycles/mm. */
83
+ mtf50CyclesPerMm: number;
84
+ /** MTF50 for detail along the radius from the image center (tangential), cycles/mm. */
85
+ tangentialMtf50CyclesPerMm: number;
86
+ /** MTF50 for detail across the radius (sagittal), cycles/mm. Equal to the tangential value at the center. */
87
+ sagittalMtf50CyclesPerMm: number;
88
+ /** MTF50 of an aberration-free lens in focus at the same working f-number (0.40397·cut-off), cycles/mm. */
89
+ diffractionMtf50CyclesPerMm: number;
90
+ /** Optics MTF at the sensor's Nyquist frequency (lower direction). */
91
+ mtfAtNyquist: number;
92
+ /** RMS wavefront error in waves at the wavelength: of the aberrations, of the defocus, and of both together. */
93
+ rmsWavefrontErrorWaves: {
94
+ aberrations: number;
95
+ defocus: number;
96
+ total: number;
97
+ };
98
+ /** Whether the total RMS wavefront error is within the Maréchal criterion λ/14. */
99
+ withinMarechalCriterion: boolean;
100
+ /** Geometric blur-circle diameter of the defocus on the sensor, mm (0 in focus). */
101
+ defocusBlurDiameterMm: number;
102
+ }
103
+ export interface DetailLimitSampling {
104
+ colorFilterArray: "bayer" | "none";
105
+ /** Pixel pitch in micrometres: widthMm / pixelWidth. */
106
+ pixelPitchMicrometers: number;
107
+ /** 1/(2p), cycles/mm: the finest detail the pixel grid records without aliasing (Bayer green sites too, along rows and columns). */
108
+ nyquistCyclesPerMm: number;
109
+ /** Bayer only: 1/(4p), the Nyquist frequency of the red and blue sites. Null without a color filter array. */
110
+ redBlueNyquistCyclesPerMm: number | null;
111
+ /** Pixel aperture MTF at the Nyquist frequency: sinc(1/2) = 2/π. */
112
+ pixelApertureMtfAtNyquist: number;
113
+ /** Frequency where the pixel aperture MTF falls to 0.5 (0.60335/p), cycles/mm: beyond the Nyquist frequency. */
114
+ pixelApertureMtf50CyclesPerMm: number;
115
+ }
116
+ export interface DetailLimitSystem {
117
+ /** MTF50 of optics × pixel aperture (lower direction), cycles/mm. Detail above the Nyquist frequency aliases. */
118
+ mtf50CyclesPerMm: number;
119
+ /** The same in line pairs per picture height (cycles/mm × heightMm). */
120
+ mtf50LinePairsPerPictureHeight: number;
121
+ /** System MTF at the Nyquist frequency (lower direction). */
122
+ mtfAtNyquist: number;
123
+ }
124
+ /** Whether a quantity is exact under its stated conditions or an approximation. */
125
+ export interface DetailLimitBasis {
126
+ kind: "exact" | "approximation";
127
+ statement: string;
128
+ }
129
+ export interface DetailLimit {
130
+ /** The lens or the sensor. */
131
+ limitedBy: DetailLimitedBy;
132
+ /** Diffraction, aberrations or defocus (the lens), or the pixel pitch (the sensor). */
133
+ limitingFactor: DetailLimitingFactor;
134
+ /** Optics MTF50 / Nyquist frequency: at least 1 when the sensor limits. */
135
+ opticsToNyquistRatio: number;
136
+ fNumber: number;
137
+ wavelengthNm: number;
138
+ /** Physical focal length used (the catalogue lens's on this sensor), mm. */
139
+ focalLengthMm: number;
140
+ fieldPosition: number;
141
+ optics: DetailLimitOptics;
142
+ sampling: DetailLimitSampling;
143
+ system: DetailLimitSystem;
144
+ /**
145
+ * The f-number at which an aberration-free lens in focus has its MTF50 at the Nyquist frequency (2·p·0.40397/λ at
146
+ * infinity focus): from it on, diffraction limits detail on this sensor whatever the lens.
147
+ */
148
+ diffractionLimitedFNumber: number;
149
+ basis: Readonly<Record<"optics" | "sampling" | "system" | "limitedBy", DetailLimitBasis>>;
150
+ }
151
+ /** @internal Seidel and defocus terms of the wavefront in waves at the wavelength, field along +y. */
152
+ export interface DetailLimitWavefront {
153
+ defocus: number;
154
+ spherical: number;
155
+ coma: number;
156
+ astigmatism: number;
157
+ fieldCurvature: number;
158
+ }
159
+ /**
160
+ * @internal |OTF| at normalized frequency s (0 ≤ s < 1) along the pupil's x axis (`tangential` false) or y axis (true). The overlap
161
+ * |ρ ± s·e| ≤ 1 is |a| ≤ 1 − s, |b| ≤ √(1 − (|a| + s)²) in coordinates (a along e, b across); |a| = (1 − s)(1 − τ²) removes
162
+ * the square-root edge, so the integrand is smooth in τ and in b/b_max.
163
+ */
164
+ export declare function detailLimitOpticsMtf(w: DetailLimitWavefront, s: number, tangential: boolean, nodeScale?: number): number;
165
+ /**
166
+ * Which limits detail for given settings: the lens (diffraction at this f-number, aberrations, defocus) or the sensor's pixel
167
+ * pitch. Returns the optics' resolution (MTF50 and the diffraction cut-off, cycles/mm), the sampling's (Nyquist frequency
168
+ * and, with a Bayer filter, the red and blue sites'), the system MTF50 (optics × pixel aperture), the f-number from which
169
+ * diffraction limits on this sensor, and which one limits.
170
+ *
171
+ * The sensor limits when the optics still pass 50 % contrast at the Nyquist frequency 1/(2p); otherwise the lens does,
172
+ * through diffraction when its wavefront error is within the Maréchal criterion (RMS ≤ λ/14), else through aberrations or
173
+ * defocus. The optics MTF is the exact scalar OTF of the stated wavefront at one wavelength; the decision rule and the
174
+ * catalogue lenses' wavefronts are approximations.
175
+ *
176
+ * 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
177
+ * MTF50 ≈ 33 cycles/mm against a Nyquist frequency of ≈ 83); at f/4 the pixel pitch limits.
178
+ *
179
+ * @param input Sensor, lens, f-number, and optional focus, field position and wavelength.
180
+ * @returns The resolution of the optics and of the sampling, and which limits detail.
181
+ */
182
+ export declare function calculateDetailLimit(input: CalculateDetailLimitInput): CalculationResult<DetailLimit>;
@@ -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"]}
@@ -15,17 +15,7 @@ import { calculatedResult } from "../core/calculation-result.js";
15
15
  import { InvalidConfigurationError } from "../core/configuration-error.js";
16
16
  import { requirePositiveFinite } from "../core/validation.js";
17
17
  import { calculateThinLensImageDistance } from "./thin-lens.js";
18
- import { requireInputKeys, rejectUnknownFields } from "../core/record-validation.js";
19
- function requireRecord(value, path, allowedKeys) {
20
- if (typeof value !== "object" ||
21
- value === null ||
22
- Array.isArray(value)) {
23
- throw new InvalidConfigurationError(path + " must be an object.");
24
- }
25
- if (allowedKeys !== null)
26
- rejectUnknownFields(value, allowedKeys, path);
27
- return value;
28
- }
18
+ import { requireInputKeys, requireConfigRecord as requireRecord } from "../core/record-validation.js";
29
19
  /**
30
20
  * Parses an explicit focus-plane state from untrusted JSON.
31
21
  *