@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
@@ -1 +1 @@
1
- {"version":3,"file":"spatial-sampling-quadrature.js","sourceRoot":"","sources":["../../src/sensor/spatial-sampling-quadrature.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Spatial sampling quadrature: the sample points (nodes) and weights used to average light over one\n * sensor site.\n *\n * It combines the site's geometric light-sensitive aperture (sampling-aperture) with the anti-aliasing\n * (AA) point-splitting kernel (optical-stack). The aperture is integrated with a grid of evenly spaced\n * midpoints. If an AA component moves light by +delta on the sensor, a destination point samples the\n * light field before AA at destination − delta, so the shift is never applied twice. Normalized weights\n * give a spatial average; square-micrometre measures give a geometric area integral. Neither includes\n * throughput, QE (quantum efficiency), photon conversion, microlenses, diffusion, crosstalk or\n * integration over time.\n * @see docs/MOTION_AND_SIGNAL.md for equations, coordinate/unit conventions, blockers and support\n * limits.\n */\n\nimport {\n approximationResult,\n type CalculationResult\n} from \"../core/calculation-result.js\";\nimport {\n InvalidScientificInputError,\n requirePositiveInteger\n} from \"../core/validation.js\";\nimport type {\n NativeEffectiveRasterColorSamplingBindingProfile\n} from \"./capture-color-sampling-binding.js\";\nimport type {\n NativeColorSamplingSiteIndex,\n SensorColorSamplingProfile\n} from \"./color-sampling.js\";\nimport {\n resolveAntiAliasingSpatialKernel,\n type SensorOpticalStackProfile\n} from \"./optical-stack.js\";\nimport {\n resolveSensorSamplingAperture,\n type SensorSamplingApertureProfile\n} from \"./sampling-aperture.js\";\nimport type {\n NativeImageRaster,\n SensorImagingArea\n} from \"./sensor-geometry.js\";\nimport { requireInputKeys } from \"../core/record-validation.js\";\n\nconst MAX_SPATIAL_QUADRATURE_NODES = 100_000;\n\nexport interface CalculateSensorSpatialSamplingQuadratureInput {\n imagingArea: SensorImagingArea;\n nativeRaster: NativeImageRaster;\n colorSamplingProfile: SensorColorSamplingProfile;\n colorSamplingBindingProfile: NativeEffectiveRasterColorSamplingBindingProfile;\n samplingApertureProfile: SensorSamplingApertureProfile;\n opticalStackProfile: SensorOpticalStackProfile;\n site: NativeColorSamplingSiteIndex;\n /** Number of midpoint cells across the geometric aperture width. */\n spatialSampleCountX: number;\n /** Number of midpoint cells across the geometric aperture height. */\n spatialSampleCountY: number;\n}\n\nexport interface SensorSpatialSamplingQuadratureNode {\n antiAliasingComponentIndex: number;\n apertureSampleXIndex: number;\n apertureSampleYIndex: number;\n /**\n * Destination point inside the geometric sensitive aperture, after the\n * declared AA point splitting.\n *\n * Native sensor physical coordinates, in millimetres from the optical axis.\n */\n destinationAperturePointMm: {\n x: number;\n y: number;\n };\n /**\n * How far the declared AA component moves light on the sensor plane.\n *\n * +X right, +Y down; micrometres.\n */\n antiAliasingOffsetMicrometers: {\n x: number;\n y: number;\n };\n /**\n * The point in the light field before AA that lands on\n * destinationAperturePointMm through this split component:\n *\n * source = destination - AA offset\n */\n preAntiAliasingSourcePointMm: {\n x: number;\n y: number;\n };\n /**\n * Whether the pre-AA source point is inside the active physical imaging\n * area. False is not an error; downstream code decides how to handle valid\n * light from outside the active sampling area.\n */\n preAntiAliasingSourceInsideImagingArea: boolean;\n /** Dimensionless midpoint weight for the aperture-area average. */\n normalizedApertureWeight: number;\n /** Dimensionless normalized weight from the AA split component. */\n normalizedAntiAliasingWeight: number;\n /**\n * Product of the two weights (dimensionless), used to average a light field\n * over the site with this quadrature.\n */\n combinedNormalizedSpatialWeight: number;\n /**\n * Geometric area this aperture midpoint stands for, before the AA component\n * weight is applied. Units: square micrometres.\n */\n apertureAreaMeasureSquareMicrometers: number;\n /**\n * Geometric area multiplied by the (dimensionless) AA component weight.\n * Units are still square micrometres.\n *\n * This is not an effective light-collection area.\n */\n combinedAreaMeasureSquareMicrometers: number;\n}\n\nexport interface SensorSpatialSamplingQuadrature {\n coordinateSystem: \"native-sensor-physical\";\n colorSamplingProfileId?: string;\n samplingApertureProfileId?: string;\n opticalStackProfileId?: string;\n site: NativeColorSamplingSiteIndex;\n channelId: string;\n quadratureScheme:\n \"tensor-product-uniform-midpoint\";\n outputMeaning:\n \"pre-aa-optical-field-spatial-quadrature-nodes\";\n spatialSampleCountX: number;\n spatialSampleCountY: number;\n antiAliasingComponentCount: number;\n totalNodeCount: number;\n geometricApertureAreaSquareMicrometers: number;\n nominalSiteCellAreaSquareMicrometers?: number;\n geometricSensitiveAreaFractionOfLatticeCell?: number;\n normalizedSpatialWeightSum: number;\n combinedAreaMeasureSumSquareMicrometers: number;\n preAntiAliasingSourceOutsideImagingAreaNodeCount: number;\n sourcePointRule:\n \"pre-aa-source-equals-destination-minus-aa-offset\";\n destinationCfaChannelAppliesToAllNodes: true;\n cfaReassignmentBySourceCoordinate: false;\n sourceCoveragePolicy: \"not-applied\";\n nodes: readonly SensorSpatialSamplingQuadratureNode[];\n /**\n * Fixed statements of what this result does not include.\n */\n opticalFieldValuesIncluded: false;\n radianceIncluded: false;\n photonsIncluded: false;\n electronsIncluded: false;\n spectralResponseIncluded: false;\n quantumEfficiencyIncluded: false;\n opticalThroughputIncluded: false;\n microlensResponseIncluded: false;\n chargeDiffusionIncluded: false;\n electricalCrosstalkIncluded: false;\n temporalIntegrationIncluded: false;\n rawSampleValueIncluded: false;\n reconstructedPixelValueIncluded: false;\n geometricAreaMeasureIsRadiometricCollectionArea: false;\n componentEvidence: {\n samplingApertureProfile: readonly import(\"../core/evidence-provenance.js\").EvidenceProvenance[];\n samplingApertureLattice: readonly import(\"../core/evidence-provenance.js\").EvidenceProvenance[];\n geometricSensitiveAperture: readonly import(\"../core/evidence-provenance.js\").EvidenceProvenance[];\n opticalStackProfile: readonly import(\"../core/evidence-provenance.js\").EvidenceProvenance[];\n antiAliasingResponse: readonly import(\"../core/evidence-provenance.js\").EvidenceProvenance[];\n };\n}\n\nfunction midpointCoordinate(\n index: number,\n count: number\n): number {\n return (index + 0.5) / count;\n}\n\nfunction insideImagingArea(\n pointMm: { x: number; y: number },\n imagingArea: SensorImagingArea\n): boolean {\n const halfWidth = imagingArea.widthMm / 2;\n const halfHeight = imagingArea.heightMm / 2;\n\n return (\n pointMm.x >= -halfWidth &&\n pointMm.x <= halfWidth &&\n pointMm.y >= -halfHeight &&\n pointMm.y <= halfHeight\n );\n}\n\n/**\n * Builds the deterministic spatial sample points (quadrature nodes) for one CFA\n * (colour filter array) site.\n *\n * It combines:\n *\n * 1. the site's geometric light-sensitive aperture (from sampling-aperture); and\n * 2. the anti-aliasing (AA) point-splitting kernel (from optical-stack).\n *\n * The aperture is integrated with a grid of evenly spaced midpoints (a\n * tensor-product midpoint rule). If an AA component moves light by +delta on\n * the sensor, a destination point samples the light field before AA at\n * destination − delta. Working backwards like this means the shift is never\n * applied twice.\n *\n * The destination site's CFA channel applies to every node. The pre-AA source\n * point is a position in the light field, not another CFA site, so a shifted\n * source point never changes the destination channel.\n *\n * Nodes whose pre-AA source point falls outside the active imaging area are\n * kept. They are neither clamped nor dropped (and weights are not\n * renormalized). The renderer or light-field provider decides how to cover them.\n *\n * Normalized weights approximate a spatial average. Square-micrometre measures\n * approximate a geometric area integral. Neither includes throughput, QE,\n * photon conversion, microlenses, diffusion, crosstalk or integration over time.\n */\nexport function calculateSensorSpatialSamplingQuadrature(\n input: CalculateSensorSpatialSamplingQuadratureInput\n): CalculationResult<SensorSpatialSamplingQuadrature> {\n requireInputKeys(input, \"calculateSensorSpatialSamplingQuadrature\", [\"imagingArea\", \"nativeRaster\", \"colorSamplingProfile\", \"colorSamplingBindingProfile\", \"samplingApertureProfile\", \"opticalStackProfile\", \"site\", \"spatialSampleCountX\", \"spatialSampleCountY\"]);\n requirePositiveInteger(\n \"spatialSampleCountX\",\n input.spatialSampleCountX\n );\n requirePositiveInteger(\n \"spatialSampleCountY\",\n input.spatialSampleCountY\n );\n\n const samplingAperture =\n resolveSensorSamplingAperture({\n imagingArea: input.imagingArea,\n nativeRaster: input.nativeRaster,\n colorSamplingProfile:\n input.colorSamplingProfile,\n colorSamplingBindingProfile:\n input.colorSamplingBindingProfile,\n samplingApertureProfile:\n input.samplingApertureProfile,\n site: input.site\n });\n\n const antiAliasing =\n resolveAntiAliasingSpatialKernel(\n input.opticalStackProfile\n );\n\n const apertureNodeCount =\n input.spatialSampleCountX *\n input.spatialSampleCountY;\n if (!Number.isSafeInteger(apertureNodeCount)) {\n throw new InvalidScientificInputError(\n \"spatialSampleCountX × spatialSampleCountY must be a safe integer.\"\n );\n }\n\n const totalNodeCount =\n apertureNodeCount *\n antiAliasing.components.length;\n if (!Number.isSafeInteger(totalNodeCount)) {\n throw new InvalidScientificInputError(\n \"Spatial quadrature node count must be a safe integer.\"\n );\n }\n if (\n totalNodeCount >\n MAX_SPATIAL_QUADRATURE_NODES\n ) {\n throw new InvalidScientificInputError(\n \"Spatial quadrature node count exceeds the per-site safety limit of \" +\n MAX_SPATIAL_QUADRATURE_NODES +\n \". Increase fidelity by tiling or batching downstream work rather than allocating one oversized node set.\"\n );\n }\n\n const aperture =\n samplingAperture.geometricSensitiveAperture;\n const normalizedApertureWeight =\n 1 / apertureNodeCount;\n const apertureAreaMeasureSquareMicrometers =\n aperture.areaSquareMicrometers /\n apertureNodeCount;\n\n const apertureCenterMm =\n aperture.centerFromOpticalAxisMm;\n const widthMm =\n aperture.widthMicrometers / 1000;\n const heightMm =\n aperture.heightMicrometers / 1000;\n const leftMm =\n apertureCenterMm.x - widthMm / 2;\n const topMm =\n apertureCenterMm.y - heightMm / 2;\n\n const nodes: SensorSpatialSamplingQuadratureNode[] = [];\n let outsideCount = 0;\n\n antiAliasing.components.forEach(\n (aaComponent, antiAliasingComponentIndex) => {\n const aaOffsetMm = {\n x:\n aaComponent.offsetMicrometers.x /\n 1000,\n y:\n aaComponent.offsetMicrometers.y /\n 1000\n };\n\n for (\n let apertureSampleYIndex = 0;\n apertureSampleYIndex <\n input.spatialSampleCountY;\n apertureSampleYIndex += 1\n ) {\n const yPhase = midpointCoordinate(\n apertureSampleYIndex,\n input.spatialSampleCountY\n );\n const destinationY =\n topMm + yPhase * heightMm;\n\n for (\n let apertureSampleXIndex = 0;\n apertureSampleXIndex <\n input.spatialSampleCountX;\n apertureSampleXIndex += 1\n ) {\n const xPhase = midpointCoordinate(\n apertureSampleXIndex,\n input.spatialSampleCountX\n );\n const destinationX =\n leftMm + xPhase * widthMm;\n const destinationAperturePointMm = {\n x: destinationX,\n y: destinationY\n };\n const preAntiAliasingSourcePointMm = {\n x:\n destinationX -\n aaOffsetMm.x,\n y:\n destinationY -\n aaOffsetMm.y\n };\n const sourceInside = insideImagingArea(\n preAntiAliasingSourcePointMm,\n input.imagingArea\n );\n if (!sourceInside) {\n outsideCount += 1;\n }\n\n const combinedNormalizedSpatialWeight =\n normalizedApertureWeight *\n aaComponent.normalizedWeight;\n const combinedAreaMeasureSquareMicrometers =\n apertureAreaMeasureSquareMicrometers *\n aaComponent.normalizedWeight;\n\n nodes.push({\n antiAliasingComponentIndex,\n apertureSampleXIndex,\n apertureSampleYIndex,\n destinationAperturePointMm,\n antiAliasingOffsetMicrometers: {\n ...aaComponent.offsetMicrometers\n },\n preAntiAliasingSourcePointMm,\n preAntiAliasingSourceInsideImagingArea:\n sourceInside,\n normalizedApertureWeight,\n normalizedAntiAliasingWeight:\n aaComponent.normalizedWeight,\n combinedNormalizedSpatialWeight,\n apertureAreaMeasureSquareMicrometers,\n combinedAreaMeasureSquareMicrometers\n });\n }\n }\n }\n );\n\n const normalizedSpatialWeightSum =\n nodes.reduce(\n (sum, node) =>\n sum +\n node.combinedNormalizedSpatialWeight,\n 0\n );\n const combinedAreaMeasureSumSquareMicrometers =\n nodes.reduce(\n (sum, node) =>\n sum +\n node.combinedAreaMeasureSquareMicrometers,\n 0\n );\n\n return approximationResult(\n {\n coordinateSystem:\n \"native-sensor-physical\",\n colorSamplingProfileId:\n samplingAperture\n .colorSamplingProfileId,\n samplingApertureProfileId:\n samplingAperture.profileId,\n opticalStackProfileId:\n antiAliasing.profileId,\n site: {\n ...samplingAperture.site\n },\n channelId:\n samplingAperture.channelId,\n quadratureScheme:\n \"tensor-product-uniform-midpoint\",\n outputMeaning:\n \"pre-aa-optical-field-spatial-quadrature-nodes\",\n spatialSampleCountX:\n input.spatialSampleCountX,\n spatialSampleCountY:\n input.spatialSampleCountY,\n antiAliasingComponentCount:\n antiAliasing.components.length,\n totalNodeCount,\n geometricApertureAreaSquareMicrometers:\n aperture.areaSquareMicrometers,\n nominalSiteCellAreaSquareMicrometers:\n samplingAperture.lattice\n .nominalCellAreaSquareMicrometers,\n geometricSensitiveAreaFractionOfLatticeCell:\n aperture\n .geometricSensitiveAreaFractionOfLatticeCell,\n normalizedSpatialWeightSum,\n combinedAreaMeasureSumSquareMicrometers,\n preAntiAliasingSourceOutsideImagingAreaNodeCount:\n outsideCount,\n sourcePointRule:\n \"pre-aa-source-equals-destination-minus-aa-offset\",\n destinationCfaChannelAppliesToAllNodes:\n true,\n cfaReassignmentBySourceCoordinate:\n false,\n sourceCoveragePolicy:\n \"not-applied\",\n nodes,\n opticalFieldValuesIncluded: false,\n radianceIncluded: false,\n photonsIncluded: false,\n electronsIncluded: false,\n spectralResponseIncluded: false,\n quantumEfficiencyIncluded: false,\n opticalThroughputIncluded: false,\n microlensResponseIncluded: false,\n chargeDiffusionIncluded: false,\n electricalCrosstalkIncluded: false,\n temporalIntegrationIncluded: false,\n rawSampleValueIncluded: false,\n reconstructedPixelValueIncluded: false,\n geometricAreaMeasureIsRadiometricCollectionArea:\n false,\n componentEvidence: {\n samplingApertureProfile:\n samplingAperture.provenance.profile,\n samplingApertureLattice:\n samplingAperture.provenance.lattice,\n geometricSensitiveAperture:\n samplingAperture.provenance.aperture,\n opticalStackProfile:\n antiAliasing.provenance.profile,\n antiAliasingResponse:\n antiAliasing.provenance\n .antiAliasingResponse\n }\n },\n \"sensor-spatial-sampling-quadrature\",\n \"1.0.0\",\n [\n \"Spatial aperture integration uses a deterministic tensor-product uniform midpoint rule.\",\n \"The destination sensor site's semantic CFA/color channel applies to every quadrature node; pre-AA source coordinates never reassign the destination channel.\",\n \"AA point-splitting is inverse-sampled: a +delta output displacement reads the pre-AA optical field at destination - delta.\",\n \"AA component weights and aperture midpoint weights are dimensionless normalized spatial-average weights.\",\n \"Square-micrometre measures are geometric integration measures only and are not effective radiometric collection area, throughput, QE, or photon count.\",\n \"Pre-AA source points may leave the active physical imaging area and are not clamped, dropped, or renormalized.\",\n \"The AA kernel remains the optical-stack foundation's field/wavelength/polarization-invariant approximation where applicable.\",\n \"Microlens spatial response, charge diffusion, electrical/optical crosstalk, spectral response, radiometry, temporal integration, RAW sample generation, and reconstruction remain separate future work.\",\n \"No geometry-only spatial quadrature error estimate is reported because downstream optical-field spatial frequency/content determines integration error.\"\n ]\n );\n}\n"]}
1
+ {"version":3,"file":"spatial-sampling-quadrature.js","sourceRoot":"","sources":["../../src/sensor/spatial-sampling-quadrature.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Spatial sampling quadrature: the sample points (nodes) and weights used to average light over one\n * sensor site.\n *\n * It combines the site's geometric light-sensitive aperture (sampling-aperture) with the anti-aliasing\n * (AA) point-splitting kernel (optical-stack). The aperture is integrated with a grid of evenly spaced\n * midpoints. If an AA component moves light by +delta on the sensor, a destination point samples the\n * light field before AA at destination − delta, so the shift is never applied twice. Normalized weights\n * give a spatial average; square-micrometre measures give a geometric area integral. Neither includes\n * throughput, QE (quantum efficiency), photon conversion, microlenses, diffusion, crosstalk or\n * integration over time.\n * @see docs/MOTION_AND_SIGNAL.md for equations, coordinate/unit conventions, blockers and support\n * limits.\n */\n\nimport {\n approximationResult,\n type CalculationResult\n} from \"../core/calculation-result.js\";\nimport {\n InvalidScientificInputError,\n requirePositiveInteger\n} from \"../core/validation.js\";\nimport type {\n NativeEffectiveRasterColorSamplingBindingProfile\n} from \"./capture-color-sampling-binding.js\";\nimport type {\n NativeColorSamplingSiteIndex,\n SensorColorSamplingProfile\n} from \"./color-sampling.js\";\nimport {\n resolveAntiAliasingSpatialKernel,\n type SensorOpticalStackProfile\n} from \"./optical-stack.js\";\nimport {\n resolveSensorSamplingAperture,\n type SensorSamplingApertureProfile\n} from \"./sampling-aperture.js\";\nimport type {\n NativeImageRaster,\n SensorImagingArea\n} from \"./sensor-geometry.js\";\nimport { requireInputKeys } from \"../core/record-validation.js\";\nimport { midpointPhase as midpointCoordinate } from \"../motion/capture-rotation-temporal-quadrature.js\";\n\nconst MAX_SPATIAL_QUADRATURE_NODES = 100_000;\n\nexport interface CalculateSensorSpatialSamplingQuadratureInput {\n imagingArea: SensorImagingArea;\n nativeRaster: NativeImageRaster;\n colorSamplingProfile: SensorColorSamplingProfile;\n colorSamplingBindingProfile: NativeEffectiveRasterColorSamplingBindingProfile;\n samplingApertureProfile: SensorSamplingApertureProfile;\n opticalStackProfile: SensorOpticalStackProfile;\n site: NativeColorSamplingSiteIndex;\n /** Number of midpoint cells across the geometric aperture width. */\n spatialSampleCountX: number;\n /** Number of midpoint cells across the geometric aperture height. */\n spatialSampleCountY: number;\n}\n\nexport interface SensorSpatialSamplingQuadratureNode {\n antiAliasingComponentIndex: number;\n apertureSampleXIndex: number;\n apertureSampleYIndex: number;\n /**\n * Destination point inside the geometric sensitive aperture, after the\n * declared AA point splitting.\n *\n * Native sensor physical coordinates, in millimetres from the optical axis.\n */\n destinationAperturePointMm: {\n x: number;\n y: number;\n };\n /**\n * How far the declared AA component moves light on the sensor plane.\n *\n * +X right, +Y down; micrometres.\n */\n antiAliasingOffsetMicrometers: {\n x: number;\n y: number;\n };\n /**\n * The point in the light field before AA that lands on\n * destinationAperturePointMm through this split component:\n *\n * source = destination - AA offset\n */\n preAntiAliasingSourcePointMm: {\n x: number;\n y: number;\n };\n /**\n * Whether the pre-AA source point is inside the active physical imaging\n * area. False is not an error; downstream code decides how to handle valid\n * light from outside the active sampling area.\n */\n preAntiAliasingSourceInsideImagingArea: boolean;\n /** Dimensionless midpoint weight for the aperture-area average. */\n normalizedApertureWeight: number;\n /** Dimensionless normalized weight from the AA split component. */\n normalizedAntiAliasingWeight: number;\n /**\n * Product of the two weights (dimensionless), used to average a light field\n * over the site with this quadrature.\n */\n combinedNormalizedSpatialWeight: number;\n /**\n * Geometric area this aperture midpoint stands for, before the AA component\n * weight is applied. Units: square micrometres.\n */\n apertureAreaMeasureSquareMicrometers: number;\n /**\n * Geometric area multiplied by the (dimensionless) AA component weight.\n * Units are still square micrometres.\n *\n * This is not an effective light-collection area.\n */\n combinedAreaMeasureSquareMicrometers: number;\n}\n\nexport interface SensorSpatialSamplingQuadrature {\n coordinateSystem: \"native-sensor-physical\";\n colorSamplingProfileId?: string;\n samplingApertureProfileId?: string;\n opticalStackProfileId?: string;\n site: NativeColorSamplingSiteIndex;\n channelId: string;\n quadratureScheme:\n \"tensor-product-uniform-midpoint\";\n outputMeaning:\n \"pre-aa-optical-field-spatial-quadrature-nodes\";\n spatialSampleCountX: number;\n spatialSampleCountY: number;\n antiAliasingComponentCount: number;\n totalNodeCount: number;\n geometricApertureAreaSquareMicrometers: number;\n nominalSiteCellAreaSquareMicrometers?: number;\n geometricSensitiveAreaFractionOfLatticeCell?: number;\n normalizedSpatialWeightSum: number;\n combinedAreaMeasureSumSquareMicrometers: number;\n preAntiAliasingSourceOutsideImagingAreaNodeCount: number;\n sourcePointRule:\n \"pre-aa-source-equals-destination-minus-aa-offset\";\n destinationCfaChannelAppliesToAllNodes: true;\n cfaReassignmentBySourceCoordinate: false;\n sourceCoveragePolicy: \"not-applied\";\n nodes: readonly SensorSpatialSamplingQuadratureNode[];\n /**\n * Fixed statements of what this result does not include.\n */\n opticalFieldValuesIncluded: false;\n radianceIncluded: false;\n photonsIncluded: false;\n electronsIncluded: false;\n spectralResponseIncluded: false;\n quantumEfficiencyIncluded: false;\n opticalThroughputIncluded: false;\n microlensResponseIncluded: false;\n chargeDiffusionIncluded: false;\n electricalCrosstalkIncluded: false;\n temporalIntegrationIncluded: false;\n rawSampleValueIncluded: false;\n reconstructedPixelValueIncluded: false;\n geometricAreaMeasureIsRadiometricCollectionArea: false;\n componentEvidence: {\n samplingApertureProfile: readonly import(\"../core/evidence-provenance.js\").EvidenceProvenance[];\n samplingApertureLattice: readonly import(\"../core/evidence-provenance.js\").EvidenceProvenance[];\n geometricSensitiveAperture: readonly import(\"../core/evidence-provenance.js\").EvidenceProvenance[];\n opticalStackProfile: readonly import(\"../core/evidence-provenance.js\").EvidenceProvenance[];\n antiAliasingResponse: readonly import(\"../core/evidence-provenance.js\").EvidenceProvenance[];\n };\n}\n\nfunction insideImagingArea(\n pointMm: { x: number; y: number },\n imagingArea: SensorImagingArea\n): boolean {\n const halfWidth = imagingArea.widthMm / 2;\n const halfHeight = imagingArea.heightMm / 2;\n\n return (\n pointMm.x >= -halfWidth &&\n pointMm.x <= halfWidth &&\n pointMm.y >= -halfHeight &&\n pointMm.y <= halfHeight\n );\n}\n\n/**\n * Builds the deterministic spatial sample points (quadrature nodes) for one CFA\n * (colour filter array) site.\n *\n * It combines:\n *\n * 1. the site's geometric light-sensitive aperture (from sampling-aperture); and\n * 2. the anti-aliasing (AA) point-splitting kernel (from optical-stack).\n *\n * The aperture is integrated with a grid of evenly spaced midpoints (a\n * tensor-product midpoint rule). If an AA component moves light by +delta on\n * the sensor, a destination point samples the light field before AA at\n * destination − delta. Working backwards like this means the shift is never\n * applied twice.\n *\n * The destination site's CFA channel applies to every node. The pre-AA source\n * point is a position in the light field, not another CFA site, so a shifted\n * source point never changes the destination channel.\n *\n * Nodes whose pre-AA source point falls outside the active imaging area are\n * kept. They are neither clamped nor dropped (and weights are not\n * renormalized). The renderer or light-field provider decides how to cover them.\n *\n * Normalized weights approximate a spatial average. Square-micrometre measures\n * approximate a geometric area integral. Neither includes throughput, QE,\n * photon conversion, microlenses, diffusion, crosstalk or integration over time.\n */\nexport function calculateSensorSpatialSamplingQuadrature(\n input: CalculateSensorSpatialSamplingQuadratureInput\n): CalculationResult<SensorSpatialSamplingQuadrature> {\n requireInputKeys(input, \"calculateSensorSpatialSamplingQuadrature\", [\"imagingArea\", \"nativeRaster\", \"colorSamplingProfile\", \"colorSamplingBindingProfile\", \"samplingApertureProfile\", \"opticalStackProfile\", \"site\", \"spatialSampleCountX\", \"spatialSampleCountY\"]);\n requirePositiveInteger(\n \"spatialSampleCountX\",\n input.spatialSampleCountX\n );\n requirePositiveInteger(\n \"spatialSampleCountY\",\n input.spatialSampleCountY\n );\n\n const samplingAperture =\n resolveSensorSamplingAperture({\n imagingArea: input.imagingArea,\n nativeRaster: input.nativeRaster,\n colorSamplingProfile:\n input.colorSamplingProfile,\n colorSamplingBindingProfile:\n input.colorSamplingBindingProfile,\n samplingApertureProfile:\n input.samplingApertureProfile,\n site: input.site\n });\n\n const antiAliasing =\n resolveAntiAliasingSpatialKernel(\n input.opticalStackProfile\n );\n\n const apertureNodeCount =\n input.spatialSampleCountX *\n input.spatialSampleCountY;\n if (!Number.isSafeInteger(apertureNodeCount)) {\n throw new InvalidScientificInputError(\n \"spatialSampleCountX × spatialSampleCountY must be a safe integer.\"\n );\n }\n\n const totalNodeCount =\n apertureNodeCount *\n antiAliasing.components.length;\n if (!Number.isSafeInteger(totalNodeCount)) {\n throw new InvalidScientificInputError(\n \"Spatial quadrature node count must be a safe integer.\"\n );\n }\n if (\n totalNodeCount >\n MAX_SPATIAL_QUADRATURE_NODES\n ) {\n throw new InvalidScientificInputError(\n \"Spatial quadrature node count exceeds the per-site safety limit of \" +\n MAX_SPATIAL_QUADRATURE_NODES +\n \". Increase fidelity by tiling or batching downstream work rather than allocating one oversized node set.\"\n );\n }\n\n const aperture =\n samplingAperture.geometricSensitiveAperture;\n const normalizedApertureWeight =\n 1 / apertureNodeCount;\n const apertureAreaMeasureSquareMicrometers =\n aperture.areaSquareMicrometers /\n apertureNodeCount;\n\n const apertureCenterMm =\n aperture.centerFromOpticalAxisMm;\n const widthMm =\n aperture.widthMicrometers / 1000;\n const heightMm =\n aperture.heightMicrometers / 1000;\n const leftMm =\n apertureCenterMm.x - widthMm / 2;\n const topMm =\n apertureCenterMm.y - heightMm / 2;\n\n const nodes: SensorSpatialSamplingQuadratureNode[] = [];\n let outsideCount = 0;\n\n antiAliasing.components.forEach(\n (aaComponent, antiAliasingComponentIndex) => {\n const aaOffsetMm = {\n x:\n aaComponent.offsetMicrometers.x /\n 1000,\n y:\n aaComponent.offsetMicrometers.y /\n 1000\n };\n\n for (\n let apertureSampleYIndex = 0;\n apertureSampleYIndex <\n input.spatialSampleCountY;\n apertureSampleYIndex += 1\n ) {\n const yPhase = midpointCoordinate(\n apertureSampleYIndex,\n input.spatialSampleCountY\n );\n const destinationY =\n topMm + yPhase * heightMm;\n\n for (\n let apertureSampleXIndex = 0;\n apertureSampleXIndex <\n input.spatialSampleCountX;\n apertureSampleXIndex += 1\n ) {\n const xPhase = midpointCoordinate(\n apertureSampleXIndex,\n input.spatialSampleCountX\n );\n const destinationX =\n leftMm + xPhase * widthMm;\n const destinationAperturePointMm = {\n x: destinationX,\n y: destinationY\n };\n const preAntiAliasingSourcePointMm = {\n x:\n destinationX -\n aaOffsetMm.x,\n y:\n destinationY -\n aaOffsetMm.y\n };\n const sourceInside = insideImagingArea(\n preAntiAliasingSourcePointMm,\n input.imagingArea\n );\n if (!sourceInside) {\n outsideCount += 1;\n }\n\n const combinedNormalizedSpatialWeight =\n normalizedApertureWeight *\n aaComponent.normalizedWeight;\n const combinedAreaMeasureSquareMicrometers =\n apertureAreaMeasureSquareMicrometers *\n aaComponent.normalizedWeight;\n\n nodes.push({\n antiAliasingComponentIndex,\n apertureSampleXIndex,\n apertureSampleYIndex,\n destinationAperturePointMm,\n antiAliasingOffsetMicrometers: {\n ...aaComponent.offsetMicrometers\n },\n preAntiAliasingSourcePointMm,\n preAntiAliasingSourceInsideImagingArea:\n sourceInside,\n normalizedApertureWeight,\n normalizedAntiAliasingWeight:\n aaComponent.normalizedWeight,\n combinedNormalizedSpatialWeight,\n apertureAreaMeasureSquareMicrometers,\n combinedAreaMeasureSquareMicrometers\n });\n }\n }\n }\n );\n\n const normalizedSpatialWeightSum =\n nodes.reduce(\n (sum, node) =>\n sum +\n node.combinedNormalizedSpatialWeight,\n 0\n );\n const combinedAreaMeasureSumSquareMicrometers =\n nodes.reduce(\n (sum, node) =>\n sum +\n node.combinedAreaMeasureSquareMicrometers,\n 0\n );\n\n return approximationResult(\n {\n coordinateSystem:\n \"native-sensor-physical\",\n colorSamplingProfileId:\n samplingAperture\n .colorSamplingProfileId,\n samplingApertureProfileId:\n samplingAperture.profileId,\n opticalStackProfileId:\n antiAliasing.profileId,\n site: {\n ...samplingAperture.site\n },\n channelId:\n samplingAperture.channelId,\n quadratureScheme:\n \"tensor-product-uniform-midpoint\",\n outputMeaning:\n \"pre-aa-optical-field-spatial-quadrature-nodes\",\n spatialSampleCountX:\n input.spatialSampleCountX,\n spatialSampleCountY:\n input.spatialSampleCountY,\n antiAliasingComponentCount:\n antiAliasing.components.length,\n totalNodeCount,\n geometricApertureAreaSquareMicrometers:\n aperture.areaSquareMicrometers,\n nominalSiteCellAreaSquareMicrometers:\n samplingAperture.lattice\n .nominalCellAreaSquareMicrometers,\n geometricSensitiveAreaFractionOfLatticeCell:\n aperture\n .geometricSensitiveAreaFractionOfLatticeCell,\n normalizedSpatialWeightSum,\n combinedAreaMeasureSumSquareMicrometers,\n preAntiAliasingSourceOutsideImagingAreaNodeCount:\n outsideCount,\n sourcePointRule:\n \"pre-aa-source-equals-destination-minus-aa-offset\",\n destinationCfaChannelAppliesToAllNodes:\n true,\n cfaReassignmentBySourceCoordinate:\n false,\n sourceCoveragePolicy:\n \"not-applied\",\n nodes,\n opticalFieldValuesIncluded: false,\n radianceIncluded: false,\n photonsIncluded: false,\n electronsIncluded: false,\n spectralResponseIncluded: false,\n quantumEfficiencyIncluded: false,\n opticalThroughputIncluded: false,\n microlensResponseIncluded: false,\n chargeDiffusionIncluded: false,\n electricalCrosstalkIncluded: false,\n temporalIntegrationIncluded: false,\n rawSampleValueIncluded: false,\n reconstructedPixelValueIncluded: false,\n geometricAreaMeasureIsRadiometricCollectionArea:\n false,\n componentEvidence: {\n samplingApertureProfile:\n samplingAperture.provenance.profile,\n samplingApertureLattice:\n samplingAperture.provenance.lattice,\n geometricSensitiveAperture:\n samplingAperture.provenance.aperture,\n opticalStackProfile:\n antiAliasing.provenance.profile,\n antiAliasingResponse:\n antiAliasing.provenance\n .antiAliasingResponse\n }\n },\n \"sensor-spatial-sampling-quadrature\",\n \"1.0.0\",\n [\n \"Spatial aperture integration uses a deterministic tensor-product uniform midpoint rule.\",\n \"The destination sensor site's semantic CFA/color channel applies to every quadrature node; pre-AA source coordinates never reassign the destination channel.\",\n \"AA point-splitting is inverse-sampled: a +delta output displacement reads the pre-AA optical field at destination - delta.\",\n \"AA component weights and aperture midpoint weights are dimensionless normalized spatial-average weights.\",\n \"Square-micrometre measures are geometric integration measures only and are not effective radiometric collection area, throughput, QE, or photon count.\",\n \"Pre-AA source points may leave the active physical imaging area and are not clamped, dropped, or renormalized.\",\n \"The AA kernel remains the optical-stack foundation's field/wavelength/polarization-invariant approximation where applicable.\",\n \"Microlens spatial response, charge diffusion, electrical/optical crosstalk, spectral response, radiometry, temporal integration, RAW sample generation, and reconstruction remain separate future work.\",\n \"No geometry-only spatial quadrature error estimate is reported because downstream optical-field spatial frequency/content determines integration error.\"\n ]\n );\n}\n"]}
@@ -12,27 +12,10 @@ import { approximationResult } from "../core/calculation-result.js";
12
12
  import { InvalidScientificInputError } from "../core/validation.js";
13
13
  import { sensorSpatialQuadratureNodeIdentityKey, validateSensorSpatialSamplingQuadrature } from "./spatial-sample-reduction.js";
14
14
  import { validateSensorSpectralQuadrature } from "./spectral-quadrature.js";
15
- import { requireInputKeys } from "../core/record-validation.js";
15
+ import { requireInputKeys, requireInputNonNegativeFinite as requireNonNegativeFinite, requireInputNonNegativeSafeInteger as requireSafeNonNegativeInteger } from "../core/record-validation.js";
16
16
  import { InvalidConfigurationError } from "../core/configuration-error.js";
17
17
  const SQUARE_MICROMETERS_TO_SQUARE_METERS = 1e-12;
18
18
  const MAX_SPATIO_SPECTRAL_SAMPLE_VALUES = 100_000;
19
- function requireSafeNonNegativeInteger(value, path) {
20
- if (typeof value !== "number" ||
21
- !Number.isSafeInteger(value) ||
22
- value < 0) {
23
- throw new InvalidScientificInputError(path + " must be a non-negative safe integer.");
24
- }
25
- return value;
26
- }
27
- function requireNonNegativeFinite(value, path) {
28
- if (typeof value !== "number" ||
29
- !Number.isFinite(value) ||
30
- value < 0) {
31
- throw new InvalidScientificInputError(path +
32
- " must be finite and greater than or equal to zero.");
33
- }
34
- return value;
35
- }
36
19
  function sampleKey(spatialNode, spectralSampleIndex) {
37
20
  return (sensorSpatialQuadratureNodeIdentityKey(spatialNode) +
38
21
  "|" +
@@ -1 +1 @@
1
- {"version":3,"file":"spatio-spectral-reduction.js","sourceRoot":"","sources":["../../src/sensor/spatio-spectral-reduction.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Spatio-spectral reduction: integrates spectral irradiance on the sensor over one site's area and\n * over wavelength, giving the incident light power on the site's geometric aperture.\n *\n * It stops before the sensor response: QE (quantum efficiency), A/W responsivity and colour-filter\n * transmission are applied in later steps.\n * @see docs/MOTION_AND_SIGNAL.md for equations, coordinate/unit conventions, blockers and support\n * limits.\n */\n\nimport {\n approximationResult,\n type CalculationResult\n} from \"../core/calculation-result.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport {\n sensorSpatialQuadratureNodeIdentityKey,\n validateSensorSpatialSamplingQuadrature,\n type SensorSpatialQuadratureNodeIdentity\n} from \"./spatial-sample-reduction.js\";\nimport type {\n SensorSpatialSamplingQuadrature\n} from \"./spatial-sampling-quadrature.js\";\nimport {\n validateSensorSpectralQuadrature,\n type SensorSpectralQuadrature,\n type SensorSpectralQuadratureNode\n} from \"./spectral-quadrature.js\";\nimport { requireInputKeys } from \"../core/record-validation.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\n\nconst SQUARE_MICROMETERS_TO_SQUARE_METERS = 1e-12;\nconst MAX_SPATIO_SPECTRAL_SAMPLE_VALUES = 100_000;\n\nexport interface SensorSpatioSpectralNodeIdentity {\n spatialNode: SensorSpatialQuadratureNodeIdentity;\n spectralSampleIndex: number;\n wavelengthNanometers: number;\n}\n\nexport interface SensorSpatioSpectralIrradianceSample {\n node: SensorSpatioSpectralNodeIdentity;\n /**\n * Spectral irradiance at the spatial node's preAntiAliasingSourcePointMm\n * and this spectral node's wavelength.\n *\n * Units: W/m^2/nm.\n */\n spectralIrradianceWattsPerSquareMeterPerNanometer: number;\n}\n\nexport interface ReduceSensorSpatioSpectralIrradianceInput {\n spatialQuadrature: SensorSpatialSamplingQuadrature;\n spectralQuadrature: SensorSpectralQuadrature;\n /**\n * Exactly one E_lambda(x,y) value, with its node identity, for every\n * combination of spatial node and spectral node.\n *\n * Order does not matter; the node identity does.\n */\n sampleValues: readonly SensorSpatioSpectralIrradianceSample[];\n}\n\nexport interface SensorSpatioSpectralWavelengthReduction {\n spectralSampleIndex: number;\n wavelengthNanometers: number;\n wavelengthMeasureNanometers: number;\n normalizedWavelengthWeight: number;\n /**\n * Spatially reduced spectral irradiance at this wavelength.\n * Units: W/m^2/nm.\n */\n normalizedSpatialAverageSpectralIrradianceWattsPerSquareMeterPerNanometer:\n number;\n /**\n * Spectral radiant flux reaching the geometric sensitive aperture, after the\n * declared normalized AA spreading. Units: W/nm.\n *\n * This uses geometric area, not an established light-collection area.\n */\n geometricApertureIncidentSpectralFluxWattsPerNanometer: number;\n /**\n * Contribution to the wavelength-integrated spatial average.\n * Units: W/m^2.\n */\n wavelengthIntegratedSpatialAverageContributionWattsPerSquareMeter: number;\n /**\n * Contribution to wavelength-integrated geometric-aperture incident flux.\n * Units: W.\n */\n wavelengthIntegratedGeometricApertureIncidentFluxContributionWatts: number;\n}\n\nexport interface SensorSpatioSpectralIrradianceReduction {\n colorSamplingProfileId: string;\n samplingApertureProfileId?: string;\n opticalStackProfileId?: string;\n site: {\n x: number;\n y: number;\n };\n channelId: string;\n responseProfileId: string;\n sourceResponseKind:\n SensorSpectralQuadrature[\"sourceResponseKind\"];\n responseScope:\n SensorSpectralQuadrature[\"responseScope\"];\n responseScientificStatus:\n SensorSpectralQuadrature[\"responseScientificStatus\"];\n responseUncertainty:\n SensorSpectralQuadrature[\"responseUncertainty\"];\n responseChannelBinding?:\n SensorSpectralQuadrature[\"responseChannelBinding\"];\n responseReferenceConditions?:\n SensorSpectralQuadrature[\"responseReferenceConditions\"];\n wavelengthBasis:\n SensorSpectralQuadrature[\"wavelengthBasis\"];\n wavelengthBasisResolved: boolean;\n wavelengthRangeNanometers:\n SensorSpectralQuadrature[\"wavelengthRangeNanometers\"];\n responseDeclaredWavelengthRangeNanometers:\n SensorSpectralQuadrature[\"responseDeclaredWavelengthRangeNanometers\"];\n outputMeaning:\n \"pre-response-spatio-spectral-radiometric-reduction\";\n inputValueDomain: {\n kind: \"radiometric-spectral-irradiance\";\n unit: \"W/m^2/nm\";\n semantic:\n \"pre-aa-pre-response-sensor-plane-spectral-irradiance\";\n };\n spatialNodeCount: number;\n spectralNodeCount: number;\n combinedSampleCount: number;\n sourceValuesMatchedBy:\n \"spatial-node-identity-plus-spectral-sample-index-and-wavelength\";\n sourceValuesSuppliedForAllCombinedNodes: true;\n outsideImagingAreaSourceValuesRequired: boolean;\n geometricApertureAreaSquareMicrometers?: number;\n nominalSiteCellAreaSquareMicrometers?: number;\n geometricSensitiveAreaFractionOfLatticeCell?: number;\n perWavelength:\n readonly SensorSpatioSpectralWavelengthReduction[];\n /**\n * Integral over the requested wavelength interval of the spatially averaged\n * spectral irradiance. Units: W/m^2.\n */\n wavelengthIntegratedSpatialAverageIrradianceWattsPerSquareMeter:\n number;\n /**\n * Integral over the requested wavelength interval and the geometric\n * aperture area. Units: W.\n *\n * This is the incident radiant flux before the sensor response, over the\n * declared wavelength interval. It is not collected power or generated charge.\n */\n wavelengthIntegratedGeometricApertureIncidentFluxWatts:\n number;\n spatialIntegrationApplied: true;\n wavelengthIntegrationApplied: true;\n spectralResponsePlanUsedForWavelengthSupport: true;\n spectralResponseApplicationPerformed: false;\n responseScopeMatchedToSourcePlane: false;\n quantumEfficiencyApplied: false;\n spectralResponsivityApplied: false;\n channelFilterTransmissionApplied: false;\n opticalTransmissionAppliedByReducer: false;\n radiometricCollectionAreaEstablished: false;\n temporalIntegrationApplied: false;\n exposureDurationApplied: false;\n photonsCalculated: false;\n electronsCalculated: false;\n currentCalculated: false;\n shotNoiseApplied: false;\n readNoiseApplied: false;\n adcQuantizationApplied: false;\n rawCodeValueProduced: false;\n demosaicOrReconstructionApplied: false;\n convergenceErrorEstimated: false;\n componentEvidence: {\n spatial:\n SensorSpatialSamplingQuadrature[\"componentEvidence\"];\n spectral:\n SensorSpectralQuadrature[\"componentEvidence\"];\n };\n}\n\nfunction requireSafeNonNegativeInteger(\n value: unknown,\n path: string\n): number {\n if (\n typeof value !== \"number\" ||\n !Number.isSafeInteger(value) ||\n value < 0\n ) {\n throw new InvalidScientificInputError(\n path + \" must be a non-negative safe integer.\"\n );\n }\n return value;\n}\n\nfunction requireNonNegativeFinite(\n value: unknown,\n path: string\n): number {\n if (\n typeof value !== \"number\" ||\n !Number.isFinite(value) ||\n value < 0\n ) {\n throw new InvalidScientificInputError(\n path +\n \" must be finite and greater than or equal to zero.\"\n );\n }\n return value;\n}\n\nfunction sampleKey(\n spatialNode:\n SensorSpatialQuadratureNodeIdentity,\n spectralSampleIndex: number\n): string {\n return (\n sensorSpatialQuadratureNodeIdentityKey(\n spatialNode\n ) +\n \"|\" +\n spectralSampleIndex\n );\n}\n\nfunction parseSamples<T extends { node: SensorSpatioSpectralNodeIdentity }, V>(\n sampleValues:\n readonly T[],\n spatialNodesByKey: ReadonlyMap<\n string,\n SensorSpatialSamplingQuadrature[\"nodes\"][number]\n >,\n spectralNodesByIndex: ReadonlyMap<\n number,\n SensorSpectralQuadratureNode\n >,\n combinedSampleCount: number,\n valueForSample: (sample: T, sampleIndex: number) => V\n): Map<string, V> {\n if (!Array.isArray(sampleValues)) {\n throw new InvalidConfigurationError(\n \"sampleValues must be an array.\"\n );\n }\n if (\n sampleValues.length !==\n combinedSampleCount\n ) {\n throw new InvalidScientificInputError(\n \"sampleValues must contain exactly one value for every spatial × spectral quadrature node.\"\n );\n }\n\n const valuesByKey =\n new Map<string, V>();\n\n sampleValues.forEach(\n (sample, sampleIndex) => {\n if (\n typeof sample !== \"object\" ||\n sample === null ||\n typeof sample.node !== \"object\" ||\n sample.node === null ||\n typeof sample.node.spatialNode !==\n \"object\" ||\n sample.node.spatialNode === null\n ) {\n throw new InvalidScientificInputError(\n \"sampleValues[\" +\n sampleIndex +\n \"] must contain a complete spatio-spectral node identity.\"\n );\n }\n\n const spatialNode = {\n antiAliasingComponentIndex:\n requireSafeNonNegativeInteger(\n sample.node.spatialNode\n .antiAliasingComponentIndex,\n \"sampleValues[\" +\n sampleIndex +\n \"].node.spatialNode.antiAliasingComponentIndex\"\n ),\n apertureSampleXIndex:\n requireSafeNonNegativeInteger(\n sample.node.spatialNode\n .apertureSampleXIndex,\n \"sampleValues[\" +\n sampleIndex +\n \"].node.spatialNode.apertureSampleXIndex\"\n ),\n apertureSampleYIndex:\n requireSafeNonNegativeInteger(\n sample.node.spatialNode\n .apertureSampleYIndex,\n \"sampleValues[\" +\n sampleIndex +\n \"].node.spatialNode.apertureSampleYIndex\"\n )\n };\n const spatialKey =\n sensorSpatialQuadratureNodeIdentityKey(\n spatialNode\n );\n if (\n !spatialNodesByKey.has(\n spatialKey\n )\n ) {\n throw new InvalidScientificInputError(\n \"sampleValues identifies a spatial node that is not present in spatialQuadrature.\"\n );\n }\n\n const spectralSampleIndex =\n requireSafeNonNegativeInteger(\n sample.node.spectralSampleIndex,\n \"sampleValues[\" +\n sampleIndex +\n \"].node.spectralSampleIndex\"\n );\n const spectralNode =\n spectralNodesByIndex.get(\n spectralSampleIndex\n );\n if (spectralNode === undefined) {\n throw new InvalidScientificInputError(\n \"sampleValues identifies a spectral node that is not present in spectralQuadrature.\"\n );\n }\n\n const wavelengthNanometers =\n requireNonNegativeFinite(\n sample.node.wavelengthNanometers,\n \"sampleValues[\" +\n sampleIndex +\n \"].node.wavelengthNanometers\"\n );\n if (\n wavelengthNanometers !==\n spectralNode.wavelengthNanometers\n ) {\n throw new InvalidScientificInputError(\n \"sampleValues wavelengthNanometers must exactly match the identified spectral quadrature node.\"\n );\n }\n\n const key = sampleKey(\n spatialNode,\n spectralSampleIndex\n );\n if (valuesByKey.has(key)) {\n throw new InvalidScientificInputError(\n \"sampleValues contains duplicate spatio-spectral node identities.\"\n );\n }\n\n valuesByKey.set(\n key,\n valueForSample(sample, sampleIndex)\n );\n }\n );\n\n if (valuesByKey.size !== combinedSampleCount) {\n throw new InvalidScientificInputError(\"sampleValues must not contain sparse or missing node entries.\");\n }\n return valuesByKey;\n}\n\n/** Internal: matches samples to their spatial × spectral nodes. Each caller still validates values and applies its own equations. */\nexport function bindSensorSpatioSpectralSamples<T extends { node: SensorSpatioSpectralNodeIdentity }, V>(\n input: { spatialQuadrature: SensorSpatialSamplingQuadrature; spectralQuadrature: SensorSpectralQuadrature; sampleValues: readonly T[] },\n valueForSample: (sample: T, sampleIndex: number) => V\n): { spatialNodesByKey: Map<string, SensorSpatialSamplingQuadrature[\"nodes\"][number]>;\n spectralNodesByIndex: Map<number, SensorSpectralQuadratureNode>; colorSamplingProfileId: string;\n combinedSampleCount: number; valuesByKey: Map<string, V> } {\n const spatialNodesByKey =\n validateSensorSpatialSamplingQuadrature(\n input.spatialQuadrature\n );\n const spectralNodesByIndex =\n validateSensorSpectralQuadrature(\n input.spectralQuadrature\n );\n\n const colorSamplingProfileId =\n input.spatialQuadrature\n .colorSamplingProfileId;\n if (\n typeof colorSamplingProfileId !==\n \"string\" ||\n colorSamplingProfileId.trim().length ===\n 0\n ) {\n throw new InvalidScientificInputError(\n \"spatialQuadrature.colorSamplingProfileId is required for spatio-spectral composition.\"\n );\n }\n if (\n colorSamplingProfileId !==\n input.spectralQuadrature\n .colorSamplingProfileId\n ) {\n throw new InvalidScientificInputError(\n \"Spatial and spectral quadratures must reference the same colorSamplingProfileId.\"\n );\n }\n if (\n input.spatialQuadrature.channelId !==\n input.spectralQuadrature.channelId\n ) {\n throw new InvalidScientificInputError(\n \"Spatial and spectral quadratures must reference the same exact channelId.\"\n );\n }\n\n if (\n input.spatialQuadrature.outputMeaning !==\n \"pre-aa-optical-field-spatial-quadrature-nodes\" ||\n input.spatialQuadrature\n .spectralResponseIncluded !== false ||\n input.spatialQuadrature\n .temporalIntegrationIncluded !== false\n ) {\n throw new InvalidScientificInputError(\n \"spatialQuadrature semantics are incompatible with pre-response spatio-spectral reduction.\"\n );\n }\n\n const combinedSampleCount =\n spatialNodesByKey.size *\n spectralNodesByIndex.size;\n if (\n !Number.isSafeInteger(\n combinedSampleCount\n ) ||\n combinedSampleCount <= 0\n ) {\n throw new InvalidScientificInputError(\n \"Spatial × spectral quadrature sample count must be a positive safe integer.\"\n );\n }\n if (\n combinedSampleCount >\n MAX_SPATIO_SPECTRAL_SAMPLE_VALUES\n ) {\n throw new InvalidScientificInputError(\n \"Spatial × spectral quadrature sample count exceeds the safety limit of \" +\n MAX_SPATIO_SPECTRAL_SAMPLE_VALUES +\n \". Tile/batch the evaluation instead of materializing one oversized Cartesian product.\"\n );\n }\n\n const valuesByKey =\n parseSamples(\n input.sampleValues,\n spatialNodesByKey,\n spectralNodesByIndex,\n combinedSampleCount,\n valueForSample\n );\n\n return { spatialNodesByKey, spectralNodesByIndex, colorSamplingProfileId, combinedSampleCount, valuesByKey };\n}\n\n/**\n * Integrates supplied spectral irradiance on the sensor plane, E_lambda(x,y),\n * over every combination of an existing spatial quadrature and spectral\n * quadrature.\n *\n * Units: the density is in W/m^2/nm and wavelengthMeasureNanometers is dλ in\n * nm, so their product is in W/m^2. Do not convert dλ to metres unless the\n * density's per-nanometre unit is converted to match.\n *\n * Area integration uses the spatial quadrature's geometric area measures,\n * converted from square micrometres to square metres. The result is the\n * incident radiant flux on the geometric aperture only; geometric area is not\n * treated as an effective light-collection area.\n *\n * It deliberately stops before the sensor response. The spectral plan comes\n * from a specific response channel, so its wavelength range and breakpoints\n * fit that channel, but QE, A/W responsivity and colour-filter transmission are\n * not applied here. Matching the response's scope and source plane is a\n * separate later step.\n */\nexport function reduceSensorSpatioSpectralIrradiance(\n input:\n ReduceSensorSpatioSpectralIrradianceInput\n): CalculationResult<SensorSpatioSpectralIrradianceReduction> {\n requireInputKeys(input, \"reduceSensorSpatioSpectralIrradiance\", [\"spatialQuadrature\", \"spectralQuadrature\", \"sampleValues\"]);\n const { spatialNodesByKey, spectralNodesByIndex, colorSamplingProfileId, combinedSampleCount, valuesByKey } =\n bindSensorSpatioSpectralSamples(input, (sample, sampleIndex) => requireNonNegativeFinite(\n sample.spectralIrradianceWattsPerSquareMeterPerNanometer,\n \"sampleValues[\" + sampleIndex + \"].spectralIrradianceWattsPerSquareMeterPerNanometer\"\n ));\n\n const perWavelength:\n SensorSpatioSpectralWavelengthReduction[] =\n [];\n let wavelengthIntegratedSpatialAverage = 0;\n let wavelengthIntegratedGeometricFlux = 0;\n\n for (\n let spectralSampleIndex = 0;\n spectralSampleIndex <\n input.spectralQuadrature.totalNodeCount;\n spectralSampleIndex += 1\n ) {\n const spectralNode =\n spectralNodesByIndex.get(\n spectralSampleIndex\n );\n if (spectralNode === undefined) {\n throw new InvalidScientificInputError(\n \"spectralQuadrature is missing a validated contiguous spectral sample.\"\n );\n }\n\n let spatialAverageSpectralIrradiance = 0;\n let geometricSpectralFluxWattsPerNanometer =\n 0;\n\n spatialNodesByKey.forEach(\n (spatialNode, spatialKey) => {\n const value =\n valuesByKey.get(\n spatialKey +\n \"|\" +\n spectralSampleIndex\n );\n if (value === undefined) {\n throw new InvalidScientificInputError(\n \"A spatio-spectral quadrature node is missing its explicit source value.\"\n );\n }\n\n spatialAverageSpectralIrradiance +=\n value *\n spatialNode\n .combinedNormalizedSpatialWeight;\n geometricSpectralFluxWattsPerNanometer +=\n value *\n (\n spatialNode\n .combinedAreaMeasureSquareMicrometers *\n SQUARE_MICROMETERS_TO_SQUARE_METERS\n );\n\n if (\n !Number.isFinite(\n spatialAverageSpectralIrradiance\n ) ||\n !Number.isFinite(\n geometricSpectralFluxWattsPerNanometer\n )\n ) {\n throw new InvalidScientificInputError(\n \"Reduced spatio-spectral values must remain finite.\"\n );\n }\n }\n );\n\n const spatialAverageContribution =\n spatialAverageSpectralIrradiance *\n spectralNode\n .wavelengthMeasureNanometers;\n const geometricFluxContribution =\n geometricSpectralFluxWattsPerNanometer *\n spectralNode\n .wavelengthMeasureNanometers;\n\n wavelengthIntegratedSpatialAverage +=\n spatialAverageContribution;\n wavelengthIntegratedGeometricFlux +=\n geometricFluxContribution;\n\n if (\n !Number.isFinite(\n wavelengthIntegratedSpatialAverage\n ) ||\n !Number.isFinite(\n wavelengthIntegratedGeometricFlux\n )\n ) {\n throw new InvalidScientificInputError(\n \"Wavelength-integrated spatio-spectral values must remain finite.\"\n );\n }\n\n perWavelength.push({\n spectralSampleIndex,\n wavelengthNanometers:\n spectralNode.wavelengthNanometers,\n wavelengthMeasureNanometers:\n spectralNode\n .wavelengthMeasureNanometers,\n normalizedWavelengthWeight:\n spectralNode\n .normalizedWavelengthWeight,\n normalizedSpatialAverageSpectralIrradianceWattsPerSquareMeterPerNanometer:\n spatialAverageSpectralIrradiance,\n geometricApertureIncidentSpectralFluxWattsPerNanometer:\n geometricSpectralFluxWattsPerNanometer,\n wavelengthIntegratedSpatialAverageContributionWattsPerSquareMeter:\n spatialAverageContribution,\n wavelengthIntegratedGeometricApertureIncidentFluxContributionWatts:\n geometricFluxContribution\n });\n }\n\n return approximationResult(\n {\n colorSamplingProfileId,\n ...(input.spatialQuadrature\n .samplingApertureProfileId ===\n undefined\n ? {}\n : {\n samplingApertureProfileId:\n input.spatialQuadrature\n .samplingApertureProfileId\n }),\n ...(input.spatialQuadrature\n .opticalStackProfileId ===\n undefined\n ? {}\n : {\n opticalStackProfileId:\n input.spatialQuadrature\n .opticalStackProfileId\n }),\n site: {\n ...input.spatialQuadrature.site\n },\n channelId:\n input.spatialQuadrature.channelId,\n responseProfileId:\n input.spectralQuadrature.profileId,\n sourceResponseKind:\n input.spectralQuadrature\n .sourceResponseKind,\n responseScope:\n input.spectralQuadrature\n .responseScope,\n responseScientificStatus:\n input.spectralQuadrature\n .responseScientificStatus,\n responseUncertainty:\n input.spectralQuadrature\n .responseUncertainty,\n ...(input.spectralQuadrature\n .responseChannelBinding ===\n undefined\n ? {}\n : {\n responseChannelBinding:\n input.spectralQuadrature\n .responseChannelBinding\n }),\n ...(input.spectralQuadrature\n .responseReferenceConditions ===\n undefined\n ? {}\n : {\n responseReferenceConditions:\n input.spectralQuadrature\n .responseReferenceConditions\n }),\n wavelengthBasis:\n input.spectralQuadrature\n .wavelengthBasis,\n wavelengthBasisResolved:\n input.spectralQuadrature\n .wavelengthBasisResolved,\n wavelengthRangeNanometers: {\n ...input.spectralQuadrature\n .wavelengthRangeNanometers\n },\n responseDeclaredWavelengthRangeNanometers:\n {\n ...input.spectralQuadrature\n .responseDeclaredWavelengthRangeNanometers\n },\n outputMeaning:\n \"pre-response-spatio-spectral-radiometric-reduction\",\n inputValueDomain: {\n kind:\n \"radiometric-spectral-irradiance\",\n unit: \"W/m^2/nm\",\n semantic:\n \"pre-aa-pre-response-sensor-plane-spectral-irradiance\"\n },\n spatialNodeCount:\n input.spatialQuadrature\n .totalNodeCount,\n spectralNodeCount:\n input.spectralQuadrature\n .totalNodeCount,\n combinedSampleCount,\n sourceValuesMatchedBy:\n \"spatial-node-identity-plus-spectral-sample-index-and-wavelength\",\n sourceValuesSuppliedForAllCombinedNodes:\n true,\n outsideImagingAreaSourceValuesRequired:\n input.spatialQuadrature\n .preAntiAliasingSourceOutsideImagingAreaNodeCount >\n 0,\n geometricApertureAreaSquareMicrometers:\n input.spatialQuadrature\n .geometricApertureAreaSquareMicrometers,\n ...(input.spatialQuadrature\n .nominalSiteCellAreaSquareMicrometers ===\n undefined\n ? {}\n : {\n nominalSiteCellAreaSquareMicrometers:\n input.spatialQuadrature\n .nominalSiteCellAreaSquareMicrometers\n }),\n ...(input.spatialQuadrature\n .geometricSensitiveAreaFractionOfLatticeCell ===\n undefined\n ? {}\n : {\n geometricSensitiveAreaFractionOfLatticeCell:\n input.spatialQuadrature\n .geometricSensitiveAreaFractionOfLatticeCell\n }),\n perWavelength,\n wavelengthIntegratedSpatialAverageIrradianceWattsPerSquareMeter:\n wavelengthIntegratedSpatialAverage,\n wavelengthIntegratedGeometricApertureIncidentFluxWatts:\n wavelengthIntegratedGeometricFlux,\n spatialIntegrationApplied: true,\n wavelengthIntegrationApplied: true,\n spectralResponsePlanUsedForWavelengthSupport:\n true,\n spectralResponseApplicationPerformed:\n false,\n responseScopeMatchedToSourcePlane:\n false,\n quantumEfficiencyApplied: false,\n spectralResponsivityApplied: false,\n channelFilterTransmissionApplied:\n false,\n opticalTransmissionAppliedByReducer:\n false,\n radiometricCollectionAreaEstablished:\n false,\n temporalIntegrationApplied: false,\n exposureDurationApplied: false,\n photonsCalculated: false,\n electronsCalculated: false,\n currentCalculated: false,\n shotNoiseApplied: false,\n readNoiseApplied: false,\n adcQuantizationApplied: false,\n rawCodeValueProduced: false,\n demosaicOrReconstructionApplied:\n false,\n convergenceErrorEstimated: false,\n componentEvidence: {\n spatial:\n input.spatialQuadrature\n .componentEvidence,\n spectral:\n input.spectralQuadrature\n .componentEvidence\n }\n },\n \"sensor-spatio-spectral-irradiance-reduction\",\n \"1.0.0\",\n [\n \"Input values are nonnegative sensor-plane spectral irradiance E_lambda(x,y) in W/m^2/nm, evaluated at every spatial pre-AA source coordinate and spectral quadrature wavelength.\",\n \"Spatial and spectral plans must share the exact colorSamplingProfileId and channelId; a matching channel label alone is insufficient linkage.\",\n \"The full Cartesian product is explicit and capped at 100,000 supplied sample values to avoid multiplicative memory blowups.\",\n \"Spatial normalized weights form the per-wavelength average; geometric square-micrometre area measures are converted to square metres for incident spectral flux density.\",\n \"Physical wavelength integration multiplies W/m^2/nm or W/nm by d-lambda in nm. No extra 1e-9 factor is applied because the spectral density is already per nanometre.\",\n \"The spatial AA kernel is the existing normalized throughput-free spatial redistribution; this reducer adds no omitted optical-stack or microlens throughput.\",\n \"The spectral quadrature's response-derived support and breakpoints are consumed, but QE, A/W responsivity and channel-filter transmission are not applied.\",\n \"Response scope is preserved for downstream work but is not matched to a declared source plane in this version.\",\n \"Geometric aperture area is not promoted to effective radiometric collection area.\",\n \"Temporal exposure integration, photon/electron conversion, electrical current, noise, saturation, ADC/RAW conversion and reconstruction remain separate future work.\",\n \"No source-independent spatio-spectral convergence/error estimate is reported; downstream integrand structure controls numerical error.\"\n ]\n );\n}\n"]}
1
+ {"version":3,"file":"spatio-spectral-reduction.js","sourceRoot":"","sources":["../../src/sensor/spatio-spectral-reduction.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Spatio-spectral reduction: integrates spectral irradiance on the sensor over one site's area and\n * over wavelength, giving the incident light power on the site's geometric aperture.\n *\n * It stops before the sensor response: QE (quantum efficiency), A/W responsivity and colour-filter\n * transmission are applied in later steps.\n * @see docs/MOTION_AND_SIGNAL.md for equations, coordinate/unit conventions, blockers and support\n * limits.\n */\n\nimport {\n approximationResult,\n type CalculationResult\n} from \"../core/calculation-result.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport {\n sensorSpatialQuadratureNodeIdentityKey,\n validateSensorSpatialSamplingQuadrature,\n type SensorSpatialQuadratureNodeIdentity\n} from \"./spatial-sample-reduction.js\";\nimport type {\n SensorSpatialSamplingQuadrature\n} from \"./spatial-sampling-quadrature.js\";\nimport {\n validateSensorSpectralQuadrature,\n type SensorSpectralQuadrature,\n type SensorSpectralQuadratureNode\n} from \"./spectral-quadrature.js\";\nimport { requireInputKeys, requireInputNonNegativeFinite as requireNonNegativeFinite, requireInputNonNegativeSafeInteger as requireSafeNonNegativeInteger } from \"../core/record-validation.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\n\nconst SQUARE_MICROMETERS_TO_SQUARE_METERS = 1e-12;\nconst MAX_SPATIO_SPECTRAL_SAMPLE_VALUES = 100_000;\n\nexport interface SensorSpatioSpectralNodeIdentity {\n spatialNode: SensorSpatialQuadratureNodeIdentity;\n spectralSampleIndex: number;\n wavelengthNanometers: number;\n}\n\nexport interface SensorSpatioSpectralIrradianceSample {\n node: SensorSpatioSpectralNodeIdentity;\n /**\n * Spectral irradiance at the spatial node's preAntiAliasingSourcePointMm\n * and this spectral node's wavelength.\n *\n * Units: W/m^2/nm.\n */\n spectralIrradianceWattsPerSquareMeterPerNanometer: number;\n}\n\nexport interface ReduceSensorSpatioSpectralIrradianceInput {\n spatialQuadrature: SensorSpatialSamplingQuadrature;\n spectralQuadrature: SensorSpectralQuadrature;\n /**\n * Exactly one E_lambda(x,y) value, with its node identity, for every\n * combination of spatial node and spectral node.\n *\n * Order does not matter; the node identity does.\n */\n sampleValues: readonly SensorSpatioSpectralIrradianceSample[];\n}\n\nexport interface SensorSpatioSpectralWavelengthReduction {\n spectralSampleIndex: number;\n wavelengthNanometers: number;\n wavelengthMeasureNanometers: number;\n normalizedWavelengthWeight: number;\n /**\n * Spatially reduced spectral irradiance at this wavelength.\n * Units: W/m^2/nm.\n */\n normalizedSpatialAverageSpectralIrradianceWattsPerSquareMeterPerNanometer:\n number;\n /**\n * Spectral radiant flux reaching the geometric sensitive aperture, after the\n * declared normalized AA spreading. Units: W/nm.\n *\n * This uses geometric area, not an established light-collection area.\n */\n geometricApertureIncidentSpectralFluxWattsPerNanometer: number;\n /**\n * Contribution to the wavelength-integrated spatial average.\n * Units: W/m^2.\n */\n wavelengthIntegratedSpatialAverageContributionWattsPerSquareMeter: number;\n /**\n * Contribution to wavelength-integrated geometric-aperture incident flux.\n * Units: W.\n */\n wavelengthIntegratedGeometricApertureIncidentFluxContributionWatts: number;\n}\n\nexport interface SensorSpatioSpectralIrradianceReduction {\n colorSamplingProfileId: string;\n samplingApertureProfileId?: string;\n opticalStackProfileId?: string;\n site: {\n x: number;\n y: number;\n };\n channelId: string;\n responseProfileId: string;\n sourceResponseKind:\n SensorSpectralQuadrature[\"sourceResponseKind\"];\n responseScope:\n SensorSpectralQuadrature[\"responseScope\"];\n responseScientificStatus:\n SensorSpectralQuadrature[\"responseScientificStatus\"];\n responseUncertainty:\n SensorSpectralQuadrature[\"responseUncertainty\"];\n responseChannelBinding?:\n SensorSpectralQuadrature[\"responseChannelBinding\"];\n responseReferenceConditions?:\n SensorSpectralQuadrature[\"responseReferenceConditions\"];\n wavelengthBasis:\n SensorSpectralQuadrature[\"wavelengthBasis\"];\n wavelengthBasisResolved: boolean;\n wavelengthRangeNanometers:\n SensorSpectralQuadrature[\"wavelengthRangeNanometers\"];\n responseDeclaredWavelengthRangeNanometers:\n SensorSpectralQuadrature[\"responseDeclaredWavelengthRangeNanometers\"];\n outputMeaning:\n \"pre-response-spatio-spectral-radiometric-reduction\";\n inputValueDomain: {\n kind: \"radiometric-spectral-irradiance\";\n unit: \"W/m^2/nm\";\n semantic:\n \"pre-aa-pre-response-sensor-plane-spectral-irradiance\";\n };\n spatialNodeCount: number;\n spectralNodeCount: number;\n combinedSampleCount: number;\n sourceValuesMatchedBy:\n \"spatial-node-identity-plus-spectral-sample-index-and-wavelength\";\n sourceValuesSuppliedForAllCombinedNodes: true;\n outsideImagingAreaSourceValuesRequired: boolean;\n geometricApertureAreaSquareMicrometers?: number;\n nominalSiteCellAreaSquareMicrometers?: number;\n geometricSensitiveAreaFractionOfLatticeCell?: number;\n perWavelength:\n readonly SensorSpatioSpectralWavelengthReduction[];\n /**\n * Integral over the requested wavelength interval of the spatially averaged\n * spectral irradiance. Units: W/m^2.\n */\n wavelengthIntegratedSpatialAverageIrradianceWattsPerSquareMeter:\n number;\n /**\n * Integral over the requested wavelength interval and the geometric\n * aperture area. Units: W.\n *\n * This is the incident radiant flux before the sensor response, over the\n * declared wavelength interval. It is not collected power or generated charge.\n */\n wavelengthIntegratedGeometricApertureIncidentFluxWatts:\n number;\n spatialIntegrationApplied: true;\n wavelengthIntegrationApplied: true;\n spectralResponsePlanUsedForWavelengthSupport: true;\n spectralResponseApplicationPerformed: false;\n responseScopeMatchedToSourcePlane: false;\n quantumEfficiencyApplied: false;\n spectralResponsivityApplied: false;\n channelFilterTransmissionApplied: false;\n opticalTransmissionAppliedByReducer: false;\n radiometricCollectionAreaEstablished: false;\n temporalIntegrationApplied: false;\n exposureDurationApplied: false;\n photonsCalculated: false;\n electronsCalculated: false;\n currentCalculated: false;\n shotNoiseApplied: false;\n readNoiseApplied: false;\n adcQuantizationApplied: false;\n rawCodeValueProduced: false;\n demosaicOrReconstructionApplied: false;\n convergenceErrorEstimated: false;\n componentEvidence: {\n spatial:\n SensorSpatialSamplingQuadrature[\"componentEvidence\"];\n spectral:\n SensorSpectralQuadrature[\"componentEvidence\"];\n };\n}\n\nfunction sampleKey(\n spatialNode:\n SensorSpatialQuadratureNodeIdentity,\n spectralSampleIndex: number\n): string {\n return (\n sensorSpatialQuadratureNodeIdentityKey(\n spatialNode\n ) +\n \"|\" +\n spectralSampleIndex\n );\n}\n\nfunction parseSamples<T extends { node: SensorSpatioSpectralNodeIdentity }, V>(\n sampleValues:\n readonly T[],\n spatialNodesByKey: ReadonlyMap<\n string,\n SensorSpatialSamplingQuadrature[\"nodes\"][number]\n >,\n spectralNodesByIndex: ReadonlyMap<\n number,\n SensorSpectralQuadratureNode\n >,\n combinedSampleCount: number,\n valueForSample: (sample: T, sampleIndex: number) => V\n): Map<string, V> {\n if (!Array.isArray(sampleValues)) {\n throw new InvalidConfigurationError(\n \"sampleValues must be an array.\"\n );\n }\n if (\n sampleValues.length !==\n combinedSampleCount\n ) {\n throw new InvalidScientificInputError(\n \"sampleValues must contain exactly one value for every spatial × spectral quadrature node.\"\n );\n }\n\n const valuesByKey =\n new Map<string, V>();\n\n sampleValues.forEach(\n (sample, sampleIndex) => {\n if (\n typeof sample !== \"object\" ||\n sample === null ||\n typeof sample.node !== \"object\" ||\n sample.node === null ||\n typeof sample.node.spatialNode !==\n \"object\" ||\n sample.node.spatialNode === null\n ) {\n throw new InvalidScientificInputError(\n \"sampleValues[\" +\n sampleIndex +\n \"] must contain a complete spatio-spectral node identity.\"\n );\n }\n\n const spatialNode = {\n antiAliasingComponentIndex:\n requireSafeNonNegativeInteger(\n sample.node.spatialNode\n .antiAliasingComponentIndex,\n \"sampleValues[\" +\n sampleIndex +\n \"].node.spatialNode.antiAliasingComponentIndex\"\n ),\n apertureSampleXIndex:\n requireSafeNonNegativeInteger(\n sample.node.spatialNode\n .apertureSampleXIndex,\n \"sampleValues[\" +\n sampleIndex +\n \"].node.spatialNode.apertureSampleXIndex\"\n ),\n apertureSampleYIndex:\n requireSafeNonNegativeInteger(\n sample.node.spatialNode\n .apertureSampleYIndex,\n \"sampleValues[\" +\n sampleIndex +\n \"].node.spatialNode.apertureSampleYIndex\"\n )\n };\n const spatialKey =\n sensorSpatialQuadratureNodeIdentityKey(\n spatialNode\n );\n if (\n !spatialNodesByKey.has(\n spatialKey\n )\n ) {\n throw new InvalidScientificInputError(\n \"sampleValues identifies a spatial node that is not present in spatialQuadrature.\"\n );\n }\n\n const spectralSampleIndex =\n requireSafeNonNegativeInteger(\n sample.node.spectralSampleIndex,\n \"sampleValues[\" +\n sampleIndex +\n \"].node.spectralSampleIndex\"\n );\n const spectralNode =\n spectralNodesByIndex.get(\n spectralSampleIndex\n );\n if (spectralNode === undefined) {\n throw new InvalidScientificInputError(\n \"sampleValues identifies a spectral node that is not present in spectralQuadrature.\"\n );\n }\n\n const wavelengthNanometers =\n requireNonNegativeFinite(\n sample.node.wavelengthNanometers,\n \"sampleValues[\" +\n sampleIndex +\n \"].node.wavelengthNanometers\"\n );\n if (\n wavelengthNanometers !==\n spectralNode.wavelengthNanometers\n ) {\n throw new InvalidScientificInputError(\n \"sampleValues wavelengthNanometers must exactly match the identified spectral quadrature node.\"\n );\n }\n\n const key = sampleKey(\n spatialNode,\n spectralSampleIndex\n );\n if (valuesByKey.has(key)) {\n throw new InvalidScientificInputError(\n \"sampleValues contains duplicate spatio-spectral node identities.\"\n );\n }\n\n valuesByKey.set(\n key,\n valueForSample(sample, sampleIndex)\n );\n }\n );\n\n if (valuesByKey.size !== combinedSampleCount) {\n throw new InvalidScientificInputError(\"sampleValues must not contain sparse or missing node entries.\");\n }\n return valuesByKey;\n}\n\n/** Internal: matches samples to their spatial × spectral nodes. Each caller still validates values and applies its own equations. */\nexport function bindSensorSpatioSpectralSamples<T extends { node: SensorSpatioSpectralNodeIdentity }, V>(\n input: { spatialQuadrature: SensorSpatialSamplingQuadrature; spectralQuadrature: SensorSpectralQuadrature; sampleValues: readonly T[] },\n valueForSample: (sample: T, sampleIndex: number) => V\n): { spatialNodesByKey: Map<string, SensorSpatialSamplingQuadrature[\"nodes\"][number]>;\n spectralNodesByIndex: Map<number, SensorSpectralQuadratureNode>; colorSamplingProfileId: string;\n combinedSampleCount: number; valuesByKey: Map<string, V> } {\n const spatialNodesByKey =\n validateSensorSpatialSamplingQuadrature(\n input.spatialQuadrature\n );\n const spectralNodesByIndex =\n validateSensorSpectralQuadrature(\n input.spectralQuadrature\n );\n\n const colorSamplingProfileId =\n input.spatialQuadrature\n .colorSamplingProfileId;\n if (\n typeof colorSamplingProfileId !==\n \"string\" ||\n colorSamplingProfileId.trim().length ===\n 0\n ) {\n throw new InvalidScientificInputError(\n \"spatialQuadrature.colorSamplingProfileId is required for spatio-spectral composition.\"\n );\n }\n if (\n colorSamplingProfileId !==\n input.spectralQuadrature\n .colorSamplingProfileId\n ) {\n throw new InvalidScientificInputError(\n \"Spatial and spectral quadratures must reference the same colorSamplingProfileId.\"\n );\n }\n if (\n input.spatialQuadrature.channelId !==\n input.spectralQuadrature.channelId\n ) {\n throw new InvalidScientificInputError(\n \"Spatial and spectral quadratures must reference the same exact channelId.\"\n );\n }\n\n if (\n input.spatialQuadrature.outputMeaning !==\n \"pre-aa-optical-field-spatial-quadrature-nodes\" ||\n input.spatialQuadrature\n .spectralResponseIncluded !== false ||\n input.spatialQuadrature\n .temporalIntegrationIncluded !== false\n ) {\n throw new InvalidScientificInputError(\n \"spatialQuadrature semantics are incompatible with pre-response spatio-spectral reduction.\"\n );\n }\n\n const combinedSampleCount =\n spatialNodesByKey.size *\n spectralNodesByIndex.size;\n if (\n !Number.isSafeInteger(\n combinedSampleCount\n ) ||\n combinedSampleCount <= 0\n ) {\n throw new InvalidScientificInputError(\n \"Spatial × spectral quadrature sample count must be a positive safe integer.\"\n );\n }\n if (\n combinedSampleCount >\n MAX_SPATIO_SPECTRAL_SAMPLE_VALUES\n ) {\n throw new InvalidScientificInputError(\n \"Spatial × spectral quadrature sample count exceeds the safety limit of \" +\n MAX_SPATIO_SPECTRAL_SAMPLE_VALUES +\n \". Tile/batch the evaluation instead of materializing one oversized Cartesian product.\"\n );\n }\n\n const valuesByKey =\n parseSamples(\n input.sampleValues,\n spatialNodesByKey,\n spectralNodesByIndex,\n combinedSampleCount,\n valueForSample\n );\n\n return { spatialNodesByKey, spectralNodesByIndex, colorSamplingProfileId, combinedSampleCount, valuesByKey };\n}\n\n/**\n * Integrates supplied spectral irradiance on the sensor plane, E_lambda(x,y),\n * over every combination of an existing spatial quadrature and spectral\n * quadrature.\n *\n * Units: the density is in W/m^2/nm and wavelengthMeasureNanometers is dλ in\n * nm, so their product is in W/m^2. Do not convert dλ to metres unless the\n * density's per-nanometre unit is converted to match.\n *\n * Area integration uses the spatial quadrature's geometric area measures,\n * converted from square micrometres to square metres. The result is the\n * incident radiant flux on the geometric aperture only; geometric area is not\n * treated as an effective light-collection area.\n *\n * It deliberately stops before the sensor response. The spectral plan comes\n * from a specific response channel, so its wavelength range and breakpoints\n * fit that channel, but QE, A/W responsivity and colour-filter transmission are\n * not applied here. Matching the response's scope and source plane is a\n * separate later step.\n */\nexport function reduceSensorSpatioSpectralIrradiance(\n input:\n ReduceSensorSpatioSpectralIrradianceInput\n): CalculationResult<SensorSpatioSpectralIrradianceReduction> {\n requireInputKeys(input, \"reduceSensorSpatioSpectralIrradiance\", [\"spatialQuadrature\", \"spectralQuadrature\", \"sampleValues\"]);\n const { spatialNodesByKey, spectralNodesByIndex, colorSamplingProfileId, combinedSampleCount, valuesByKey } =\n bindSensorSpatioSpectralSamples(input, (sample, sampleIndex) => requireNonNegativeFinite(\n sample.spectralIrradianceWattsPerSquareMeterPerNanometer,\n \"sampleValues[\" + sampleIndex + \"].spectralIrradianceWattsPerSquareMeterPerNanometer\"\n ));\n\n const perWavelength:\n SensorSpatioSpectralWavelengthReduction[] =\n [];\n let wavelengthIntegratedSpatialAverage = 0;\n let wavelengthIntegratedGeometricFlux = 0;\n\n for (\n let spectralSampleIndex = 0;\n spectralSampleIndex <\n input.spectralQuadrature.totalNodeCount;\n spectralSampleIndex += 1\n ) {\n const spectralNode =\n spectralNodesByIndex.get(\n spectralSampleIndex\n );\n if (spectralNode === undefined) {\n throw new InvalidScientificInputError(\n \"spectralQuadrature is missing a validated contiguous spectral sample.\"\n );\n }\n\n let spatialAverageSpectralIrradiance = 0;\n let geometricSpectralFluxWattsPerNanometer =\n 0;\n\n spatialNodesByKey.forEach(\n (spatialNode, spatialKey) => {\n const value =\n valuesByKey.get(\n spatialKey +\n \"|\" +\n spectralSampleIndex\n );\n if (value === undefined) {\n throw new InvalidScientificInputError(\n \"A spatio-spectral quadrature node is missing its explicit source value.\"\n );\n }\n\n spatialAverageSpectralIrradiance +=\n value *\n spatialNode\n .combinedNormalizedSpatialWeight;\n geometricSpectralFluxWattsPerNanometer +=\n value *\n (\n spatialNode\n .combinedAreaMeasureSquareMicrometers *\n SQUARE_MICROMETERS_TO_SQUARE_METERS\n );\n\n if (\n !Number.isFinite(\n spatialAverageSpectralIrradiance\n ) ||\n !Number.isFinite(\n geometricSpectralFluxWattsPerNanometer\n )\n ) {\n throw new InvalidScientificInputError(\n \"Reduced spatio-spectral values must remain finite.\"\n );\n }\n }\n );\n\n const spatialAverageContribution =\n spatialAverageSpectralIrradiance *\n spectralNode\n .wavelengthMeasureNanometers;\n const geometricFluxContribution =\n geometricSpectralFluxWattsPerNanometer *\n spectralNode\n .wavelengthMeasureNanometers;\n\n wavelengthIntegratedSpatialAverage +=\n spatialAverageContribution;\n wavelengthIntegratedGeometricFlux +=\n geometricFluxContribution;\n\n if (\n !Number.isFinite(\n wavelengthIntegratedSpatialAverage\n ) ||\n !Number.isFinite(\n wavelengthIntegratedGeometricFlux\n )\n ) {\n throw new InvalidScientificInputError(\n \"Wavelength-integrated spatio-spectral values must remain finite.\"\n );\n }\n\n perWavelength.push({\n spectralSampleIndex,\n wavelengthNanometers:\n spectralNode.wavelengthNanometers,\n wavelengthMeasureNanometers:\n spectralNode\n .wavelengthMeasureNanometers,\n normalizedWavelengthWeight:\n spectralNode\n .normalizedWavelengthWeight,\n normalizedSpatialAverageSpectralIrradianceWattsPerSquareMeterPerNanometer:\n spatialAverageSpectralIrradiance,\n geometricApertureIncidentSpectralFluxWattsPerNanometer:\n geometricSpectralFluxWattsPerNanometer,\n wavelengthIntegratedSpatialAverageContributionWattsPerSquareMeter:\n spatialAverageContribution,\n wavelengthIntegratedGeometricApertureIncidentFluxContributionWatts:\n geometricFluxContribution\n });\n }\n\n return approximationResult(\n {\n colorSamplingProfileId,\n ...(input.spatialQuadrature\n .samplingApertureProfileId ===\n undefined\n ? {}\n : {\n samplingApertureProfileId:\n input.spatialQuadrature\n .samplingApertureProfileId\n }),\n ...(input.spatialQuadrature\n .opticalStackProfileId ===\n undefined\n ? {}\n : {\n opticalStackProfileId:\n input.spatialQuadrature\n .opticalStackProfileId\n }),\n site: {\n ...input.spatialQuadrature.site\n },\n channelId:\n input.spatialQuadrature.channelId,\n responseProfileId:\n input.spectralQuadrature.profileId,\n sourceResponseKind:\n input.spectralQuadrature\n .sourceResponseKind,\n responseScope:\n input.spectralQuadrature\n .responseScope,\n responseScientificStatus:\n input.spectralQuadrature\n .responseScientificStatus,\n responseUncertainty:\n input.spectralQuadrature\n .responseUncertainty,\n ...(input.spectralQuadrature\n .responseChannelBinding ===\n undefined\n ? {}\n : {\n responseChannelBinding:\n input.spectralQuadrature\n .responseChannelBinding\n }),\n ...(input.spectralQuadrature\n .responseReferenceConditions ===\n undefined\n ? {}\n : {\n responseReferenceConditions:\n input.spectralQuadrature\n .responseReferenceConditions\n }),\n wavelengthBasis:\n input.spectralQuadrature\n .wavelengthBasis,\n wavelengthBasisResolved:\n input.spectralQuadrature\n .wavelengthBasisResolved,\n wavelengthRangeNanometers: {\n ...input.spectralQuadrature\n .wavelengthRangeNanometers\n },\n responseDeclaredWavelengthRangeNanometers:\n {\n ...input.spectralQuadrature\n .responseDeclaredWavelengthRangeNanometers\n },\n outputMeaning:\n \"pre-response-spatio-spectral-radiometric-reduction\",\n inputValueDomain: {\n kind:\n \"radiometric-spectral-irradiance\",\n unit: \"W/m^2/nm\",\n semantic:\n \"pre-aa-pre-response-sensor-plane-spectral-irradiance\"\n },\n spatialNodeCount:\n input.spatialQuadrature\n .totalNodeCount,\n spectralNodeCount:\n input.spectralQuadrature\n .totalNodeCount,\n combinedSampleCount,\n sourceValuesMatchedBy:\n \"spatial-node-identity-plus-spectral-sample-index-and-wavelength\",\n sourceValuesSuppliedForAllCombinedNodes:\n true,\n outsideImagingAreaSourceValuesRequired:\n input.spatialQuadrature\n .preAntiAliasingSourceOutsideImagingAreaNodeCount >\n 0,\n geometricApertureAreaSquareMicrometers:\n input.spatialQuadrature\n .geometricApertureAreaSquareMicrometers,\n ...(input.spatialQuadrature\n .nominalSiteCellAreaSquareMicrometers ===\n undefined\n ? {}\n : {\n nominalSiteCellAreaSquareMicrometers:\n input.spatialQuadrature\n .nominalSiteCellAreaSquareMicrometers\n }),\n ...(input.spatialQuadrature\n .geometricSensitiveAreaFractionOfLatticeCell ===\n undefined\n ? {}\n : {\n geometricSensitiveAreaFractionOfLatticeCell:\n input.spatialQuadrature\n .geometricSensitiveAreaFractionOfLatticeCell\n }),\n perWavelength,\n wavelengthIntegratedSpatialAverageIrradianceWattsPerSquareMeter:\n wavelengthIntegratedSpatialAverage,\n wavelengthIntegratedGeometricApertureIncidentFluxWatts:\n wavelengthIntegratedGeometricFlux,\n spatialIntegrationApplied: true,\n wavelengthIntegrationApplied: true,\n spectralResponsePlanUsedForWavelengthSupport:\n true,\n spectralResponseApplicationPerformed:\n false,\n responseScopeMatchedToSourcePlane:\n false,\n quantumEfficiencyApplied: false,\n spectralResponsivityApplied: false,\n channelFilterTransmissionApplied:\n false,\n opticalTransmissionAppliedByReducer:\n false,\n radiometricCollectionAreaEstablished:\n false,\n temporalIntegrationApplied: false,\n exposureDurationApplied: false,\n photonsCalculated: false,\n electronsCalculated: false,\n currentCalculated: false,\n shotNoiseApplied: false,\n readNoiseApplied: false,\n adcQuantizationApplied: false,\n rawCodeValueProduced: false,\n demosaicOrReconstructionApplied:\n false,\n convergenceErrorEstimated: false,\n componentEvidence: {\n spatial:\n input.spatialQuadrature\n .componentEvidence,\n spectral:\n input.spectralQuadrature\n .componentEvidence\n }\n },\n \"sensor-spatio-spectral-irradiance-reduction\",\n \"1.0.0\",\n [\n \"Input values are nonnegative sensor-plane spectral irradiance E_lambda(x,y) in W/m^2/nm, evaluated at every spatial pre-AA source coordinate and spectral quadrature wavelength.\",\n \"Spatial and spectral plans must share the exact colorSamplingProfileId and channelId; a matching channel label alone is insufficient linkage.\",\n \"The full Cartesian product is explicit and capped at 100,000 supplied sample values to avoid multiplicative memory blowups.\",\n \"Spatial normalized weights form the per-wavelength average; geometric square-micrometre area measures are converted to square metres for incident spectral flux density.\",\n \"Physical wavelength integration multiplies W/m^2/nm or W/nm by d-lambda in nm. No extra 1e-9 factor is applied because the spectral density is already per nanometre.\",\n \"The spatial AA kernel is the existing normalized throughput-free spatial redistribution; this reducer adds no omitted optical-stack or microlens throughput.\",\n \"The spectral quadrature's response-derived support and breakpoints are consumed, but QE, A/W responsivity and channel-filter transmission are not applied.\",\n \"Response scope is preserved for downstream work but is not matched to a declared source plane in this version.\",\n \"Geometric aperture area is not promoted to effective radiometric collection area.\",\n \"Temporal exposure integration, photon/electron conversion, electrical current, noise, saturation, ADC/RAW conversion and reconstruction remain separate future work.\",\n \"No source-independent spatio-spectral convergence/error estimate is reported; downstream integrand structure controls numerical error.\"\n ]\n );\n}\n"]}
@@ -5,12 +5,20 @@
5
5
  * limits.
6
6
  */
7
7
  import type { SpectralCoverageParticipant } from "../core/spectral-composition.js";
8
- import { type SensorSpectralResponseProfile } from "./spectral-response.js";
8
+ import { type SensorSpectralChannelResponse, type SensorSpectralResponseProfile } from "./spectral-response.js";
9
9
  export interface CreateSensorSpectralCoverageParticipantInput {
10
10
  spectralResponseProfile: SensorSpectralResponseProfile;
11
11
  channelId: string;
12
12
  participantId?: string;
13
13
  }
14
+ type ResponseCurve = Extract<SensorSpectralChannelResponse, {
15
+ kind: "effective-external-quantum-efficiency";
16
+ }>["externalQuantumEfficiency"] | Extract<SensorSpectralChannelResponse, {
17
+ kind: "effective-spectral-responsivity";
18
+ }>["spectralResponsivity"] | Extract<SensorSpectralChannelResponse, {
19
+ kind: "separable-channel-filter-and-detector-eqe";
20
+ }>["channelFilterTransmittance"];
21
+ export declare function responseCurves(response: SensorSpectralChannelResponse): readonly ResponseCurve[];
14
22
  /**
15
23
  * Converts one parsed sensor response channel into the shared continuous
16
24
  * spectral-coverage format.
@@ -19,3 +27,4 @@ export interface CreateSensorSpectralCoverageParticipantInput {
19
27
  * apply QE or A/W responsivity, and says nothing about scene or optics coverage.
20
28
  */
21
29
  export declare function createSensorSpectralCoverageParticipant(input: CreateSensorSpectralCoverageParticipantInput): SpectralCoverageParticipant;
30
+ export {};
@@ -3,7 +3,7 @@ import { InvalidScientificInputError } from "../core/validation.js";
3
3
  import { parseSensorSpectralResponseProfile } from "./spectral-response.js";
4
4
  import { requireInputKeys } from "../core/record-validation.js";
5
5
  import { InvalidConfigurationError } from "../core/configuration-error.js";
6
- function responseCurves(response) {
6
+ export function responseCurves(response) {
7
7
  if (response.kind ===
8
8
  "effective-external-quantum-efficiency") {
9
9
  return [
@@ -1 +1 @@
1
- {"version":3,"file":"spectral-coverage.js","sourceRoot":"","sources":["../../src/sensor/spectral-coverage.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Describes a sensor response channel's wavelength coverage in the shared spectral-coverage format\n * (core/spectral-composition), so it can be intersected with the light source and optics.\n * @see docs/MOTION_AND_SIGNAL.md for equations, coordinate/unit conventions, blockers and support\n * limits.\n */\n\nimport type {\n SpectralCoverageParticipant\n} from \"../core/spectral-composition.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport {\n parseSensorSpectralResponseProfile,\n type SensorSpectralChannelResponse,\n type SensorSpectralResponseProfile\n} from \"./spectral-response.js\";\nimport { requireInputKeys } from \"../core/record-validation.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\n\nexport interface CreateSensorSpectralCoverageParticipantInput {\n spectralResponseProfile:\n SensorSpectralResponseProfile;\n channelId: string;\n participantId?: string;\n}\n\ntype ResponseCurve =\n | Extract<\n SensorSpectralChannelResponse,\n {\n kind:\n \"effective-external-quantum-efficiency\";\n }\n >[\"externalQuantumEfficiency\"]\n | Extract<\n SensorSpectralChannelResponse,\n {\n kind:\n \"effective-spectral-responsivity\";\n }\n >[\"spectralResponsivity\"]\n | Extract<\n SensorSpectralChannelResponse,\n {\n kind:\n \"separable-channel-filter-and-detector-eqe\";\n }\n >[\"channelFilterTransmittance\"];\n\nfunction responseCurves(\n response:\n SensorSpectralChannelResponse\n): readonly ResponseCurve[] {\n if (\n response.kind ===\n \"effective-external-quantum-efficiency\"\n ) {\n return [\n response.externalQuantumEfficiency\n ];\n }\n if (\n response.kind ===\n \"effective-spectral-responsivity\"\n ) {\n return [\n response.spectralResponsivity\n ];\n }\n return [\n response.channelFilterTransmittance,\n response.detectorExternalQuantumEfficiency\n ];\n}\n\n/**\n * Converts one parsed sensor response channel into the shared continuous\n * spectral-coverage format.\n *\n * It gives only the wavelength range and interpolation breakpoints. It does not\n * apply QE or A/W responsivity, and says nothing about scene or optics coverage.\n */\nexport function createSensorSpectralCoverageParticipant(\n input:\n CreateSensorSpectralCoverageParticipantInput\n): SpectralCoverageParticipant {\n requireInputKeys(input, \"createSensorSpectralCoverageParticipant\", [\"spectralResponseProfile\", \"channelId\", \"participantId\"]);\n const profile =\n parseSensorSpectralResponseProfile(\n input.spectralResponseProfile\n );\n\n if (\n typeof input.channelId !==\n \"string\" ||\n input.channelId.trim().length === 0\n ) {\n throw new InvalidConfigurationError(\n \"channelId must be a non-empty string.\"\n );\n }\n\n const response =\n profile.channels.find(\n (entry) =>\n entry.channelId ===\n input.channelId\n );\n if (response === undefined) {\n throw new InvalidScientificInputError(\n \"No spectral response is declared for the requested channelId.\"\n );\n }\n\n const curves =\n responseCurves(response);\n const wavelengthBasis =\n curves[0]!.wavelengthBasis;\n if (wavelengthBasis === \"unspecified\") {\n throw new InvalidScientificInputError(\n \"Sensor spectral coverage requires a resolved air or vacuum wavelength basis.\"\n );\n }\n if (\n curves.some(\n (curve) =>\n curve.wavelengthBasis !==\n wavelengthBasis\n )\n ) {\n throw new InvalidScientificInputError(\n \"Sensor response component curves must share one wavelengthBasis before coverage composition.\"\n );\n }\n\n const minimum = Math.max(\n ...curves.map(\n (curve) =>\n curve.samples[0]!\n .wavelengthNanometers\n )\n );\n const maximum = Math.min(\n ...curves.map(\n (curve) =>\n curve.samples[\n curve.samples.length - 1\n ]!.wavelengthNanometers\n )\n );\n if (!(minimum < maximum)) {\n throw new InvalidScientificInputError(\n \"Sensor response component curves do not share a non-empty wavelength interval.\"\n );\n }\n\n const breakpoints =\n new Set<number>();\n for (const curve of curves) {\n for (const sample of curve.samples) {\n if (\n sample.wavelengthNanometers >\n minimum &&\n sample.wavelengthNanometers <\n maximum\n ) {\n breakpoints.add(\n sample.wavelengthNanometers\n );\n }\n }\n }\n\n const participantId =\n input.participantId ??\n \"sensor-response:\" +\n profile.profileId +\n \":\" +\n input.channelId;\n\n if (\n participantId.trim().length === 0\n ) {\n throw new InvalidConfigurationError(\n \"participantId must be a non-empty string when supplied.\"\n );\n }\n\n return {\n participantId,\n role: \"sensor-response\",\n wavelengthBasis,\n wavelengthRangeNanometers: {\n minimum,\n maximum\n },\n breakpointsNanometers:\n [...breakpoints].sort(\n (a, b) => a - b\n )\n };\n}\n"]}
1
+ {"version":3,"file":"spectral-coverage.js","sourceRoot":"","sources":["../../src/sensor/spectral-coverage.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Describes a sensor response channel's wavelength coverage in the shared spectral-coverage format\n * (core/spectral-composition), so it can be intersected with the light source and optics.\n * @see docs/MOTION_AND_SIGNAL.md for equations, coordinate/unit conventions, blockers and support\n * limits.\n */\n\nimport type {\n SpectralCoverageParticipant\n} from \"../core/spectral-composition.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport {\n parseSensorSpectralResponseProfile,\n type SensorSpectralChannelResponse,\n type SensorSpectralResponseProfile\n} from \"./spectral-response.js\";\nimport { requireInputKeys } from \"../core/record-validation.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\n\nexport interface CreateSensorSpectralCoverageParticipantInput {\n spectralResponseProfile:\n SensorSpectralResponseProfile;\n channelId: string;\n participantId?: string;\n}\n\ntype ResponseCurve =\n | Extract<\n SensorSpectralChannelResponse,\n {\n kind:\n \"effective-external-quantum-efficiency\";\n }\n >[\"externalQuantumEfficiency\"]\n | Extract<\n SensorSpectralChannelResponse,\n {\n kind:\n \"effective-spectral-responsivity\";\n }\n >[\"spectralResponsivity\"]\n | Extract<\n SensorSpectralChannelResponse,\n {\n kind:\n \"separable-channel-filter-and-detector-eqe\";\n }\n >[\"channelFilterTransmittance\"];\n\nexport function responseCurves(\n response:\n SensorSpectralChannelResponse\n): readonly ResponseCurve[] {\n if (\n response.kind ===\n \"effective-external-quantum-efficiency\"\n ) {\n return [\n response.externalQuantumEfficiency\n ];\n }\n if (\n response.kind ===\n \"effective-spectral-responsivity\"\n ) {\n return [\n response.spectralResponsivity\n ];\n }\n return [\n response.channelFilterTransmittance,\n response.detectorExternalQuantumEfficiency\n ];\n}\n\n/**\n * Converts one parsed sensor response channel into the shared continuous\n * spectral-coverage format.\n *\n * It gives only the wavelength range and interpolation breakpoints. It does not\n * apply QE or A/W responsivity, and says nothing about scene or optics coverage.\n */\nexport function createSensorSpectralCoverageParticipant(\n input:\n CreateSensorSpectralCoverageParticipantInput\n): SpectralCoverageParticipant {\n requireInputKeys(input, \"createSensorSpectralCoverageParticipant\", [\"spectralResponseProfile\", \"channelId\", \"participantId\"]);\n const profile =\n parseSensorSpectralResponseProfile(\n input.spectralResponseProfile\n );\n\n if (\n typeof input.channelId !==\n \"string\" ||\n input.channelId.trim().length === 0\n ) {\n throw new InvalidConfigurationError(\n \"channelId must be a non-empty string.\"\n );\n }\n\n const response =\n profile.channels.find(\n (entry) =>\n entry.channelId ===\n input.channelId\n );\n if (response === undefined) {\n throw new InvalidScientificInputError(\n \"No spectral response is declared for the requested channelId.\"\n );\n }\n\n const curves =\n responseCurves(response);\n const wavelengthBasis =\n curves[0]!.wavelengthBasis;\n if (wavelengthBasis === \"unspecified\") {\n throw new InvalidScientificInputError(\n \"Sensor spectral coverage requires a resolved air or vacuum wavelength basis.\"\n );\n }\n if (\n curves.some(\n (curve) =>\n curve.wavelengthBasis !==\n wavelengthBasis\n )\n ) {\n throw new InvalidScientificInputError(\n \"Sensor response component curves must share one wavelengthBasis before coverage composition.\"\n );\n }\n\n const minimum = Math.max(\n ...curves.map(\n (curve) =>\n curve.samples[0]!\n .wavelengthNanometers\n )\n );\n const maximum = Math.min(\n ...curves.map(\n (curve) =>\n curve.samples[\n curve.samples.length - 1\n ]!.wavelengthNanometers\n )\n );\n if (!(minimum < maximum)) {\n throw new InvalidScientificInputError(\n \"Sensor response component curves do not share a non-empty wavelength interval.\"\n );\n }\n\n const breakpoints =\n new Set<number>();\n for (const curve of curves) {\n for (const sample of curve.samples) {\n if (\n sample.wavelengthNanometers >\n minimum &&\n sample.wavelengthNanometers <\n maximum\n ) {\n breakpoints.add(\n sample.wavelengthNanometers\n );\n }\n }\n }\n\n const participantId =\n input.participantId ??\n \"sensor-response:\" +\n profile.profileId +\n \":\" +\n input.channelId;\n\n if (\n participantId.trim().length === 0\n ) {\n throw new InvalidConfigurationError(\n \"participantId must be a non-empty string when supplied.\"\n );\n }\n\n return {\n participantId,\n role: \"sensor-response\",\n wavelengthBasis,\n wavelengthRangeNanometers: {\n minimum,\n maximum\n },\n breakpointsNanometers:\n [...breakpoints].sort(\n (a, b) => a - b\n )\n };\n}\n"]}
@@ -14,17 +14,11 @@ import { approximationResult } from "../core/calculation-result.js";
14
14
  import { InvalidScientificInputError } from "../core/validation.js";
15
15
  import { parseSensorColorSamplingProfile } from "./color-sampling.js";
16
16
  import { createSensorSpectralResponseChannelBinding, parseSensorSpectralResponseProfile, resolveSensorSpectralResponseAtWavelength } from "./spectral-response.js";
17
- import { requireInputKeys } from "../core/record-validation.js";
17
+ import { requireInputKeys, requireInputPositiveFinite as requirePositiveFinite, requireInputNonNegativeSafeInteger as requireNonNegativeSafeInteger } from "../core/record-validation.js";
18
18
  import { InvalidConfigurationError } from "../core/configuration-error.js";
19
+ import { responseCurves } from "./spectral-coverage.js";
20
+ import { approximatelyEqual } from "../core/shared-helpers.js";
19
21
  const MAX_SPECTRAL_QUADRATURE_NODES = 100_000;
20
- function requirePositiveFinite(value, path) {
21
- if (typeof value !== "number" ||
22
- !Number.isFinite(value) ||
23
- value <= 0) {
24
- throw new InvalidScientificInputError(path + " must be finite and greater than zero.");
25
- }
26
- return value;
27
- }
28
22
  function channelResponse(profile, channelId) {
29
23
  if (typeof channelId !== "string" ||
30
24
  channelId.trim().length === 0) {
@@ -36,24 +30,6 @@ function channelResponse(profile, channelId) {
36
30
  }
37
31
  return response;
38
32
  }
39
- function responseCurves(response) {
40
- if (response.kind ===
41
- "effective-external-quantum-efficiency") {
42
- return [
43
- response.externalQuantumEfficiency
44
- ];
45
- }
46
- if (response.kind ===
47
- "effective-spectral-responsivity") {
48
- return [
49
- response.spectralResponsivity
50
- ];
51
- }
52
- return [
53
- response.channelFilterTransmittance,
54
- response.detectorExternalQuantumEfficiency
55
- ];
56
- }
57
33
  function usableResponseRange(curves) {
58
34
  const minimum = Math.max(...curves.map((curve) => curve.samples[0]
59
35
  .wavelengthNanometers));
@@ -167,20 +143,6 @@ function planSegments(boundaries, maximumSubintervalWidthNanometers) {
167
143
  totalNodeCount
168
144
  };
169
145
  }
170
- const SPECTRAL_VALIDATION_TOLERANCE = 1e-10;
171
- function requireNonNegativeSafeInteger(value, path) {
172
- if (typeof value !== "number" ||
173
- !Number.isSafeInteger(value) ||
174
- value < 0) {
175
- throw new InvalidScientificInputError(path + " must be a non-negative safe integer.");
176
- }
177
- return value;
178
- }
179
- function approximatelyEqual(actual, expected, tolerance = SPECTRAL_VALIDATION_TOLERANCE) {
180
- const scale = Math.max(1, Math.abs(actual), Math.abs(expected));
181
- return (Math.abs(actual - expected) <=
182
- tolerance * scale);
183
- }
184
146
  /**
185
147
  * Rechecks a supplied wavelength plan (node identities, coverage and widths in nanometres) before it
186
148
  * is used.