@photivra/engine 2.18.0 → 2.19.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 (261) hide show
  1. package/CHANGELOG.md +68 -1
  2. package/CITATION.cff +1 -1
  3. package/README.md +6 -6
  4. package/SECURITY.md +1 -1
  5. package/dist/capture/focus-control.js +2 -75
  6. package/dist/capture/focus-control.js.map +1 -1
  7. package/dist/capture/native-environment-lean.d.ts +1 -0
  8. package/dist/capture/native-environment-lean.js +1 -1
  9. package/dist/capture/native-environment-lean.js.map +1 -1
  10. package/dist/capture/native-environment-optical-stage.d.ts +1 -0
  11. package/dist/capture/native-environment-optical-stage.js +2 -5
  12. package/dist/capture/native-environment-optical-stage.js.map +1 -1
  13. package/dist/capture/native-environment-raw.js +1 -5
  14. package/dist/capture/native-environment-raw.js.map +1 -1
  15. package/dist/capture/release-sequence.js +2 -47
  16. package/dist/capture/release-sequence.js.map +1 -1
  17. package/dist/color/white-balance.d.ts +2 -0
  18. package/dist/color/white-balance.js +3 -56
  19. package/dist/color/white-balance.js.map +1 -1
  20. package/dist/composition/image-formation-plan.d.ts +1 -0
  21. package/dist/composition/image-formation-plan.js +3 -49
  22. package/dist/composition/image-formation-plan.js.map +1 -1
  23. package/dist/core/evidence-provenance.js +1 -16
  24. package/dist/core/evidence-provenance.js.map +1 -1
  25. package/dist/core/record-validation.d.ts +54 -0
  26. package/dist/core/record-validation.js +176 -0
  27. package/dist/core/record-validation.js.map +1 -1
  28. package/dist/core/scientific-assurance.js +1 -18
  29. package/dist/core/scientific-assurance.js.map +1 -1
  30. package/dist/core/shared-helpers.d.ts +33 -0
  31. package/dist/core/shared-helpers.js +34 -0
  32. package/dist/core/shared-helpers.js.map +1 -0
  33. package/dist/core/spectral-composition.js +1 -36
  34. package/dist/core/spectral-composition.js.map +1 -1
  35. package/dist/core/spectral.js +1 -19
  36. package/dist/core/spectral.js.map +1 -1
  37. package/dist/core/version.d.ts +1 -1
  38. package/dist/core/version.js +1 -1
  39. package/dist/core/version.js.map +1 -1
  40. package/dist/equipment/exposure-capabilities.d.ts +1 -0
  41. package/dist/equipment/exposure-capabilities.js +2 -36
  42. package/dist/equipment/exposure-capabilities.js.map +1 -1
  43. package/dist/equipment/iso-capabilities.js +2 -49
  44. package/dist/equipment/iso-capabilities.js.map +1 -1
  45. package/dist/equipment/metering-capabilities.js +1 -26
  46. package/dist/equipment/metering-capabilities.js.map +1 -1
  47. package/dist/equipment/release-capabilities.js +1 -53
  48. package/dist/equipment/release-capabilities.js.map +1 -1
  49. package/dist/exposure/auto-iso-slowest-shutter.js +1 -7
  50. package/dist/exposure/auto-iso-slowest-shutter.js.map +1 -1
  51. package/dist/exposure/exposure-duration-control.js +1 -16
  52. package/dist/exposure/exposure-duration-control.js.map +1 -1
  53. package/dist/exposure/exposure-mode-resolver.js +1 -17
  54. package/dist/exposure/exposure-mode-resolver.js.map +1 -1
  55. package/dist/exposure/filtered-exposure-time.d.ts +93 -0
  56. package/dist/exposure/filtered-exposure-time.js +129 -0
  57. package/dist/exposure/filtered-exposure-time.js.map +1 -0
  58. package/dist/exposure/flash-ttl.d.ts +1 -0
  59. package/dist/exposure/flash-ttl.js +1 -1
  60. package/dist/exposure/flash-ttl.js.map +1 -1
  61. package/dist/exposure/flash.js +1 -49
  62. package/dist/exposure/flash.js.map +1 -1
  63. package/dist/exposure/metering-scene-radiance.js +1 -25
  64. package/dist/exposure/metering-scene-radiance.js.map +1 -1
  65. package/dist/exposure/metering-target.js +1 -30
  66. package/dist/exposure/metering-target.js.map +1 -1
  67. package/dist/exposure/metering-temporal.js +1 -8
  68. package/dist/exposure/metering-temporal.js.map +1 -1
  69. package/dist/exposure/metering.js +1 -72
  70. package/dist/exposure/metering.js.map +1 -1
  71. package/dist/exposure/program-line.js +1 -45
  72. package/dist/exposure/program-line.js.map +1 -1
  73. package/dist/facade/develop-joined-parts.d.ts +29 -0
  74. package/dist/facade/develop-joined-parts.js +33 -0
  75. package/dist/facade/develop-joined-parts.js.map +1 -0
  76. package/dist/facade/generic-lens-capture.d.ts +4 -2
  77. package/dist/facade/generic-lens-capture.js +4 -2
  78. package/dist/facade/generic-lens-capture.js.map +1 -1
  79. package/dist/facade/simulate-capture.d.ts +37 -5
  80. package/dist/facade/simulate-capture.js +153 -96
  81. package/dist/facade/simulate-capture.js.map +1 -1
  82. package/dist/gpu/chart-composite-gpu.js +1 -3
  83. package/dist/gpu/chart-composite-gpu.js.map +1 -1
  84. package/dist/gpu/chart-radiance-gpu.js +1 -5
  85. package/dist/gpu/chart-radiance-gpu.js.map +1 -1
  86. package/dist/gpu/display-view-quality-gpu.d.ts +13 -1
  87. package/dist/gpu/display-view-quality-gpu.js +4 -8
  88. package/dist/gpu/display-view-quality-gpu.js.map +1 -1
  89. package/dist/gpu/display-view-speed-gpu.js +3 -25
  90. package/dist/gpu/display-view-speed-gpu.js.map +1 -1
  91. package/dist/gpu/sensor-stage-gpu.js +1 -5
  92. package/dist/gpu/sensor-stage-gpu.js.map +1 -1
  93. package/dist/gpu/webgpu-runtime.d.ts +2 -0
  94. package/dist/gpu/webgpu-runtime.js +3 -0
  95. package/dist/gpu/webgpu-runtime.js.map +1 -1
  96. package/dist/index.d.ts +4 -1
  97. package/dist/index.js +3 -0
  98. package/dist/index.js.map +1 -1
  99. package/dist/motion/camera-rotation.js +1 -6
  100. package/dist/motion/camera-rotation.js.map +1 -1
  101. package/dist/motion/capture-rotation-inverse-mapping.d.ts +12 -0
  102. package/dist/motion/capture-rotation-inverse-mapping.js +2 -2
  103. package/dist/motion/capture-rotation-inverse-mapping.js.map +1 -1
  104. package/dist/motion/capture-rotation-temporal-quadrature.d.ts +1 -0
  105. package/dist/motion/capture-rotation-temporal-quadrature.js +1 -1
  106. package/dist/motion/capture-rotation-temporal-quadrature.js.map +1 -1
  107. package/dist/motion/capture-rotation-trajectory.js +2 -17
  108. package/dist/motion/capture-rotation-trajectory.js.map +1 -1
  109. package/dist/motion/capture-translation-parallax-temporal-quadrature.d.ts +1 -0
  110. package/dist/motion/capture-translation-parallax-temporal-quadrature.js +3 -14
  111. package/dist/motion/capture-translation-parallax-temporal-quadrature.js.map +1 -1
  112. package/dist/motion/extended-object-projection.js +5 -40
  113. package/dist/motion/extended-object-projection.js.map +1 -1
  114. package/dist/optics/atmospheric-haze.js +1 -7
  115. package/dist/optics/atmospheric-haze.js.map +1 -1
  116. package/dist/optics/brown-conrady-distortion.js +1 -7
  117. package/dist/optics/brown-conrady-distortion.js.map +1 -1
  118. package/dist/optics/complex-pupil-psf.d.ts +1 -0
  119. package/dist/optics/complex-pupil-psf.js +2 -84
  120. package/dist/optics/complex-pupil-psf.js.map +1 -1
  121. package/dist/optics/focus-state.js +1 -11
  122. package/dist/optics/focus-state.js.map +1 -1
  123. package/dist/optics/front-of-lens-filter.d.ts +5 -0
  124. package/dist/optics/front-of-lens-filter.js +3 -65
  125. package/dist/optics/front-of-lens-filter.js.map +1 -1
  126. package/dist/optics/generic-nd-filter-presets.d.ts +12 -0
  127. package/dist/optics/generic-nd-filter-presets.js +48 -0
  128. package/dist/optics/generic-nd-filter-presets.js.map +1 -0
  129. package/dist/optics/illumination-vignetting.js +1 -6
  130. package/dist/optics/illumination-vignetting.js.map +1 -1
  131. package/dist/optics/lens-psf-profile.d.ts +4 -1
  132. package/dist/optics/lens-psf-profile.js +12 -111
  133. package/dist/optics/lens-psf-profile.js.map +1 -1
  134. package/dist/optics/longitudinal-chromatic-aberration.js +1 -7
  135. package/dist/optics/longitudinal-chromatic-aberration.js.map +1 -1
  136. package/dist/optics/pupil-vignetting.js +1 -7
  137. package/dist/optics/pupil-vignetting.js.map +1 -1
  138. package/dist/optics/radial-distortion.js +1 -6
  139. package/dist/optics/radial-distortion.js.map +1 -1
  140. package/dist/optics/scene-to-sensor-irradiance.js +4 -96
  141. package/dist/optics/scene-to-sensor-irradiance.js.map +1 -1
  142. package/dist/optics/stray-light.d.ts +1 -0
  143. package/dist/optics/stray-light.js +1 -1
  144. package/dist/optics/stray-light.js.map +1 -1
  145. package/dist/optics/zernike-wavefront.js +1 -7
  146. package/dist/optics/zernike-wavefront.js.map +1 -1
  147. package/dist/output/capture-geometry.js +1 -8
  148. package/dist/output/capture-geometry.js.map +1 -1
  149. package/dist/output/developed-lens-corrections.d.ts +62 -0
  150. package/dist/output/developed-lens-corrections.js +135 -0
  151. package/dist/output/developed-lens-corrections.js.map +1 -0
  152. package/dist/output/generic-lens-corrections.d.ts +2 -6
  153. package/dist/output/generic-lens-corrections.js +3 -6
  154. package/dist/output/generic-lens-corrections.js.map +1 -1
  155. package/dist/output/geometric-transforms.js +1 -4
  156. package/dist/output/geometric-transforms.js.map +1 -1
  157. package/dist/output/native-capture-sdr.d.ts +2 -2
  158. package/dist/output/native-capture-sdr.js +3 -2
  159. package/dist/output/native-capture-sdr.js.map +1 -1
  160. package/dist/processing/development-detail.d.ts +29 -0
  161. package/dist/processing/development-detail.js +128 -107
  162. package/dist/processing/development-detail.js.map +1 -1
  163. package/dist/schema/illumination-temporal.js +3 -72
  164. package/dist/schema/illumination-temporal.js.map +1 -1
  165. package/dist/schema/illumination.d.ts +1 -0
  166. package/dist/schema/illumination.js +2 -65
  167. package/dist/schema/illumination.js.map +1 -1
  168. package/dist/schema/scene-radiance-temporal.js +2 -14
  169. package/dist/schema/scene-radiance-temporal.js.map +1 -1
  170. package/dist/schema/scene-radiance.js +2 -60
  171. package/dist/schema/scene-radiance.js.map +1 -1
  172. package/dist/schema/validation.d.ts +2 -0
  173. package/dist/schema/validation.js +2 -17
  174. package/dist/schema/validation.js.map +1 -1
  175. package/dist/sensor/accumulated-charge.js +2 -59
  176. package/dist/sensor/accumulated-charge.js.map +1 -1
  177. package/dist/sensor/architecture.js +1 -16
  178. package/dist/sensor/architecture.js.map +1 -1
  179. package/dist/sensor/camera-saturation-capacity.d.ts +1 -0
  180. package/dist/sensor/camera-saturation-capacity.js +3 -55
  181. package/dist/sensor/camera-saturation-capacity.js.map +1 -1
  182. package/dist/sensor/capture-color-sampling-binding.js +1 -26
  183. package/dist/sensor/capture-color-sampling-binding.js.map +1 -1
  184. package/dist/sensor/capture-mode-timing.js +1 -39
  185. package/dist/sensor/capture-mode-timing.js.map +1 -1
  186. package/dist/sensor/capture-mode.js +1 -23
  187. package/dist/sensor/capture-mode.js.map +1 -1
  188. package/dist/sensor/color-sampling.js +1 -26
  189. package/dist/sensor/color-sampling.js.map +1 -1
  190. package/dist/sensor/constant-rate-temporal-integration.js +2 -46
  191. package/dist/sensor/constant-rate-temporal-integration.js.map +1 -1
  192. package/dist/sensor/dark-current.js +2 -45
  193. package/dist/sensor/dark-current.js.map +1 -1
  194. package/dist/sensor/eqe-electron-rate.js +2 -35
  195. package/dist/sensor/eqe-electron-rate.js.map +1 -1
  196. package/dist/sensor/exposure-window.d.ts +5 -0
  197. package/dist/sensor/exposure-window.js +7 -22
  198. package/dist/sensor/exposure-window.js.map +1 -1
  199. package/dist/sensor/iso-gain.js +1 -7
  200. package/dist/sensor/iso-gain.js.map +1 -1
  201. package/dist/sensor/iso-signal-chain.js +1 -43
  202. package/dist/sensor/iso-signal-chain.js.map +1 -1
  203. package/dist/sensor/optical-stack.js +1 -25
  204. package/dist/sensor/optical-stack.js.map +1 -1
  205. package/dist/sensor/photon-energy.js +1 -26
  206. package/dist/sensor/photon-energy.js.map +1 -1
  207. package/dist/sensor/physical-charge-capacity.js +3 -77
  208. package/dist/sensor/physical-charge-capacity.js.map +1 -1
  209. package/dist/sensor/radiometry-readiness.js +2 -23
  210. package/dist/sensor/radiometry-readiness.js.map +1 -1
  211. package/dist/sensor/raw-readout.d.ts +3 -0
  212. package/dist/sensor/raw-readout.js +4 -66
  213. package/dist/sensor/raw-readout.js.map +1 -1
  214. package/dist/sensor/raw-reconstruction.js +2 -64
  215. package/dist/sensor/raw-reconstruction.js.map +1 -1
  216. package/dist/sensor/readout-exposure-linkage.js +1 -9
  217. package/dist/sensor/readout-exposure-linkage.js.map +1 -1
  218. package/dist/sensor/readout-timing.js +3 -57
  219. package/dist/sensor/readout-timing.js.map +1 -1
  220. package/dist/sensor/response-application-compatibility.d.ts +2 -0
  221. package/dist/sensor/response-application-compatibility.js +3 -29
  222. package/dist/sensor/response-application-compatibility.js.map +1 -1
  223. package/dist/sensor/response-operating-range.d.ts +1 -0
  224. package/dist/sensor/response-operating-range.js +3 -79
  225. package/dist/sensor/response-operating-range.js.map +1 -1
  226. package/dist/sensor/responsivity-photocurrent.js +4 -108
  227. package/dist/sensor/responsivity-photocurrent.js.map +1 -1
  228. package/dist/sensor/sampling-aperture.js +1 -25
  229. package/dist/sensor/sampling-aperture.js.map +1 -1
  230. package/dist/sensor/spatial-sample-reduction.js +2 -19
  231. package/dist/sensor/spatial-sample-reduction.js.map +1 -1
  232. package/dist/sensor/spatial-sampling-quadrature.js +1 -3
  233. package/dist/sensor/spatial-sampling-quadrature.js.map +1 -1
  234. package/dist/sensor/spatio-spectral-reduction.js +1 -18
  235. package/dist/sensor/spatio-spectral-reduction.js.map +1 -1
  236. package/dist/sensor/spectral-coverage.d.ts +10 -1
  237. package/dist/sensor/spectral-coverage.js +1 -1
  238. package/dist/sensor/spectral-coverage.js.map +1 -1
  239. package/dist/sensor/spectral-quadrature.js +2 -35
  240. package/dist/sensor/spectral-quadrature.js.map +1 -1
  241. package/dist/sensor/spectral-response.js +1 -47
  242. package/dist/sensor/spectral-response.js.map +1 -1
  243. package/dist/stabilization/camera-shake.js +1 -6
  244. package/dist/stabilization/camera-shake.js.map +1 -1
  245. package/dist/stabilization/system.js +2 -80
  246. package/dist/stabilization/system.js.map +1 -1
  247. package/docs/AGENT_GUIDE.md +1 -0
  248. package/docs/API_REFERENCE.md +41 -2
  249. package/docs/QUICK_START.md +90 -8
  250. package/docs/USAGE.md +47 -2
  251. package/docs/api/core-version.md +1 -1
  252. package/docs/api/exports.json +53 -3
  253. package/docs/api/exposure-filtered-exposure-time.md +105 -0
  254. package/docs/api/facade-develop-joined-parts.md +32 -0
  255. package/docs/api/facade-simulate-capture.md +36 -5
  256. package/docs/api/optics-generic-nd-filter-presets.md +34 -0
  257. package/docs/api/optics-lens-psf-profile.md +3 -1
  258. package/docs/api/output-developed-lens-corrections.md +12 -0
  259. package/docs/api/output-generic-lens-corrections.md +0 -9
  260. package/llms.txt +1 -0
  261. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"spectral-response.js","sourceRoot":"","sources":["../../src/sensor/spectral-response.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Sensor spectral response: how strongly each sensor channel responds at each wavelength, as an EQE\n * (external quantum efficiency) curve, an A/W responsivity curve, or a colour-filter curve times a\n * detector EQE curve.\n *\n * Responses are never guessed from channel names (such as \"red\") or the CFA type, and are never\n * extrapolated outside the sampled range. A numeric curve can be embedded only with evidence that the\n * data is reusable or owned by Photivra; a public reference without reuse permission can support\n * metadata but does not by itself allow copying its numbers into the engine.\n * @see docs/MOTION_AND_SIGNAL.md for equations, coordinate/unit conventions, blockers and support\n * limits.\n */\n\nimport {\n calculatedResult,\n type CalculationResult\n} from \"../core/calculation-result.js\";\nimport { stringifyCanonicalJson } from \"../core/canonical-json.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport {\n parseEvidenceList,\n type EvidenceProvenance\n} from \"../core/evidence-provenance.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport {\n parseSpectralWavelengthBasis,\n type SpectralWavelengthBasis,\n type SpectralWavelengthSample\n} from \"../core/spectral.js\";\nexport type { SpectralWavelengthBasis } from \"../core/spectral.js\";\nimport {\n parseSensorColorSamplingProfile,\n type SensorColorSamplingProfile\n} from \"./color-sampling.js\";\nimport { requireInputKeys, rejectUnknownFields, rejectUnknownVariantFields, UNCERTAINTY_VARIANT_FIELDS } from \"../core/record-validation.js\";\n\ntype UnknownRecord = Record<string, unknown>;\n\nexport type SensorSpectralResponseScientificStatus =\n | \"calibrated\"\n | \"approximation\";\n\nexport type SensorSpectralResponseUncertainty =\n | {\n kind: \"relative\";\n fraction: number;\n basis: string;\n }\n | {\n kind: \"not-quantified\";\n limitation: string;\n };\n\nexport interface SensorSpectralReferenceConditions {\n temperatureC?: number;\n incidenceAngleDegreesFromNormal?: number;\n polarization?:\n | \"unpolarized\"\n | \"unspecified\";\n}\n\nexport interface SpectralFractionSample\n extends SpectralWavelengthSample {\n value: number;\n}\n\nexport interface SpectralResponsivitySample\n extends SpectralWavelengthSample {\n amperesPerWatt: number;\n}\n\ninterface SpectralCurveBase {\n wavelengthUnit: \"nm\";\n wavelengthBasis: SpectralWavelengthBasis;\n interpolation: \"piecewise-linear\";\n outsideRangeBehavior: \"fail-closed\";\n evidence: readonly EvidenceProvenance[];\n}\n\nexport interface SpectralFractionCurve\n extends SpectralCurveBase {\n samples: readonly SpectralFractionSample[];\n}\n\nexport interface SpectralResponsivityCurve\n extends SpectralCurveBase {\n samples: readonly SpectralResponsivitySample[];\n}\n\ninterface SensorSpectralChannelResponseBase {\n channelId: string;\n scientificStatus:\n SensorSpectralResponseScientificStatus;\n uncertainty:\n SensorSpectralResponseUncertainty;\n evidence: readonly EvidenceProvenance[];\n referenceConditions?:\n SensorSpectralReferenceConditions;\n conditionDependence:\n \"not-modeled\";\n}\n\nexport type EffectiveSensorResponseScope =\n | \"site-incident-effective-channel-response\"\n | \"sensor-package-incident-effective-channel-response\";\n\nexport type SensorSpectralChannelResponse =\n | (SensorSpectralChannelResponseBase & {\n kind:\n \"effective-external-quantum-efficiency\";\n responseScope:\n EffectiveSensorResponseScope;\n externalQuantumEfficiency:\n SpectralFractionCurve;\n })\n | (SensorSpectralChannelResponseBase & {\n kind:\n \"effective-spectral-responsivity\";\n responseScope:\n EffectiveSensorResponseScope;\n spectralResponsivity:\n SpectralResponsivityCurve;\n })\n | (SensorSpectralChannelResponseBase & {\n kind:\n \"separable-channel-filter-and-detector-eqe\";\n responseScope:\n \"site-incident-channel-filter-times-detector-eqe\";\n combinationRule: \"multiply\";\n channelFilterTransmittance:\n SpectralFractionCurve;\n detectorExternalQuantumEfficiency:\n SpectralFractionCurve;\n });\n\n/**\n * Current channel-binding format. v2 (package 2.0.0) serializes the parsed channel with sorted keys.\n * v1 (package 1.5.0 and earlier) used the unparsed channel's JSON in insertion order; it is rejected\n * with an explicit version error instead of being compared.\n */\nexport const SENSOR_SPECTRAL_RESPONSE_CHANNEL_BINDING_FORMAT =\n \"photivra-spectral-channel-response-canonical-json-v2\" as const;\n\nexport interface SensorSpectralResponseChannelBinding {\n format:\n typeof SENSOR_SPECTRAL_RESPONSE_CHANNEL_BINDING_FORMAT;\n canonicalJson: string;\n}\n\n/**\n * Versioned response data for the channels of one exact colour-sampling profile.\n * Each channel declares an effective EQE, an effective A/W response, or separate\n * filter and detector EQE curves, with its sampled range, interpolation and an\n * explicit wavelength basis (air or vacuum). Reference conditions, evidence and\n * uncertainty each keep their own validity. Names such as red, green or blue\n * never fill in a missing curve or justify extrapolation.\n */\nexport interface SensorSpectralResponseProfile {\n schemaVersion: \"0.1.0\";\n profileId: string;\n colorSamplingProfileId: string;\n evidence: readonly EvidenceProvenance[];\n channels:\n readonly SensorSpectralChannelResponse[];\n}\n\nexport interface ResolveSensorSpectralResponseAtWavelengthInput {\n colorSamplingProfile:\n SensorColorSamplingProfile;\n spectralResponseProfile:\n SensorSpectralResponseProfile;\n channelId: string;\n wavelengthNanometers: number;\n wavelengthBasis: SpectralWavelengthBasis;\n}\n\nexport type ResolvedSensorSpectralResponseValue =\n | {\n kind:\n \"effective-external-quantum-efficiency\";\n externalQuantumEfficiency: number;\n composition:\n | \"direct-effective-response\"\n | \"channel-filter-transmittance-times-detector-eqe\";\n channelFilterTransmittance?: number;\n detectorExternalQuantumEfficiency?: number;\n }\n | {\n kind:\n \"effective-spectral-responsivity\";\n amperesPerWatt: number;\n composition:\n \"direct-effective-response\";\n };\n\nexport interface ResolvedSensorSpectralResponse {\n profileId: string;\n colorSamplingProfileId: string;\n channelId: string;\n sourceResponseKind:\n SensorSpectralChannelResponse[\"kind\"];\n responseScope:\n SensorSpectralChannelResponse[\"responseScope\"];\n scientificStatus:\n SensorSpectralResponseScientificStatus;\n uncertainty:\n SensorSpectralResponseUncertainty;\n wavelengthNanometers: number;\n wavelengthBasis: SpectralWavelengthBasis;\n wavelengthBasisResolved: boolean;\n declaredWavelengthRangeNanometers: {\n minimum: number;\n maximum: number;\n };\n interpolation:\n \"piecewise-linear\";\n interpolationUsed: boolean;\n outsideRangeBehavior:\n \"fail-closed\";\n response:\n ResolvedSensorSpectralResponseValue;\n referenceConditions?:\n SensorSpectralReferenceConditions;\n conditionDependenceModeled: false;\n fieldAngleDependenceModeled: false;\n temperatureDependenceModeled: false;\n polarizationDependenceModeled: false;\n spectralIrradianceIntegrated: false;\n wavelengthIntegrationPerformed: false;\n qeResponsivityConversionPerformed: false;\n photonsCalculated: false;\n electronsCalculated: false;\n rawCodeValueProduced: false;\n componentEvidence: {\n profile: readonly EvidenceProvenance[];\n channel: readonly EvidenceProvenance[];\n curves:\n readonly (readonly EvidenceProvenance[])[];\n };\n}\n\nfunction requireRecord(\n value: unknown,\n path: string,\n allowedKeys: readonly string[] | null\n): UnknownRecord {\n if (\n typeof value !== \"object\" ||\n value === null ||\n Array.isArray(value)\n ) {\n throw new InvalidConfigurationError(\n path + \" must be an object.\"\n );\n }\n if (allowedKeys !== null) rejectUnknownFields(value, allowedKeys, path);\n return value as UnknownRecord;\n}\n\nfunction requireNonEmptyString(\n value: unknown,\n path: string\n): string {\n if (\n typeof value !== \"string\" ||\n value.trim().length === 0\n ) {\n throw new InvalidConfigurationError(\n path + \" must be a non-empty string.\"\n );\n }\n return value;\n}\n\nfunction requireFinite(\n value: unknown,\n path: string\n): number {\n if (\n typeof value !== \"number\" ||\n !Number.isFinite(value)\n ) {\n throw new InvalidConfigurationError(\n path + \" must be a finite number.\"\n );\n }\n return value;\n}\n\nfunction requirePositiveFinite(\n value: unknown,\n path: string\n): number {\n const number = requireFinite(value, path);\n if (number <= 0) {\n throw new InvalidConfigurationError(\n path + \" must be greater than zero.\"\n );\n }\n return number;\n}\n\nfunction requireFraction(\n value: unknown,\n path: string\n): number {\n const number = requireFinite(value, path);\n if (number < 0 || number > 1) {\n throw new InvalidConfigurationError(\n path +\n \" must be a finite fraction from 0 through 1.\"\n );\n }\n return number;\n}\n\nfunction requireReusableCurveEvidence(\n value: unknown,\n path: string\n): readonly EvidenceProvenance[] {\n const evidence = parseEvidenceList(\n value,\n path\n );\n\n if (\n !evidence.some(\n (entry) =>\n entry.reuseStatus ===\n \"reusable-data\" ||\n entry.reuseStatus ===\n \"photivra-owned\"\n )\n ) {\n throw new InvalidConfigurationError(\n path +\n \" must contain reusable-data or photivra-owned evidence because the curve contains reusable numeric data.\"\n );\n }\n\n return evidence;\n}\n\nfunction parseUncertainty(\n value: unknown,\n path: string\n): SensorSpectralResponseUncertainty {\n const record = requireRecord(value, path, null);\n rejectUnknownVariantFields(record, \"kind\", UNCERTAINTY_VARIANT_FIELDS, path);\n\n if (record.kind === \"relative\") {\n const fraction = requireFinite(\n record.fraction,\n path + \".fraction\"\n );\n if (fraction < 0) {\n throw new InvalidConfigurationError(\n path +\n \".fraction must be greater than or equal to zero.\"\n );\n }\n return {\n kind: \"relative\",\n fraction,\n basis: requireNonEmptyString(\n record.basis,\n path + \".basis\"\n )\n };\n }\n\n if (record.kind === \"not-quantified\") {\n return {\n kind: \"not-quantified\",\n limitation: requireNonEmptyString(\n record.limitation,\n path + \".limitation\"\n )\n };\n }\n\n throw new InvalidConfigurationError(\n path + \".kind is invalid.\"\n );\n}\n\nfunction parseReferenceConditions(\n value: unknown,\n path: string\n): SensorSpectralReferenceConditions {\n const record = requireRecord(value, path, [\"temperatureC\", \"incidenceAngleDegreesFromNormal\", \"polarization\"]);\n const temperatureC =\n record.temperatureC === undefined\n ? undefined\n : requireFinite(\n record.temperatureC,\n path + \".temperatureC\"\n );\n const incidenceAngleDegreesFromNormal =\n record.incidenceAngleDegreesFromNormal ===\n undefined\n ? undefined\n : requireFinite(\n record.incidenceAngleDegreesFromNormal,\n path +\n \".incidenceAngleDegreesFromNormal\"\n );\n\n if (\n incidenceAngleDegreesFromNormal !==\n undefined &&\n (incidenceAngleDegreesFromNormal < 0 ||\n incidenceAngleDegreesFromNormal > 90)\n ) {\n throw new InvalidConfigurationError(\n path +\n \".incidenceAngleDegreesFromNormal must be from 0 through 90 degrees.\"\n );\n }\n\n const polarization =\n record.polarization;\n if (\n polarization !== undefined &&\n polarization !== \"unpolarized\" &&\n polarization !== \"unspecified\"\n ) {\n throw new InvalidConfigurationError(\n path + \".polarization is invalid.\"\n );\n }\n\n if (\n temperatureC === undefined &&\n incidenceAngleDegreesFromNormal ===\n undefined &&\n polarization === undefined\n ) {\n throw new InvalidConfigurationError(\n path +\n \" must declare at least one reference condition.\"\n );\n }\n\n return {\n ...(temperatureC === undefined\n ? {}\n : { temperatureC }),\n ...(incidenceAngleDegreesFromNormal ===\n undefined\n ? {}\n : {\n incidenceAngleDegreesFromNormal\n }),\n ...(polarization === undefined\n ? {}\n : { polarization })\n };\n}\n\nfunction parseCurveBase(\n record: UnknownRecord,\n path: string\n): SpectralCurveBase {\n if (record.wavelengthUnit !== \"nm\") {\n throw new InvalidConfigurationError(\n path +\n '.wavelengthUnit must be \"nm\".'\n );\n }\n if (\n record.interpolation !==\n \"piecewise-linear\"\n ) {\n throw new InvalidConfigurationError(\n path +\n '.interpolation must be \"piecewise-linear\".'\n );\n }\n if (\n record.outsideRangeBehavior !==\n \"fail-closed\"\n ) {\n throw new InvalidConfigurationError(\n path +\n '.outsideRangeBehavior must be \"fail-closed\".'\n );\n }\n\n return {\n wavelengthUnit: \"nm\",\n wavelengthBasis:\n parseSpectralWavelengthBasis(\n record.wavelengthBasis,\n path + \".wavelengthBasis\"\n ),\n interpolation: \"piecewise-linear\",\n outsideRangeBehavior: \"fail-closed\",\n evidence:\n requireReusableCurveEvidence(\n record.evidence,\n path + \".evidence\"\n )\n };\n}\n\nfunction parseFractionSamples(\n value: unknown,\n path: string\n): readonly SpectralFractionSample[] {\n if (\n !Array.isArray(value) ||\n value.length < 2\n ) {\n throw new InvalidConfigurationError(\n path +\n \" must contain at least two wavelength samples.\"\n );\n }\n\n let previousWavelength =\n Number.NEGATIVE_INFINITY;\n\n return value.map((entry, index) => {\n const record = requireRecord(\n entry,\n path + \"[\" + index + \"]\", [\"wavelengthNanometers\", \"value\"]\n );\n const wavelengthNanometers =\n requirePositiveFinite(\n record.wavelengthNanometers,\n path +\n \"[\" +\n index +\n \"].wavelengthNanometers\"\n );\n\n if (\n wavelengthNanometers <=\n previousWavelength\n ) {\n throw new InvalidConfigurationError(\n path +\n \" wavelengths must be strictly increasing.\"\n );\n }\n previousWavelength =\n wavelengthNanometers;\n\n return {\n wavelengthNanometers,\n value: requireFraction(\n record.value,\n path +\n \"[\" +\n index +\n \"].value\"\n )\n };\n });\n}\n\nfunction parseResponsivitySamples(\n value: unknown,\n path: string\n): readonly SpectralResponsivitySample[] {\n if (\n !Array.isArray(value) ||\n value.length < 2\n ) {\n throw new InvalidConfigurationError(\n path +\n \" must contain at least two wavelength samples.\"\n );\n }\n\n let previousWavelength =\n Number.NEGATIVE_INFINITY;\n\n return value.map((entry, index) => {\n const record = requireRecord(\n entry,\n path + \"[\" + index + \"]\", [\"wavelengthNanometers\", \"amperesPerWatt\"]\n );\n const wavelengthNanometers =\n requirePositiveFinite(\n record.wavelengthNanometers,\n path +\n \"[\" +\n index +\n \"].wavelengthNanometers\"\n );\n\n if (\n wavelengthNanometers <=\n previousWavelength\n ) {\n throw new InvalidConfigurationError(\n path +\n \" wavelengths must be strictly increasing.\"\n );\n }\n previousWavelength =\n wavelengthNanometers;\n\n const amperesPerWatt =\n requireFinite(\n record.amperesPerWatt,\n path +\n \"[\" +\n index +\n \"].amperesPerWatt\"\n );\n if (amperesPerWatt < 0) {\n throw new InvalidConfigurationError(\n path +\n \"[\" +\n index +\n \"].amperesPerWatt must be greater than or equal to zero.\"\n );\n }\n\n return {\n wavelengthNanometers,\n amperesPerWatt\n };\n });\n}\n\nfunction parseFractionCurve(\n value: unknown,\n path: string\n): SpectralFractionCurve {\n const record = requireRecord(\n value,\n path, [\"wavelengthUnit\", \"interpolation\", \"outsideRangeBehavior\", \"wavelengthBasis\", \"evidence\", \"samples\"]\n );\n return {\n ...parseCurveBase(record, path),\n samples: parseFractionSamples(\n record.samples,\n path + \".samples\"\n )\n };\n}\n\nfunction parseResponsivityCurve(\n value: unknown,\n path: string\n): SpectralResponsivityCurve {\n const record = requireRecord(\n value,\n path, [\"wavelengthUnit\", \"interpolation\", \"outsideRangeBehavior\", \"wavelengthBasis\", \"evidence\", \"samples\"]\n );\n return {\n ...parseCurveBase(record, path),\n samples: parseResponsivitySamples(\n record.samples,\n path + \".samples\"\n )\n };\n}\n\nfunction parseScientificStatus(\n value: unknown,\n path: string\n): SensorSpectralResponseScientificStatus {\n if (\n value !== \"calibrated\" &&\n value !== \"approximation\"\n ) {\n throw new InvalidConfigurationError(\n path + \" is invalid.\"\n );\n }\n return value;\n}\n\nfunction validateCalibratedWavelengthBasis(\n scientificStatus:\n SensorSpectralResponseScientificStatus,\n curves: readonly (\n | SpectralFractionCurve\n | SpectralResponsivityCurve\n )[],\n path: string\n): void {\n if (\n scientificStatus === \"calibrated\" &&\n curves.some(\n (curve) =>\n curve.wavelengthBasis ===\n \"unspecified\"\n )\n ) {\n throw new InvalidConfigurationError(\n path +\n ' calibrated responses must declare wavelengthBasis \"air\" or \"vacuum\".'\n );\n }\n}\n\nfunction usableRange(\n curves: readonly (\n | SpectralFractionCurve\n | SpectralResponsivityCurve\n )[]\n): {\n minimum: number;\n maximum: number;\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\n if (!(minimum < maximum)) {\n throw new InvalidConfigurationError(\n \"Separable spectral-response component curves must have a non-empty overlapping wavelength interval.\"\n );\n }\n\n return { minimum, maximum };\n}\n\nfunction parseChannelResponse(\n value: unknown,\n path: string\n): SensorSpectralChannelResponse {\n const record = requireRecord(\n value,\n path, [\"channelId\", \"scientificStatus\", \"uncertainty\", \"evidence\", \"referenceConditions\", \"conditionDependence\", \"kind\", \"responseScope\", \"externalQuantumEfficiency\", \"spectralResponsivity\", \"combinationRule\", \"channelFilterTransmittance\", \"detectorExternalQuantumEfficiency\"]\n );\n const channelId =\n requireNonEmptyString(\n record.channelId,\n path + \".channelId\"\n );\n const scientificStatus =\n parseScientificStatus(\n record.scientificStatus,\n path + \".scientificStatus\"\n );\n const uncertainty =\n parseUncertainty(\n record.uncertainty,\n path + \".uncertainty\"\n );\n const evidence =\n parseEvidenceList(\n record.evidence,\n path + \".evidence\"\n );\n const referenceConditions =\n record.referenceConditions ===\n undefined\n ? undefined\n : parseReferenceConditions(\n record.referenceConditions,\n path + \".referenceConditions\"\n );\n\n if (\n record.conditionDependence !==\n \"not-modeled\"\n ) {\n throw new InvalidConfigurationError(\n path +\n '.conditionDependence must be \"not-modeled\".'\n );\n }\n\n const common = {\n channelId,\n scientificStatus,\n uncertainty,\n evidence,\n ...(referenceConditions === undefined\n ? {}\n : { referenceConditions }),\n conditionDependence:\n \"not-modeled\" as const\n };\n\n if (\n record.kind ===\n \"effective-external-quantum-efficiency\"\n ) {\n if (\n record.responseScope !==\n \"site-incident-effective-channel-response\" &&\n record.responseScope !==\n \"sensor-package-incident-effective-channel-response\"\n ) {\n throw new InvalidConfigurationError(\n path +\n \".responseScope is invalid.\"\n );\n }\n const curve = parseFractionCurve(\n record.externalQuantumEfficiency,\n path +\n \".externalQuantumEfficiency\"\n );\n validateCalibratedWavelengthBasis(\n scientificStatus,\n [curve],\n path\n );\n return {\n ...common,\n kind:\n \"effective-external-quantum-efficiency\",\n responseScope:\n record.responseScope,\n externalQuantumEfficiency:\n curve\n };\n }\n\n if (\n record.kind ===\n \"effective-spectral-responsivity\"\n ) {\n if (\n record.responseScope !==\n \"site-incident-effective-channel-response\" &&\n record.responseScope !==\n \"sensor-package-incident-effective-channel-response\"\n ) {\n throw new InvalidConfigurationError(\n path +\n \".responseScope is invalid.\"\n );\n }\n const curve =\n parseResponsivityCurve(\n record.spectralResponsivity,\n path +\n \".spectralResponsivity\"\n );\n validateCalibratedWavelengthBasis(\n scientificStatus,\n [curve],\n path\n );\n return {\n ...common,\n kind:\n \"effective-spectral-responsivity\",\n responseScope:\n record.responseScope,\n spectralResponsivity: curve\n };\n }\n\n if (\n record.kind ===\n \"separable-channel-filter-and-detector-eqe\"\n ) {\n if (\n record.responseScope !==\n \"site-incident-channel-filter-times-detector-eqe\"\n ) {\n throw new InvalidConfigurationError(\n path +\n \".responseScope is invalid.\"\n );\n }\n if (\n record.combinationRule !==\n \"multiply\"\n ) {\n throw new InvalidConfigurationError(\n path +\n '.combinationRule must be \"multiply\".'\n );\n }\n\n const filterCurve =\n parseFractionCurve(\n record.channelFilterTransmittance,\n path +\n \".channelFilterTransmittance\"\n );\n const detectorCurve =\n parseFractionCurve(\n record.detectorExternalQuantumEfficiency,\n path +\n \".detectorExternalQuantumEfficiency\"\n );\n\n if (\n filterCurve.wavelengthBasis !==\n detectorCurve.wavelengthBasis\n ) {\n throw new InvalidConfigurationError(\n path +\n \" separable curves must use the same wavelength basis.\"\n );\n }\n\n usableRange([\n filterCurve,\n detectorCurve\n ]);\n validateCalibratedWavelengthBasis(\n scientificStatus,\n [\n filterCurve,\n detectorCurve\n ],\n path\n );\n\n return {\n ...common,\n kind:\n \"separable-channel-filter-and-detector-eqe\",\n responseScope:\n \"site-incident-channel-filter-times-detector-eqe\",\n combinationRule: \"multiply\",\n channelFilterTransmittance:\n filterCurve,\n detectorExternalQuantumEfficiency:\n detectorCurve\n };\n }\n\n throw new InvalidConfigurationError(\n path + \".kind is invalid.\"\n );\n}\n\n/**\n * Parses wavelength-dependent response data for sensor channels. Responses are\n * never guessed from channel names or the CFA type.\n *\n * An embedded multi-point curve needs evidence that the data is reusable or\n * owned by Photivra. A public reference without reuse permission can support\n * metadata, but does not by itself allow copying numeric curve data into the engine.\n */\nexport function parseSensorSpectralResponseProfile(\n value: unknown\n): SensorSpectralResponseProfile {\n const profile = requireRecord(\n value,\n \"sensorSpectralResponse\", [\"schemaVersion\", \"channels\", \"profileId\", \"colorSamplingProfileId\", \"evidence\"]\n );\n\n if (\n profile.schemaVersion !== \"0.1.0\"\n ) {\n throw new InvalidConfigurationError(\n 'sensorSpectralResponse.schemaVersion must be \"0.1.0\".'\n );\n }\n\n if (\n !Array.isArray(profile.channels) ||\n profile.channels.length === 0\n ) {\n throw new InvalidConfigurationError(\n \"sensorSpectralResponse.channels must be a non-empty array.\"\n );\n }\n\n const channels =\n profile.channels.map(\n (entry, index) =>\n parseChannelResponse(\n entry,\n \"sensorSpectralResponse.channels[\" +\n index +\n \"]\"\n )\n );\n\n const channelIds = channels.map(\n (entry) => entry.channelId\n );\n if (\n new Set(channelIds).size !==\n channelIds.length\n ) {\n throw new InvalidConfigurationError(\n \"sensorSpectralResponse.channels must not contain duplicate channel IDs.\"\n );\n }\n\n return {\n schemaVersion: \"0.1.0\",\n profileId: requireNonEmptyString(\n profile.profileId,\n \"sensorSpectralResponse.profileId\"\n ),\n colorSamplingProfileId:\n requireNonEmptyString(\n profile.colorSamplingProfileId,\n \"sensorSpectralResponse.colorSamplingProfileId\"\n ),\n evidence: parseEvidenceList(\n profile.evidence,\n \"sensorSpectralResponse.evidence\"\n ),\n channels\n };\n}\n\n/**\n * Creates an exact binding to one parsed response channel's data, so a swapped numeric curve cannot\n * hide behind an unchanged profile ID.\n *\n * The binding includes the channel's EQE or A/W curve (or its separately evidenced filter × detector\n * EQE curves), its range, interpolation and wavelength basis, and the exact canonical channel data.\n * The channel is parsed first and serialized with sorted object keys, so property order never changes\n * the binding and a malformed channel throws.\n * @param response - The input; see `SensorSpectralChannelResponse` for its fields, units and coordinates.\n * @returns The result. Success does not imply calibration.\n *\n * @see docs/API_REFERENCE.md for the package export and its full type definitions.\n */\nexport function createSensorSpectralResponseChannelBinding(\n response: SensorSpectralChannelResponse\n): SensorSpectralResponseChannelBinding {\n return {\n format:\n SENSOR_SPECTRAL_RESPONSE_CHANNEL_BINDING_FORMAT,\n // Parse first so the binding never repeats unvalidated input, then sort keys\n // so channels with the same content bind identically whatever their key order.\n canonicalJson:\n stringifyCanonicalJson(\n parseChannelResponse(\n response,\n \"sensorSpectralResponseChannel\"\n ),\n {\n undefinedObjectProperties: \"omit\",\n nonFiniteNumberMessage:\n \"sensorSpectralResponseChannel must contain finite numbers.\",\n unsupportedValueMessage:\n \"sensorSpectralResponseChannel must be plain JSON data.\"\n }\n )\n };\n}\n\n/**\n * Rejects a channel binding stored in a reduction unless it uses the current format. A v1 binding from\n * package 1.5.0 or earlier fails with an explicit version error: a different serializer produced it, so\n * comparing it would report a misleading data mismatch. Recompute the spectral quadrature and reduction\n * with this release to get a v2 binding.\n * Internal to the repository; not exported from the package.\n */\nexport function requireCurrentSensorSpectralResponseChannelBinding(\n binding: unknown\n): void {\n const format =\n typeof binding === \"object\" && binding !== null\n ? (binding as { format?: unknown }).format\n : undefined;\n if (format === SENSOR_SPECTRAL_RESPONSE_CHANNEL_BINDING_FORMAT) return;\n throw new InvalidConfigurationError(\n format === \"photivra-spectral-channel-response-canonical-json-v1\"\n ? \"Response-channel binding format v1 (package 1.5.0 and earlier) is not supported; recompute the spectral quadrature and reduction to obtain a v2 binding.\"\n : \"Response-channel binding has an unsupported format; expected \" + SENSOR_SPECTRAL_RESPONSE_CHANNEL_BINDING_FORMAT + \".\"\n );\n}\n\nfunction topologyChannelIds(\n profile: SensorColorSamplingProfile\n): readonly string[] {\n if (\n profile.layout.kind ===\n \"layered\"\n ) {\n throw new InvalidScientificInputError(\n \"Layered color sampling remains spatially unresolved; schema 0.1.0 spectral response cannot yet be composed into per-site layered sampling.\"\n );\n }\n\n if (\n profile.layout.kind ===\n \"monochrome\"\n ) {\n return [profile.layout.channelId];\n }\n\n return [\n ...new Set(\n profile.layout.siteChannelIds\n )\n ];\n}\n\nfunction interpolateFraction(\n curve: SpectralFractionCurve,\n wavelengthNanometers: number\n): {\n value: number;\n interpolationUsed: boolean;\n} {\n return interpolateCurve(\n curve.samples,\n wavelengthNanometers,\n (sample) => sample.value\n );\n}\n\nfunction interpolateResponsivity(\n curve: SpectralResponsivityCurve,\n wavelengthNanometers: number\n): {\n value: number;\n interpolationUsed: boolean;\n} {\n return interpolateCurve(\n curve.samples,\n wavelengthNanometers,\n (sample) =>\n sample.amperesPerWatt\n );\n}\n\nfunction interpolateCurve<T extends {\n wavelengthNanometers: number;\n}>(\n samples: readonly T[],\n wavelengthNanometers: number,\n valueOf: (sample: T) => number\n): {\n value: number;\n interpolationUsed: boolean;\n} {\n const first = samples[0]!;\n const last =\n samples[samples.length - 1]!;\n\n if (\n wavelengthNanometers <\n first.wavelengthNanometers ||\n wavelengthNanometers >\n last.wavelengthNanometers\n ) {\n throw new InvalidScientificInputError(\n \"Requested wavelength is outside the declared spectral-response range; extrapolation/zero-fill is not permitted.\"\n );\n }\n\n for (\n let index = 0;\n index < samples.length;\n index += 1\n ) {\n const sample = samples[index]!;\n if (\n wavelengthNanometers ===\n sample.wavelengthNanometers\n ) {\n return {\n value: valueOf(sample),\n interpolationUsed: false\n };\n }\n\n if (\n wavelengthNanometers <\n sample.wavelengthNanometers\n ) {\n const previous =\n samples[index - 1]!;\n const phase =\n (wavelengthNanometers -\n previous.wavelengthNanometers) /\n (sample.wavelengthNanometers -\n previous.wavelengthNanometers);\n return {\n value:\n valueOf(previous) +\n phase *\n (valueOf(sample) -\n valueOf(previous)),\n interpolationUsed: true\n };\n }\n }\n\n return {\n value: valueOf(last),\n interpolationUsed: false\n };\n}\n\n/**\n * Returns one sensor channel's response at one wavelength.\n *\n * It checks the response profile against the exact colour-sampling profile,\n * rejects layered layouts whose sampling is not defined, uses only the declared\n * piecewise-linear interpolation, and never extrapolates.\n *\n * Both a direct effective response and separate filter × detector QE curves are\n * supported. QE and A/W responsivity are different physical quantities; this\n * API does not convert between them.\n */\nexport function resolveParsedSensorSpectralResponseAtWavelength(\n input:\n ResolveSensorSpectralResponseAtWavelengthInput\n): CalculationResult<ResolvedSensorSpectralResponse> {\n const colorProfile =\n input.colorSamplingProfile;\n const responseProfile =\n input.spectralResponseProfile;\n\n if (\n responseProfile\n .colorSamplingProfileId !==\n colorProfile.profileId\n ) {\n throw new InvalidScientificInputError(\n \"Spectral-response colorSamplingProfileId must match the supplied color-sampling profile.\"\n );\n }\n\n const knownChannelIds =\n topologyChannelIds(colorProfile);\n const knownSet =\n new Set(knownChannelIds);\n\n for (\n const response of\n responseProfile.channels\n ) {\n if (\n !knownSet.has(\n response.channelId\n )\n ) {\n throw new InvalidScientificInputError(\n \"Spectral-response profile declares a channel ID that is not present in the linked color-sampling topology.\"\n );\n }\n }\n\n if (\n typeof input.channelId !==\n \"string\" ||\n input.channelId.trim().length ===\n 0\n ) {\n throw new InvalidConfigurationError(\n \"channelId must be a non-empty string.\"\n );\n }\n if (\n !knownSet.has(input.channelId)\n ) {\n throw new InvalidScientificInputError(\n \"channelId is not present in the linked color-sampling topology.\"\n );\n }\n\n const response =\n responseProfile.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 if (\n typeof input.wavelengthNanometers !==\n \"number\" ||\n !Number.isFinite(\n input.wavelengthNanometers\n ) ||\n input.wavelengthNanometers <= 0\n ) {\n throw new InvalidScientificInputError(\n \"wavelengthNanometers must be finite and greater than zero.\"\n );\n }\n\n if (\n input.wavelengthBasis !==\n \"air\" &&\n input.wavelengthBasis !==\n \"vacuum\" &&\n input.wavelengthBasis !==\n \"unspecified\"\n ) {\n throw new InvalidConfigurationError(\n \"wavelengthBasis is invalid.\"\n );\n }\n\n const curves:\n (\n | SpectralFractionCurve\n | SpectralResponsivityCurve\n )[] = [];\n\n let resolvedValue:\n ResolvedSensorSpectralResponseValue;\n let interpolationUsed: boolean;\n let wavelengthBasis:\n SpectralWavelengthBasis;\n\n if (\n response.kind ===\n \"effective-external-quantum-efficiency\"\n ) {\n const curve =\n response.externalQuantumEfficiency;\n curves.push(curve);\n wavelengthBasis =\n curve.wavelengthBasis;\n\n if (\n input.wavelengthBasis !==\n wavelengthBasis\n ) {\n throw new InvalidScientificInputError(\n \"Requested wavelengthBasis must exactly match the response curve; Photivra does not convert air/vacuum/unspecified wavelength coordinates implicitly.\"\n );\n }\n\n const resolved =\n interpolateFraction(\n curve,\n input.wavelengthNanometers\n );\n interpolationUsed =\n resolved.interpolationUsed;\n resolvedValue = {\n kind:\n \"effective-external-quantum-efficiency\",\n externalQuantumEfficiency:\n resolved.value,\n composition:\n \"direct-effective-response\"\n };\n } else if (\n response.kind ===\n \"effective-spectral-responsivity\"\n ) {\n const curve =\n response.spectralResponsivity;\n curves.push(curve);\n wavelengthBasis =\n curve.wavelengthBasis;\n\n if (\n input.wavelengthBasis !==\n wavelengthBasis\n ) {\n throw new InvalidScientificInputError(\n \"Requested wavelengthBasis must exactly match the response curve; Photivra does not convert air/vacuum/unspecified wavelength coordinates implicitly.\"\n );\n }\n\n const resolved =\n interpolateResponsivity(\n curve,\n input.wavelengthNanometers\n );\n interpolationUsed =\n resolved.interpolationUsed;\n resolvedValue = {\n kind:\n \"effective-spectral-responsivity\",\n amperesPerWatt:\n resolved.value,\n composition:\n \"direct-effective-response\"\n };\n } else {\n const filterCurve =\n response\n .channelFilterTransmittance;\n const detectorCurve =\n response\n .detectorExternalQuantumEfficiency;\n curves.push(\n filterCurve,\n detectorCurve\n );\n wavelengthBasis =\n filterCurve.wavelengthBasis;\n\n if (\n input.wavelengthBasis !==\n wavelengthBasis\n ) {\n throw new InvalidScientificInputError(\n \"Requested wavelengthBasis must exactly match the separable response curves.\"\n );\n }\n\n const filter =\n interpolateFraction(\n filterCurve,\n input.wavelengthNanometers\n );\n const detector =\n interpolateFraction(\n detectorCurve,\n input.wavelengthNanometers\n );\n\n interpolationUsed =\n filter.interpolationUsed ||\n detector.interpolationUsed;\n\n resolvedValue = {\n kind:\n \"effective-external-quantum-efficiency\",\n externalQuantumEfficiency:\n filter.value *\n detector.value,\n composition:\n \"channel-filter-transmittance-times-detector-eqe\",\n channelFilterTransmittance:\n filter.value,\n detectorExternalQuantumEfficiency:\n detector.value\n };\n }\n\n const range = usableRange(curves);\n\n return calculatedResult(\n {\n profileId:\n responseProfile.profileId,\n colorSamplingProfileId:\n responseProfile\n .colorSamplingProfileId,\n channelId: response.channelId,\n sourceResponseKind:\n response.kind,\n responseScope:\n response.responseScope,\n scientificStatus:\n response.scientificStatus,\n uncertainty:\n response.uncertainty,\n wavelengthNanometers:\n input.wavelengthNanometers,\n wavelengthBasis,\n wavelengthBasisResolved:\n wavelengthBasis !==\n \"unspecified\",\n declaredWavelengthRangeNanometers:\n range,\n interpolation:\n \"piecewise-linear\",\n interpolationUsed,\n outsideRangeBehavior:\n \"fail-closed\",\n response: resolvedValue,\n ...(response.referenceConditions ===\n undefined\n ? {}\n : {\n referenceConditions:\n response.referenceConditions\n }),\n conditionDependenceModeled:\n false,\n fieldAngleDependenceModeled:\n false,\n temperatureDependenceModeled:\n false,\n polarizationDependenceModeled:\n false,\n spectralIrradianceIntegrated:\n false,\n wavelengthIntegrationPerformed:\n false,\n qeResponsivityConversionPerformed:\n false,\n photonsCalculated: false,\n electronsCalculated: false,\n rawCodeValueProduced: false,\n componentEvidence: {\n profile:\n responseProfile.evidence,\n channel: response.evidence,\n curves: curves.map(\n (curve) => curve.evidence\n )\n }\n },\n \"sensor-spectral-response-lookup\",\n \"1.0.0\",\n [\n \"Channel IDs are exact semantic identifiers from the linked color-sampling topology; names do not imply spectral behavior.\",\n \"Piecewise-linear interpolation is applied only inside the declared wavelength range; no extrapolation or implicit zero response is permitted.\",\n \"Air, vacuum and unspecified wavelength bases are never converted implicitly.\",\n \"Direct effective response is preserved when decomposition is not established; separable filter×detector-QE is composed only when explicitly declared.\",\n \"Spectral responsivity in A/W and external QE remain distinct physical representations and are not converted by this API.\",\n \"Reference-condition metadata does not create field-angle, temperature or polarization dependence models.\",\n \"Spectral irradiance integration, wavelength integration, photon/electron conversion and RAW-domain output remain future work.\"\n ]\n );\n}\n\n\n/**\n * Public spectral-response lookup for untrusted, unparsed profiles.\n *\n * The profiles are parsed here, once. Internal code that has already parsed\n * both profiles can call resolveParsedSensorSpectralResponseAtWavelength, to\n * avoid parsing them again for every wavelength node.\n */\nexport function resolveSensorSpectralResponseAtWavelength(\n input:\n ResolveSensorSpectralResponseAtWavelengthInput\n): CalculationResult<ResolvedSensorSpectralResponse> {\n requireInputKeys(input, \"resolveSensorSpectralResponseAtWavelength\", [\"colorSamplingProfile\", \"spectralResponseProfile\", \"channelId\", \"wavelengthNanometers\", \"wavelengthBasis\"]);\n const colorProfile =\n parseSensorColorSamplingProfile(\n input.colorSamplingProfile\n );\n const responseProfile =\n parseSensorSpectralResponseProfile(\n input.spectralResponseProfile\n );\n\n return resolveParsedSensorSpectralResponseAtWavelength({\n ...input,\n colorSamplingProfile:\n colorProfile,\n spectralResponseProfile:\n responseProfile\n });\n}\n"]}
1
+ {"version":3,"file":"spectral-response.js","sourceRoot":"","sources":["../../src/sensor/spectral-response.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Sensor spectral response: how strongly each sensor channel responds at each wavelength, as an EQE\n * (external quantum efficiency) curve, an A/W responsivity curve, or a colour-filter curve times a\n * detector EQE curve.\n *\n * Responses are never guessed from channel names (such as \"red\") or the CFA type, and are never\n * extrapolated outside the sampled range. A numeric curve can be embedded only with evidence that the\n * data is reusable or owned by Photivra; a public reference without reuse permission can support\n * metadata but does not by itself allow copying its numbers into the engine.\n * @see docs/MOTION_AND_SIGNAL.md for equations, coordinate/unit conventions, blockers and support\n * limits.\n */\n\nimport {\n calculatedResult,\n type CalculationResult\n} from \"../core/calculation-result.js\";\nimport { stringifyCanonicalJson } from \"../core/canonical-json.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport {\n parseEvidenceList,\n type EvidenceProvenance\n} from \"../core/evidence-provenance.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport {\n parseSpectralWavelengthBasis,\n type SpectralWavelengthBasis,\n type SpectralWavelengthSample\n} from \"../core/spectral.js\";\nexport type { SpectralWavelengthBasis } from \"../core/spectral.js\";\nimport {\n parseSensorColorSamplingProfile,\n type SensorColorSamplingProfile\n} from \"./color-sampling.js\";\nimport { requireInputKeys, rejectUnknownVariantFields, UNCERTAINTY_VARIANT_FIELDS, requireConfigRecord as requireRecord, requireConfigString as requireNonEmptyString, parseConfigScientificStatus as parseScientificStatus, requireConfigFiniteNumber as requireFinite, requireConfigFraction as requireFraction, requireConfigNumberAboveZero as requirePositiveFinite } from \"../core/record-validation.js\";\n\ntype UnknownRecord = Record<string, unknown>;\n\nexport type SensorSpectralResponseScientificStatus =\n | \"calibrated\"\n | \"approximation\";\n\nexport type SensorSpectralResponseUncertainty =\n | {\n kind: \"relative\";\n fraction: number;\n basis: string;\n }\n | {\n kind: \"not-quantified\";\n limitation: string;\n };\n\nexport interface SensorSpectralReferenceConditions {\n temperatureC?: number;\n incidenceAngleDegreesFromNormal?: number;\n polarization?:\n | \"unpolarized\"\n | \"unspecified\";\n}\n\nexport interface SpectralFractionSample\n extends SpectralWavelengthSample {\n value: number;\n}\n\nexport interface SpectralResponsivitySample\n extends SpectralWavelengthSample {\n amperesPerWatt: number;\n}\n\ninterface SpectralCurveBase {\n wavelengthUnit: \"nm\";\n wavelengthBasis: SpectralWavelengthBasis;\n interpolation: \"piecewise-linear\";\n outsideRangeBehavior: \"fail-closed\";\n evidence: readonly EvidenceProvenance[];\n}\n\nexport interface SpectralFractionCurve\n extends SpectralCurveBase {\n samples: readonly SpectralFractionSample[];\n}\n\nexport interface SpectralResponsivityCurve\n extends SpectralCurveBase {\n samples: readonly SpectralResponsivitySample[];\n}\n\ninterface SensorSpectralChannelResponseBase {\n channelId: string;\n scientificStatus:\n SensorSpectralResponseScientificStatus;\n uncertainty:\n SensorSpectralResponseUncertainty;\n evidence: readonly EvidenceProvenance[];\n referenceConditions?:\n SensorSpectralReferenceConditions;\n conditionDependence:\n \"not-modeled\";\n}\n\nexport type EffectiveSensorResponseScope =\n | \"site-incident-effective-channel-response\"\n | \"sensor-package-incident-effective-channel-response\";\n\nexport type SensorSpectralChannelResponse =\n | (SensorSpectralChannelResponseBase & {\n kind:\n \"effective-external-quantum-efficiency\";\n responseScope:\n EffectiveSensorResponseScope;\n externalQuantumEfficiency:\n SpectralFractionCurve;\n })\n | (SensorSpectralChannelResponseBase & {\n kind:\n \"effective-spectral-responsivity\";\n responseScope:\n EffectiveSensorResponseScope;\n spectralResponsivity:\n SpectralResponsivityCurve;\n })\n | (SensorSpectralChannelResponseBase & {\n kind:\n \"separable-channel-filter-and-detector-eqe\";\n responseScope:\n \"site-incident-channel-filter-times-detector-eqe\";\n combinationRule: \"multiply\";\n channelFilterTransmittance:\n SpectralFractionCurve;\n detectorExternalQuantumEfficiency:\n SpectralFractionCurve;\n });\n\n/**\n * Current channel-binding format. v2 (package 2.0.0) serializes the parsed channel with sorted keys.\n * v1 (package 1.5.0 and earlier) used the unparsed channel's JSON in insertion order; it is rejected\n * with an explicit version error instead of being compared.\n */\nexport const SENSOR_SPECTRAL_RESPONSE_CHANNEL_BINDING_FORMAT =\n \"photivra-spectral-channel-response-canonical-json-v2\" as const;\n\nexport interface SensorSpectralResponseChannelBinding {\n format:\n typeof SENSOR_SPECTRAL_RESPONSE_CHANNEL_BINDING_FORMAT;\n canonicalJson: string;\n}\n\n/**\n * Versioned response data for the channels of one exact colour-sampling profile.\n * Each channel declares an effective EQE, an effective A/W response, or separate\n * filter and detector EQE curves, with its sampled range, interpolation and an\n * explicit wavelength basis (air or vacuum). Reference conditions, evidence and\n * uncertainty each keep their own validity. Names such as red, green or blue\n * never fill in a missing curve or justify extrapolation.\n */\nexport interface SensorSpectralResponseProfile {\n schemaVersion: \"0.1.0\";\n profileId: string;\n colorSamplingProfileId: string;\n evidence: readonly EvidenceProvenance[];\n channels:\n readonly SensorSpectralChannelResponse[];\n}\n\nexport interface ResolveSensorSpectralResponseAtWavelengthInput {\n colorSamplingProfile:\n SensorColorSamplingProfile;\n spectralResponseProfile:\n SensorSpectralResponseProfile;\n channelId: string;\n wavelengthNanometers: number;\n wavelengthBasis: SpectralWavelengthBasis;\n}\n\nexport type ResolvedSensorSpectralResponseValue =\n | {\n kind:\n \"effective-external-quantum-efficiency\";\n externalQuantumEfficiency: number;\n composition:\n | \"direct-effective-response\"\n | \"channel-filter-transmittance-times-detector-eqe\";\n channelFilterTransmittance?: number;\n detectorExternalQuantumEfficiency?: number;\n }\n | {\n kind:\n \"effective-spectral-responsivity\";\n amperesPerWatt: number;\n composition:\n \"direct-effective-response\";\n };\n\nexport interface ResolvedSensorSpectralResponse {\n profileId: string;\n colorSamplingProfileId: string;\n channelId: string;\n sourceResponseKind:\n SensorSpectralChannelResponse[\"kind\"];\n responseScope:\n SensorSpectralChannelResponse[\"responseScope\"];\n scientificStatus:\n SensorSpectralResponseScientificStatus;\n uncertainty:\n SensorSpectralResponseUncertainty;\n wavelengthNanometers: number;\n wavelengthBasis: SpectralWavelengthBasis;\n wavelengthBasisResolved: boolean;\n declaredWavelengthRangeNanometers: {\n minimum: number;\n maximum: number;\n };\n interpolation:\n \"piecewise-linear\";\n interpolationUsed: boolean;\n outsideRangeBehavior:\n \"fail-closed\";\n response:\n ResolvedSensorSpectralResponseValue;\n referenceConditions?:\n SensorSpectralReferenceConditions;\n conditionDependenceModeled: false;\n fieldAngleDependenceModeled: false;\n temperatureDependenceModeled: false;\n polarizationDependenceModeled: false;\n spectralIrradianceIntegrated: false;\n wavelengthIntegrationPerformed: false;\n qeResponsivityConversionPerformed: false;\n photonsCalculated: false;\n electronsCalculated: false;\n rawCodeValueProduced: false;\n componentEvidence: {\n profile: readonly EvidenceProvenance[];\n channel: readonly EvidenceProvenance[];\n curves:\n readonly (readonly EvidenceProvenance[])[];\n };\n}\n\nfunction requireReusableCurveEvidence(\n value: unknown,\n path: string\n): readonly EvidenceProvenance[] {\n const evidence = parseEvidenceList(\n value,\n path\n );\n\n if (\n !evidence.some(\n (entry) =>\n entry.reuseStatus ===\n \"reusable-data\" ||\n entry.reuseStatus ===\n \"photivra-owned\"\n )\n ) {\n throw new InvalidConfigurationError(\n path +\n \" must contain reusable-data or photivra-owned evidence because the curve contains reusable numeric data.\"\n );\n }\n\n return evidence;\n}\n\nfunction parseUncertainty(\n value: unknown,\n path: string\n): SensorSpectralResponseUncertainty {\n const record = requireRecord(value, path, null);\n rejectUnknownVariantFields(record, \"kind\", UNCERTAINTY_VARIANT_FIELDS, path);\n\n if (record.kind === \"relative\") {\n const fraction = requireFinite(\n record.fraction,\n path + \".fraction\"\n );\n if (fraction < 0) {\n throw new InvalidConfigurationError(\n path +\n \".fraction must be greater than or equal to zero.\"\n );\n }\n return {\n kind: \"relative\",\n fraction,\n basis: requireNonEmptyString(\n record.basis,\n path + \".basis\"\n )\n };\n }\n\n if (record.kind === \"not-quantified\") {\n return {\n kind: \"not-quantified\",\n limitation: requireNonEmptyString(\n record.limitation,\n path + \".limitation\"\n )\n };\n }\n\n throw new InvalidConfigurationError(\n path + \".kind is invalid.\"\n );\n}\n\nfunction parseReferenceConditions(\n value: unknown,\n path: string\n): SensorSpectralReferenceConditions {\n const record = requireRecord(value, path, [\"temperatureC\", \"incidenceAngleDegreesFromNormal\", \"polarization\"]);\n const temperatureC =\n record.temperatureC === undefined\n ? undefined\n : requireFinite(\n record.temperatureC,\n path + \".temperatureC\"\n );\n const incidenceAngleDegreesFromNormal =\n record.incidenceAngleDegreesFromNormal ===\n undefined\n ? undefined\n : requireFinite(\n record.incidenceAngleDegreesFromNormal,\n path +\n \".incidenceAngleDegreesFromNormal\"\n );\n\n if (\n incidenceAngleDegreesFromNormal !==\n undefined &&\n (incidenceAngleDegreesFromNormal < 0 ||\n incidenceAngleDegreesFromNormal > 90)\n ) {\n throw new InvalidConfigurationError(\n path +\n \".incidenceAngleDegreesFromNormal must be from 0 through 90 degrees.\"\n );\n }\n\n const polarization =\n record.polarization;\n if (\n polarization !== undefined &&\n polarization !== \"unpolarized\" &&\n polarization !== \"unspecified\"\n ) {\n throw new InvalidConfigurationError(\n path + \".polarization is invalid.\"\n );\n }\n\n if (\n temperatureC === undefined &&\n incidenceAngleDegreesFromNormal ===\n undefined &&\n polarization === undefined\n ) {\n throw new InvalidConfigurationError(\n path +\n \" must declare at least one reference condition.\"\n );\n }\n\n return {\n ...(temperatureC === undefined\n ? {}\n : { temperatureC }),\n ...(incidenceAngleDegreesFromNormal ===\n undefined\n ? {}\n : {\n incidenceAngleDegreesFromNormal\n }),\n ...(polarization === undefined\n ? {}\n : { polarization })\n };\n}\n\nfunction parseCurveBase(\n record: UnknownRecord,\n path: string\n): SpectralCurveBase {\n if (record.wavelengthUnit !== \"nm\") {\n throw new InvalidConfigurationError(\n path +\n '.wavelengthUnit must be \"nm\".'\n );\n }\n if (\n record.interpolation !==\n \"piecewise-linear\"\n ) {\n throw new InvalidConfigurationError(\n path +\n '.interpolation must be \"piecewise-linear\".'\n );\n }\n if (\n record.outsideRangeBehavior !==\n \"fail-closed\"\n ) {\n throw new InvalidConfigurationError(\n path +\n '.outsideRangeBehavior must be \"fail-closed\".'\n );\n }\n\n return {\n wavelengthUnit: \"nm\",\n wavelengthBasis:\n parseSpectralWavelengthBasis(\n record.wavelengthBasis,\n path + \".wavelengthBasis\"\n ),\n interpolation: \"piecewise-linear\",\n outsideRangeBehavior: \"fail-closed\",\n evidence:\n requireReusableCurveEvidence(\n record.evidence,\n path + \".evidence\"\n )\n };\n}\n\nfunction parseFractionSamples(\n value: unknown,\n path: string\n): readonly SpectralFractionSample[] {\n if (\n !Array.isArray(value) ||\n value.length < 2\n ) {\n throw new InvalidConfigurationError(\n path +\n \" must contain at least two wavelength samples.\"\n );\n }\n\n let previousWavelength =\n Number.NEGATIVE_INFINITY;\n\n return value.map((entry, index) => {\n const record = requireRecord(\n entry,\n path + \"[\" + index + \"]\", [\"wavelengthNanometers\", \"value\"]\n );\n const wavelengthNanometers =\n requirePositiveFinite(\n record.wavelengthNanometers,\n path +\n \"[\" +\n index +\n \"].wavelengthNanometers\"\n );\n\n if (\n wavelengthNanometers <=\n previousWavelength\n ) {\n throw new InvalidConfigurationError(\n path +\n \" wavelengths must be strictly increasing.\"\n );\n }\n previousWavelength =\n wavelengthNanometers;\n\n return {\n wavelengthNanometers,\n value: requireFraction(\n record.value,\n path +\n \"[\" +\n index +\n \"].value\"\n )\n };\n });\n}\n\nfunction parseResponsivitySamples(\n value: unknown,\n path: string\n): readonly SpectralResponsivitySample[] {\n if (\n !Array.isArray(value) ||\n value.length < 2\n ) {\n throw new InvalidConfigurationError(\n path +\n \" must contain at least two wavelength samples.\"\n );\n }\n\n let previousWavelength =\n Number.NEGATIVE_INFINITY;\n\n return value.map((entry, index) => {\n const record = requireRecord(\n entry,\n path + \"[\" + index + \"]\", [\"wavelengthNanometers\", \"amperesPerWatt\"]\n );\n const wavelengthNanometers =\n requirePositiveFinite(\n record.wavelengthNanometers,\n path +\n \"[\" +\n index +\n \"].wavelengthNanometers\"\n );\n\n if (\n wavelengthNanometers <=\n previousWavelength\n ) {\n throw new InvalidConfigurationError(\n path +\n \" wavelengths must be strictly increasing.\"\n );\n }\n previousWavelength =\n wavelengthNanometers;\n\n const amperesPerWatt =\n requireFinite(\n record.amperesPerWatt,\n path +\n \"[\" +\n index +\n \"].amperesPerWatt\"\n );\n if (amperesPerWatt < 0) {\n throw new InvalidConfigurationError(\n path +\n \"[\" +\n index +\n \"].amperesPerWatt must be greater than or equal to zero.\"\n );\n }\n\n return {\n wavelengthNanometers,\n amperesPerWatt\n };\n });\n}\n\nfunction parseFractionCurve(\n value: unknown,\n path: string\n): SpectralFractionCurve {\n const record = requireRecord(\n value,\n path, [\"wavelengthUnit\", \"interpolation\", \"outsideRangeBehavior\", \"wavelengthBasis\", \"evidence\", \"samples\"]\n );\n return {\n ...parseCurveBase(record, path),\n samples: parseFractionSamples(\n record.samples,\n path + \".samples\"\n )\n };\n}\n\nfunction parseResponsivityCurve(\n value: unknown,\n path: string\n): SpectralResponsivityCurve {\n const record = requireRecord(\n value,\n path, [\"wavelengthUnit\", \"interpolation\", \"outsideRangeBehavior\", \"wavelengthBasis\", \"evidence\", \"samples\"]\n );\n return {\n ...parseCurveBase(record, path),\n samples: parseResponsivitySamples(\n record.samples,\n path + \".samples\"\n )\n };\n}\n\nfunction validateCalibratedWavelengthBasis(\n scientificStatus:\n SensorSpectralResponseScientificStatus,\n curves: readonly (\n | SpectralFractionCurve\n | SpectralResponsivityCurve\n )[],\n path: string\n): void {\n if (\n scientificStatus === \"calibrated\" &&\n curves.some(\n (curve) =>\n curve.wavelengthBasis ===\n \"unspecified\"\n )\n ) {\n throw new InvalidConfigurationError(\n path +\n ' calibrated responses must declare wavelengthBasis \"air\" or \"vacuum\".'\n );\n }\n}\n\nfunction usableRange(\n curves: readonly (\n | SpectralFractionCurve\n | SpectralResponsivityCurve\n )[]\n): {\n minimum: number;\n maximum: number;\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\n if (!(minimum < maximum)) {\n throw new InvalidConfigurationError(\n \"Separable spectral-response component curves must have a non-empty overlapping wavelength interval.\"\n );\n }\n\n return { minimum, maximum };\n}\n\nfunction parseChannelResponse(\n value: unknown,\n path: string\n): SensorSpectralChannelResponse {\n const record = requireRecord(\n value,\n path, [\"channelId\", \"scientificStatus\", \"uncertainty\", \"evidence\", \"referenceConditions\", \"conditionDependence\", \"kind\", \"responseScope\", \"externalQuantumEfficiency\", \"spectralResponsivity\", \"combinationRule\", \"channelFilterTransmittance\", \"detectorExternalQuantumEfficiency\"]\n );\n const channelId =\n requireNonEmptyString(\n record.channelId,\n path + \".channelId\"\n );\n const scientificStatus =\n parseScientificStatus(\n record.scientificStatus,\n path + \".scientificStatus\"\n );\n const uncertainty =\n parseUncertainty(\n record.uncertainty,\n path + \".uncertainty\"\n );\n const evidence =\n parseEvidenceList(\n record.evidence,\n path + \".evidence\"\n );\n const referenceConditions =\n record.referenceConditions ===\n undefined\n ? undefined\n : parseReferenceConditions(\n record.referenceConditions,\n path + \".referenceConditions\"\n );\n\n if (\n record.conditionDependence !==\n \"not-modeled\"\n ) {\n throw new InvalidConfigurationError(\n path +\n '.conditionDependence must be \"not-modeled\".'\n );\n }\n\n const common = {\n channelId,\n scientificStatus,\n uncertainty,\n evidence,\n ...(referenceConditions === undefined\n ? {}\n : { referenceConditions }),\n conditionDependence:\n \"not-modeled\" as const\n };\n\n if (\n record.kind ===\n \"effective-external-quantum-efficiency\"\n ) {\n if (\n record.responseScope !==\n \"site-incident-effective-channel-response\" &&\n record.responseScope !==\n \"sensor-package-incident-effective-channel-response\"\n ) {\n throw new InvalidConfigurationError(\n path +\n \".responseScope is invalid.\"\n );\n }\n const curve = parseFractionCurve(\n record.externalQuantumEfficiency,\n path +\n \".externalQuantumEfficiency\"\n );\n validateCalibratedWavelengthBasis(\n scientificStatus,\n [curve],\n path\n );\n return {\n ...common,\n kind:\n \"effective-external-quantum-efficiency\",\n responseScope:\n record.responseScope,\n externalQuantumEfficiency:\n curve\n };\n }\n\n if (\n record.kind ===\n \"effective-spectral-responsivity\"\n ) {\n if (\n record.responseScope !==\n \"site-incident-effective-channel-response\" &&\n record.responseScope !==\n \"sensor-package-incident-effective-channel-response\"\n ) {\n throw new InvalidConfigurationError(\n path +\n \".responseScope is invalid.\"\n );\n }\n const curve =\n parseResponsivityCurve(\n record.spectralResponsivity,\n path +\n \".spectralResponsivity\"\n );\n validateCalibratedWavelengthBasis(\n scientificStatus,\n [curve],\n path\n );\n return {\n ...common,\n kind:\n \"effective-spectral-responsivity\",\n responseScope:\n record.responseScope,\n spectralResponsivity: curve\n };\n }\n\n if (\n record.kind ===\n \"separable-channel-filter-and-detector-eqe\"\n ) {\n if (\n record.responseScope !==\n \"site-incident-channel-filter-times-detector-eqe\"\n ) {\n throw new InvalidConfigurationError(\n path +\n \".responseScope is invalid.\"\n );\n }\n if (\n record.combinationRule !==\n \"multiply\"\n ) {\n throw new InvalidConfigurationError(\n path +\n '.combinationRule must be \"multiply\".'\n );\n }\n\n const filterCurve =\n parseFractionCurve(\n record.channelFilterTransmittance,\n path +\n \".channelFilterTransmittance\"\n );\n const detectorCurve =\n parseFractionCurve(\n record.detectorExternalQuantumEfficiency,\n path +\n \".detectorExternalQuantumEfficiency\"\n );\n\n if (\n filterCurve.wavelengthBasis !==\n detectorCurve.wavelengthBasis\n ) {\n throw new InvalidConfigurationError(\n path +\n \" separable curves must use the same wavelength basis.\"\n );\n }\n\n usableRange([\n filterCurve,\n detectorCurve\n ]);\n validateCalibratedWavelengthBasis(\n scientificStatus,\n [\n filterCurve,\n detectorCurve\n ],\n path\n );\n\n return {\n ...common,\n kind:\n \"separable-channel-filter-and-detector-eqe\",\n responseScope:\n \"site-incident-channel-filter-times-detector-eqe\",\n combinationRule: \"multiply\",\n channelFilterTransmittance:\n filterCurve,\n detectorExternalQuantumEfficiency:\n detectorCurve\n };\n }\n\n throw new InvalidConfigurationError(\n path + \".kind is invalid.\"\n );\n}\n\n/**\n * Parses wavelength-dependent response data for sensor channels. Responses are\n * never guessed from channel names or the CFA type.\n *\n * An embedded multi-point curve needs evidence that the data is reusable or\n * owned by Photivra. A public reference without reuse permission can support\n * metadata, but does not by itself allow copying numeric curve data into the engine.\n */\nexport function parseSensorSpectralResponseProfile(\n value: unknown\n): SensorSpectralResponseProfile {\n const profile = requireRecord(\n value,\n \"sensorSpectralResponse\", [\"schemaVersion\", \"channels\", \"profileId\", \"colorSamplingProfileId\", \"evidence\"]\n );\n\n if (\n profile.schemaVersion !== \"0.1.0\"\n ) {\n throw new InvalidConfigurationError(\n 'sensorSpectralResponse.schemaVersion must be \"0.1.0\".'\n );\n }\n\n if (\n !Array.isArray(profile.channels) ||\n profile.channels.length === 0\n ) {\n throw new InvalidConfigurationError(\n \"sensorSpectralResponse.channels must be a non-empty array.\"\n );\n }\n\n const channels =\n profile.channels.map(\n (entry, index) =>\n parseChannelResponse(\n entry,\n \"sensorSpectralResponse.channels[\" +\n index +\n \"]\"\n )\n );\n\n const channelIds = channels.map(\n (entry) => entry.channelId\n );\n if (\n new Set(channelIds).size !==\n channelIds.length\n ) {\n throw new InvalidConfigurationError(\n \"sensorSpectralResponse.channels must not contain duplicate channel IDs.\"\n );\n }\n\n return {\n schemaVersion: \"0.1.0\",\n profileId: requireNonEmptyString(\n profile.profileId,\n \"sensorSpectralResponse.profileId\"\n ),\n colorSamplingProfileId:\n requireNonEmptyString(\n profile.colorSamplingProfileId,\n \"sensorSpectralResponse.colorSamplingProfileId\"\n ),\n evidence: parseEvidenceList(\n profile.evidence,\n \"sensorSpectralResponse.evidence\"\n ),\n channels\n };\n}\n\n/**\n * Creates an exact binding to one parsed response channel's data, so a swapped numeric curve cannot\n * hide behind an unchanged profile ID.\n *\n * The binding includes the channel's EQE or A/W curve (or its separately evidenced filter × detector\n * EQE curves), its range, interpolation and wavelength basis, and the exact canonical channel data.\n * The channel is parsed first and serialized with sorted object keys, so property order never changes\n * the binding and a malformed channel throws.\n * @param response - The input; see `SensorSpectralChannelResponse` for its fields, units and coordinates.\n * @returns The result. Success does not imply calibration.\n *\n * @see docs/API_REFERENCE.md for the package export and its full type definitions.\n */\nexport function createSensorSpectralResponseChannelBinding(\n response: SensorSpectralChannelResponse\n): SensorSpectralResponseChannelBinding {\n return {\n format:\n SENSOR_SPECTRAL_RESPONSE_CHANNEL_BINDING_FORMAT,\n // Parse first so the binding never repeats unvalidated input, then sort keys\n // so channels with the same content bind identically whatever their key order.\n canonicalJson:\n stringifyCanonicalJson(\n parseChannelResponse(\n response,\n \"sensorSpectralResponseChannel\"\n ),\n {\n undefinedObjectProperties: \"omit\",\n nonFiniteNumberMessage:\n \"sensorSpectralResponseChannel must contain finite numbers.\",\n unsupportedValueMessage:\n \"sensorSpectralResponseChannel must be plain JSON data.\"\n }\n )\n };\n}\n\n/**\n * Rejects a channel binding stored in a reduction unless it uses the current format. A v1 binding from\n * package 1.5.0 or earlier fails with an explicit version error: a different serializer produced it, so\n * comparing it would report a misleading data mismatch. Recompute the spectral quadrature and reduction\n * with this release to get a v2 binding.\n * Internal to the repository; not exported from the package.\n */\nexport function requireCurrentSensorSpectralResponseChannelBinding(\n binding: unknown\n): void {\n const format =\n typeof binding === \"object\" && binding !== null\n ? (binding as { format?: unknown }).format\n : undefined;\n if (format === SENSOR_SPECTRAL_RESPONSE_CHANNEL_BINDING_FORMAT) return;\n throw new InvalidConfigurationError(\n format === \"photivra-spectral-channel-response-canonical-json-v1\"\n ? \"Response-channel binding format v1 (package 1.5.0 and earlier) is not supported; recompute the spectral quadrature and reduction to obtain a v2 binding.\"\n : \"Response-channel binding has an unsupported format; expected \" + SENSOR_SPECTRAL_RESPONSE_CHANNEL_BINDING_FORMAT + \".\"\n );\n}\n\nfunction topologyChannelIds(\n profile: SensorColorSamplingProfile\n): readonly string[] {\n if (\n profile.layout.kind ===\n \"layered\"\n ) {\n throw new InvalidScientificInputError(\n \"Layered color sampling remains spatially unresolved; schema 0.1.0 spectral response cannot yet be composed into per-site layered sampling.\"\n );\n }\n\n if (\n profile.layout.kind ===\n \"monochrome\"\n ) {\n return [profile.layout.channelId];\n }\n\n return [\n ...new Set(\n profile.layout.siteChannelIds\n )\n ];\n}\n\nfunction interpolateFraction(\n curve: SpectralFractionCurve,\n wavelengthNanometers: number\n): {\n value: number;\n interpolationUsed: boolean;\n} {\n return interpolateCurve(\n curve.samples,\n wavelengthNanometers,\n (sample) => sample.value\n );\n}\n\nfunction interpolateResponsivity(\n curve: SpectralResponsivityCurve,\n wavelengthNanometers: number\n): {\n value: number;\n interpolationUsed: boolean;\n} {\n return interpolateCurve(\n curve.samples,\n wavelengthNanometers,\n (sample) =>\n sample.amperesPerWatt\n );\n}\n\nfunction interpolateCurve<T extends {\n wavelengthNanometers: number;\n}>(\n samples: readonly T[],\n wavelengthNanometers: number,\n valueOf: (sample: T) => number\n): {\n value: number;\n interpolationUsed: boolean;\n} {\n const first = samples[0]!;\n const last =\n samples[samples.length - 1]!;\n\n if (\n wavelengthNanometers <\n first.wavelengthNanometers ||\n wavelengthNanometers >\n last.wavelengthNanometers\n ) {\n throw new InvalidScientificInputError(\n \"Requested wavelength is outside the declared spectral-response range; extrapolation/zero-fill is not permitted.\"\n );\n }\n\n for (\n let index = 0;\n index < samples.length;\n index += 1\n ) {\n const sample = samples[index]!;\n if (\n wavelengthNanometers ===\n sample.wavelengthNanometers\n ) {\n return {\n value: valueOf(sample),\n interpolationUsed: false\n };\n }\n\n if (\n wavelengthNanometers <\n sample.wavelengthNanometers\n ) {\n const previous =\n samples[index - 1]!;\n const phase =\n (wavelengthNanometers -\n previous.wavelengthNanometers) /\n (sample.wavelengthNanometers -\n previous.wavelengthNanometers);\n return {\n value:\n valueOf(previous) +\n phase *\n (valueOf(sample) -\n valueOf(previous)),\n interpolationUsed: true\n };\n }\n }\n\n return {\n value: valueOf(last),\n interpolationUsed: false\n };\n}\n\n/**\n * Returns one sensor channel's response at one wavelength.\n *\n * It checks the response profile against the exact colour-sampling profile,\n * rejects layered layouts whose sampling is not defined, uses only the declared\n * piecewise-linear interpolation, and never extrapolates.\n *\n * Both a direct effective response and separate filter × detector QE curves are\n * supported. QE and A/W responsivity are different physical quantities; this\n * API does not convert between them.\n */\nexport function resolveParsedSensorSpectralResponseAtWavelength(\n input:\n ResolveSensorSpectralResponseAtWavelengthInput\n): CalculationResult<ResolvedSensorSpectralResponse> {\n const colorProfile =\n input.colorSamplingProfile;\n const responseProfile =\n input.spectralResponseProfile;\n\n if (\n responseProfile\n .colorSamplingProfileId !==\n colorProfile.profileId\n ) {\n throw new InvalidScientificInputError(\n \"Spectral-response colorSamplingProfileId must match the supplied color-sampling profile.\"\n );\n }\n\n const knownChannelIds =\n topologyChannelIds(colorProfile);\n const knownSet =\n new Set(knownChannelIds);\n\n for (\n const response of\n responseProfile.channels\n ) {\n if (\n !knownSet.has(\n response.channelId\n )\n ) {\n throw new InvalidScientificInputError(\n \"Spectral-response profile declares a channel ID that is not present in the linked color-sampling topology.\"\n );\n }\n }\n\n if (\n typeof input.channelId !==\n \"string\" ||\n input.channelId.trim().length ===\n 0\n ) {\n throw new InvalidConfigurationError(\n \"channelId must be a non-empty string.\"\n );\n }\n if (\n !knownSet.has(input.channelId)\n ) {\n throw new InvalidScientificInputError(\n \"channelId is not present in the linked color-sampling topology.\"\n );\n }\n\n const response =\n responseProfile.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 if (\n typeof input.wavelengthNanometers !==\n \"number\" ||\n !Number.isFinite(\n input.wavelengthNanometers\n ) ||\n input.wavelengthNanometers <= 0\n ) {\n throw new InvalidScientificInputError(\n \"wavelengthNanometers must be finite and greater than zero.\"\n );\n }\n\n if (\n input.wavelengthBasis !==\n \"air\" &&\n input.wavelengthBasis !==\n \"vacuum\" &&\n input.wavelengthBasis !==\n \"unspecified\"\n ) {\n throw new InvalidConfigurationError(\n \"wavelengthBasis is invalid.\"\n );\n }\n\n const curves:\n (\n | SpectralFractionCurve\n | SpectralResponsivityCurve\n )[] = [];\n\n let resolvedValue:\n ResolvedSensorSpectralResponseValue;\n let interpolationUsed: boolean;\n let wavelengthBasis:\n SpectralWavelengthBasis;\n\n if (\n response.kind ===\n \"effective-external-quantum-efficiency\"\n ) {\n const curve =\n response.externalQuantumEfficiency;\n curves.push(curve);\n wavelengthBasis =\n curve.wavelengthBasis;\n\n if (\n input.wavelengthBasis !==\n wavelengthBasis\n ) {\n throw new InvalidScientificInputError(\n \"Requested wavelengthBasis must exactly match the response curve; Photivra does not convert air/vacuum/unspecified wavelength coordinates implicitly.\"\n );\n }\n\n const resolved =\n interpolateFraction(\n curve,\n input.wavelengthNanometers\n );\n interpolationUsed =\n resolved.interpolationUsed;\n resolvedValue = {\n kind:\n \"effective-external-quantum-efficiency\",\n externalQuantumEfficiency:\n resolved.value,\n composition:\n \"direct-effective-response\"\n };\n } else if (\n response.kind ===\n \"effective-spectral-responsivity\"\n ) {\n const curve =\n response.spectralResponsivity;\n curves.push(curve);\n wavelengthBasis =\n curve.wavelengthBasis;\n\n if (\n input.wavelengthBasis !==\n wavelengthBasis\n ) {\n throw new InvalidScientificInputError(\n \"Requested wavelengthBasis must exactly match the response curve; Photivra does not convert air/vacuum/unspecified wavelength coordinates implicitly.\"\n );\n }\n\n const resolved =\n interpolateResponsivity(\n curve,\n input.wavelengthNanometers\n );\n interpolationUsed =\n resolved.interpolationUsed;\n resolvedValue = {\n kind:\n \"effective-spectral-responsivity\",\n amperesPerWatt:\n resolved.value,\n composition:\n \"direct-effective-response\"\n };\n } else {\n const filterCurve =\n response\n .channelFilterTransmittance;\n const detectorCurve =\n response\n .detectorExternalQuantumEfficiency;\n curves.push(\n filterCurve,\n detectorCurve\n );\n wavelengthBasis =\n filterCurve.wavelengthBasis;\n\n if (\n input.wavelengthBasis !==\n wavelengthBasis\n ) {\n throw new InvalidScientificInputError(\n \"Requested wavelengthBasis must exactly match the separable response curves.\"\n );\n }\n\n const filter =\n interpolateFraction(\n filterCurve,\n input.wavelengthNanometers\n );\n const detector =\n interpolateFraction(\n detectorCurve,\n input.wavelengthNanometers\n );\n\n interpolationUsed =\n filter.interpolationUsed ||\n detector.interpolationUsed;\n\n resolvedValue = {\n kind:\n \"effective-external-quantum-efficiency\",\n externalQuantumEfficiency:\n filter.value *\n detector.value,\n composition:\n \"channel-filter-transmittance-times-detector-eqe\",\n channelFilterTransmittance:\n filter.value,\n detectorExternalQuantumEfficiency:\n detector.value\n };\n }\n\n const range = usableRange(curves);\n\n return calculatedResult(\n {\n profileId:\n responseProfile.profileId,\n colorSamplingProfileId:\n responseProfile\n .colorSamplingProfileId,\n channelId: response.channelId,\n sourceResponseKind:\n response.kind,\n responseScope:\n response.responseScope,\n scientificStatus:\n response.scientificStatus,\n uncertainty:\n response.uncertainty,\n wavelengthNanometers:\n input.wavelengthNanometers,\n wavelengthBasis,\n wavelengthBasisResolved:\n wavelengthBasis !==\n \"unspecified\",\n declaredWavelengthRangeNanometers:\n range,\n interpolation:\n \"piecewise-linear\",\n interpolationUsed,\n outsideRangeBehavior:\n \"fail-closed\",\n response: resolvedValue,\n ...(response.referenceConditions ===\n undefined\n ? {}\n : {\n referenceConditions:\n response.referenceConditions\n }),\n conditionDependenceModeled:\n false,\n fieldAngleDependenceModeled:\n false,\n temperatureDependenceModeled:\n false,\n polarizationDependenceModeled:\n false,\n spectralIrradianceIntegrated:\n false,\n wavelengthIntegrationPerformed:\n false,\n qeResponsivityConversionPerformed:\n false,\n photonsCalculated: false,\n electronsCalculated: false,\n rawCodeValueProduced: false,\n componentEvidence: {\n profile:\n responseProfile.evidence,\n channel: response.evidence,\n curves: curves.map(\n (curve) => curve.evidence\n )\n }\n },\n \"sensor-spectral-response-lookup\",\n \"1.0.0\",\n [\n \"Channel IDs are exact semantic identifiers from the linked color-sampling topology; names do not imply spectral behavior.\",\n \"Piecewise-linear interpolation is applied only inside the declared wavelength range; no extrapolation or implicit zero response is permitted.\",\n \"Air, vacuum and unspecified wavelength bases are never converted implicitly.\",\n \"Direct effective response is preserved when decomposition is not established; separable filter×detector-QE is composed only when explicitly declared.\",\n \"Spectral responsivity in A/W and external QE remain distinct physical representations and are not converted by this API.\",\n \"Reference-condition metadata does not create field-angle, temperature or polarization dependence models.\",\n \"Spectral irradiance integration, wavelength integration, photon/electron conversion and RAW-domain output remain future work.\"\n ]\n );\n}\n\n/**\n * Public spectral-response lookup for untrusted, unparsed profiles.\n *\n * The profiles are parsed here, once. Internal code that has already parsed\n * both profiles can call resolveParsedSensorSpectralResponseAtWavelength, to\n * avoid parsing them again for every wavelength node.\n */\nexport function resolveSensorSpectralResponseAtWavelength(\n input:\n ResolveSensorSpectralResponseAtWavelengthInput\n): CalculationResult<ResolvedSensorSpectralResponse> {\n requireInputKeys(input, \"resolveSensorSpectralResponseAtWavelength\", [\"colorSamplingProfile\", \"spectralResponseProfile\", \"channelId\", \"wavelengthNanometers\", \"wavelengthBasis\"]);\n const colorProfile =\n parseSensorColorSamplingProfile(\n input.colorSamplingProfile\n );\n const responseProfile =\n parseSensorSpectralResponseProfile(\n input.spectralResponseProfile\n );\n\n return resolveParsedSensorSpectralResponseAtWavelength({\n ...input,\n colorSamplingProfile:\n colorProfile,\n spectralResponseProfile:\n responseProfile\n });\n}\n"]}
@@ -17,12 +17,7 @@ import { approximationResult } from "../core/calculation-result.js";
17
17
  import { InvalidScientificInputError, requirePositiveFinite } from "../core/validation.js";
18
18
  import { portableHypot } from "../core/portable-math.js";
19
19
  import { calculateThinLensImageDistance } from "../optics/thin-lens.js";
20
- import { rejectUnknownFields, requireInputKeys, requireInputObject } from "../core/record-validation.js";
21
- function requireFinite(name, value) {
22
- if (!Number.isFinite(value)) {
23
- throw new InvalidScientificInputError(`${name} must be finite.`);
24
- }
25
- }
20
+ import { rejectUnknownFields, requireInputKeys, requireInputObject, assertNamedFinite as requireFinite } from "../core/record-validation.js";
26
21
  function projectAngularDisplacement(projectionDistanceMm, yawRad, pitchRad, pixelPitchMicrometers) {
27
22
  // Physical right-hand rule in the left-handed camera basis: positive yaw
28
23
  // (turn left) moves content toward +X; positive pitch (tilt up) toward -Y.
@@ -1 +1 @@
1
- {"version":3,"file":"camera-shake.js","sourceRoot":"","sources":["../../src/stabilization/camera-shake.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Estimates blur on the sensor from a simple handheld-shake profile: the camera turns at a constant\n * angular velocity. An optional stabilization rating reduces that motion.\n *\n * With focusDistanceM, angular motion is projected using the ideal thin-lens sensor distance, like\n * the other focus-aware projection functions. Without it, the nominal focal length is used, which\n * keeps the older approximation.\n *\n * The optical projection is physically calculated. But turning a stabilization rating in stops into\n * residual angular motion by 2^-stops is an educational approximation. It is not a CIPA DC-011\n * measurement or a real-camera rating.\n * @see docs/STABILIZATION.md for the equations, units, coordinate conventions, known blockers and\n * support limits.\n */\n\nimport {\n approximationResult,\n type CalculationResult\n} from \"../core/calculation-result.js\";\nimport {\n InvalidScientificInputError,\n requirePositiveFinite\n} from \"../core/validation.js\";\nimport { portableHypot } from \"../core/portable-math.js\";\nimport { calculateThinLensImageDistance } from \"../optics/thin-lens.js\";\nimport { rejectUnknownFields, requireInputKeys, requireInputObject } from \"../core/record-validation.js\";\n\nexport interface EstimateCameraShakeBlurInput {\n focalLengthMm: number;\n shutterSeconds: number;\n /**\n * Constant camera angular velocity in radians per second. Signs follow the\n * same physical right-hand rule as calculateCameraRotationImageMapping().\n * The camera axes are +X right, +Y up, +Z forward (a left-handed basis).\n * - Positive yaw turns the optical axis left and moves still image content\n * toward +X.\n * - Positive pitch tilts the optical axis up and moves still image content\n * toward -Y.\n */\n angularVelocityRadPerSec: {\n yaw: number;\n pitch: number;\n };\n stabilizationStopsEquivalent: number;\n /** Optional focus distance in metres for focus-aware sensor-plane projection. */\n focusDistanceM?: number;\n pixelPitchMicrometers?: number;\n}\n\n/**\n * How far a still world point on the optical axis moves in the image during\n * the exposure, in image-plane axes (+X right, +Y up). For a single axis it\n * equals the deltaMm from calculateCameraRotationImageMapping() at the\n * optical axis.\n */\nexport interface CameraShakeBlurSample {\n /** +X-right image displacement in millimetres: +d·tan(yaw·t). */\n deltaXmm: number;\n /** +Y-up image displacement in millimetres: -d·tan(pitch·t). */\n deltaYmm: number;\n distanceMm: number;\n distancePixels?: number;\n deltaXPixels?: number;\n deltaYPixels?: number;\n}\n\nexport interface CameraShakeEstimate {\n residualMotionFactor: number;\n projectionDistanceMm: number;\n unstabilized: CameraShakeBlurSample;\n stabilized: CameraShakeBlurSample;\n}\n\nfunction requireFinite(name: string, value: number): void {\n if (!Number.isFinite(value)) {\n throw new InvalidScientificInputError(`${name} must be finite.`);\n }\n}\n\nfunction projectAngularDisplacement(\n projectionDistanceMm: number,\n yawRad: number,\n pitchRad: number,\n pixelPitchMicrometers?: number\n): CameraShakeBlurSample {\n // Physical right-hand rule in the left-handed camera basis: positive yaw\n // (turn left) moves content toward +X; positive pitch (tilt up) toward -Y.\n const deltaXmm = projectionDistanceMm * Math.tan(yawRad);\n // \"0 -\" keeps zero pitch at +0 rather than -0.\n const deltaYmm = 0 - projectionDistanceMm * Math.tan(pitchRad);\n const distanceMm = portableHypot(deltaXmm, deltaYmm);\n\n if (pixelPitchMicrometers === undefined) {\n return { deltaXmm, deltaYmm, distanceMm };\n }\n\n const pitchMm = pixelPitchMicrometers / 1000;\n return {\n deltaXmm,\n deltaYmm,\n distanceMm,\n distancePixels: distanceMm / pitchMm,\n deltaXPixels: deltaXmm / pitchMm,\n deltaYPixels: deltaYmm / pitchMm\n };\n}\n\n/**\n * Estimates blur on the sensor from a simple handheld-shake profile, where\n * the camera turns at a constant angular velocity. An optional stabilization\n * rating reduces that motion.\n *\n * With focusDistanceM, angular motion is projected using the ideal thin-lens\n * sensor distance, like the other focus-aware projection functions. Without\n * it, the nominal focal length is used, which keeps the older approximation.\n *\n * The optical projection is physically calculated. But turning a\n * stabilization rating in stops into residual angular motion by 2^-stops is\n * an educational approximation. It is not a CIPA DC-011 measurement or a\n * real-camera rating.\n *\n * Each axis's rotation during the exposure must stay below a quarter turn\n * (π/2 rad).\n */\nexport function estimateCameraShakeBlur(\n input: EstimateCameraShakeBlurInput\n): CalculationResult<CameraShakeEstimate> {\n requireInputKeys(input, \"estimateCameraShakeBlur\", [\"focalLengthMm\", \"shutterSeconds\", \"angularVelocityRadPerSec\", \"stabilizationStopsEquivalent\", \"focusDistanceM\", \"pixelPitchMicrometers\"]);\n requirePositiveFinite(\"focalLengthMm\", input.focalLengthMm);\n requirePositiveFinite(\"shutterSeconds\", input.shutterSeconds);\n requireInputObject(input.angularVelocityRadPerSec, \"angularVelocityRadPerSec\");\n rejectUnknownFields(input.angularVelocityRadPerSec, [\"yaw\", \"pitch\"], \"angularVelocityRadPerSec\");\n requireFinite(\n \"angularVelocityRadPerSec.yaw\",\n input.angularVelocityRadPerSec.yaw\n );\n requireFinite(\n \"angularVelocityRadPerSec.pitch\",\n input.angularVelocityRadPerSec.pitch\n );\n\n if (\n !Number.isFinite(input.stabilizationStopsEquivalent) ||\n input.stabilizationStopsEquivalent < 0\n ) {\n throw new InvalidScientificInputError(\n \"stabilizationStopsEquivalent must be finite and greater than or equal to zero.\"\n );\n }\n\n if (input.pixelPitchMicrometers !== undefined) {\n requirePositiveFinite(\n \"pixelPitchMicrometers\",\n input.pixelPitchMicrometers\n );\n }\n\n const focusProjection =\n input.focusDistanceM === undefined\n ? undefined\n : calculateThinLensImageDistance({\n focalLengthMm: input.focalLengthMm,\n objectDistanceM: input.focusDistanceM\n });\n const projectionDistanceMm =\n focusProjection?.value.imageDistanceMm ?? input.focalLengthMm;\n const yawRad =\n input.angularVelocityRadPerSec.yaw * input.shutterSeconds;\n const pitchRad =\n input.angularVelocityRadPerSec.pitch * input.shutterSeconds;\n // tan() flips sign past a quarter turn, which would break the sign\n // convention. The rotation mapping likewise rejects a world ray that maps to\n // or behind the camera plane.\n if (Math.abs(yawRad) >= Math.PI / 2 || Math.abs(pitchRad) >= Math.PI / 2) {\n throw new InvalidScientificInputError(\n \"Camera-shake rotation over the exposure must stay below a quarter turn (|angular velocity × shutterSeconds| < π/2 rad) on each axis.\"\n );\n }\n const residualMotionFactor =\n 2 ** -input.stabilizationStopsEquivalent;\n\n return approximationResult(\n {\n residualMotionFactor,\n projectionDistanceMm,\n unstabilized: projectAngularDisplacement(\n projectionDistanceMm,\n yawRad,\n pitchRad,\n input.pixelPitchMicrometers\n ),\n stabilized: projectAngularDisplacement(\n projectionDistanceMm,\n yawRad * residualMotionFactor,\n pitchRad * residualMotionFactor,\n input.pixelPitchMicrometers\n )\n },\n focusProjection === undefined\n ? \"constant-angular-velocity-stabilization-equivalent\"\n : \"focus-aware-constant-angular-velocity-stabilization-equivalent\",\n \"2.0.0\",\n [\n \"Camera rotation is constant during the exposure\",\n focusProjection === undefined\n ? \"Nominal focal length is used as the infinity-focus projection distance\"\n : \"Thin-lens image distance for the selected focus plane is used as the projection distance\",\n \"Yaw and pitch are treated independently\",\n \"Positive yaw/pitch follow the physical right-hand rule about camera +Y/+X (yaw left moves content toward +X; pitch up moves content toward -Y)\",\n \"Equivalent stabilization stops attenuate angular displacement by 2^-stops\",\n \"This is not a CIPA DC-011 compliance measurement\",\n \"Roll, translation, subject motion, and stabilization limits are not modeled\"\n ]\n );\n}\n"]}
1
+ {"version":3,"file":"camera-shake.js","sourceRoot":"","sources":["../../src/stabilization/camera-shake.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Estimates blur on the sensor from a simple handheld-shake profile: the camera turns at a constant\n * angular velocity. An optional stabilization rating reduces that motion.\n *\n * With focusDistanceM, angular motion is projected using the ideal thin-lens sensor distance, like\n * the other focus-aware projection functions. Without it, the nominal focal length is used, which\n * keeps the older approximation.\n *\n * The optical projection is physically calculated. But turning a stabilization rating in stops into\n * residual angular motion by 2^-stops is an educational approximation. It is not a CIPA DC-011\n * measurement or a real-camera rating.\n * @see docs/STABILIZATION.md for the equations, units, coordinate conventions, known blockers and\n * support limits.\n */\n\nimport {\n approximationResult,\n type CalculationResult\n} from \"../core/calculation-result.js\";\nimport {\n InvalidScientificInputError,\n requirePositiveFinite\n} from \"../core/validation.js\";\nimport { portableHypot } from \"../core/portable-math.js\";\nimport { calculateThinLensImageDistance } from \"../optics/thin-lens.js\";\nimport { rejectUnknownFields, requireInputKeys, requireInputObject, assertNamedFinite as requireFinite } from \"../core/record-validation.js\";\n\nexport interface EstimateCameraShakeBlurInput {\n focalLengthMm: number;\n shutterSeconds: number;\n /**\n * Constant camera angular velocity in radians per second. Signs follow the\n * same physical right-hand rule as calculateCameraRotationImageMapping().\n * The camera axes are +X right, +Y up, +Z forward (a left-handed basis).\n * - Positive yaw turns the optical axis left and moves still image content\n * toward +X.\n * - Positive pitch tilts the optical axis up and moves still image content\n * toward -Y.\n */\n angularVelocityRadPerSec: {\n yaw: number;\n pitch: number;\n };\n stabilizationStopsEquivalent: number;\n /** Optional focus distance in metres for focus-aware sensor-plane projection. */\n focusDistanceM?: number;\n pixelPitchMicrometers?: number;\n}\n\n/**\n * How far a still world point on the optical axis moves in the image during\n * the exposure, in image-plane axes (+X right, +Y up). For a single axis it\n * equals the deltaMm from calculateCameraRotationImageMapping() at the\n * optical axis.\n */\nexport interface CameraShakeBlurSample {\n /** +X-right image displacement in millimetres: +d·tan(yaw·t). */\n deltaXmm: number;\n /** +Y-up image displacement in millimetres: -d·tan(pitch·t). */\n deltaYmm: number;\n distanceMm: number;\n distancePixels?: number;\n deltaXPixels?: number;\n deltaYPixels?: number;\n}\n\nexport interface CameraShakeEstimate {\n residualMotionFactor: number;\n projectionDistanceMm: number;\n unstabilized: CameraShakeBlurSample;\n stabilized: CameraShakeBlurSample;\n}\n\nfunction projectAngularDisplacement(\n projectionDistanceMm: number,\n yawRad: number,\n pitchRad: number,\n pixelPitchMicrometers?: number\n): CameraShakeBlurSample {\n // Physical right-hand rule in the left-handed camera basis: positive yaw\n // (turn left) moves content toward +X; positive pitch (tilt up) toward -Y.\n const deltaXmm = projectionDistanceMm * Math.tan(yawRad);\n // \"0 -\" keeps zero pitch at +0 rather than -0.\n const deltaYmm = 0 - projectionDistanceMm * Math.tan(pitchRad);\n const distanceMm = portableHypot(deltaXmm, deltaYmm);\n\n if (pixelPitchMicrometers === undefined) {\n return { deltaXmm, deltaYmm, distanceMm };\n }\n\n const pitchMm = pixelPitchMicrometers / 1000;\n return {\n deltaXmm,\n deltaYmm,\n distanceMm,\n distancePixels: distanceMm / pitchMm,\n deltaXPixels: deltaXmm / pitchMm,\n deltaYPixels: deltaYmm / pitchMm\n };\n}\n\n/**\n * Estimates blur on the sensor from a simple handheld-shake profile, where\n * the camera turns at a constant angular velocity. An optional stabilization\n * rating reduces that motion.\n *\n * With focusDistanceM, angular motion is projected using the ideal thin-lens\n * sensor distance, like the other focus-aware projection functions. Without\n * it, the nominal focal length is used, which keeps the older approximation.\n *\n * The optical projection is physically calculated. But turning a\n * stabilization rating in stops into residual angular motion by 2^-stops is\n * an educational approximation. It is not a CIPA DC-011 measurement or a\n * real-camera rating.\n *\n * Each axis's rotation during the exposure must stay below a quarter turn\n * (π/2 rad).\n */\nexport function estimateCameraShakeBlur(\n input: EstimateCameraShakeBlurInput\n): CalculationResult<CameraShakeEstimate> {\n requireInputKeys(input, \"estimateCameraShakeBlur\", [\"focalLengthMm\", \"shutterSeconds\", \"angularVelocityRadPerSec\", \"stabilizationStopsEquivalent\", \"focusDistanceM\", \"pixelPitchMicrometers\"]);\n requirePositiveFinite(\"focalLengthMm\", input.focalLengthMm);\n requirePositiveFinite(\"shutterSeconds\", input.shutterSeconds);\n requireInputObject(input.angularVelocityRadPerSec, \"angularVelocityRadPerSec\");\n rejectUnknownFields(input.angularVelocityRadPerSec, [\"yaw\", \"pitch\"], \"angularVelocityRadPerSec\");\n requireFinite(\n \"angularVelocityRadPerSec.yaw\",\n input.angularVelocityRadPerSec.yaw\n );\n requireFinite(\n \"angularVelocityRadPerSec.pitch\",\n input.angularVelocityRadPerSec.pitch\n );\n\n if (\n !Number.isFinite(input.stabilizationStopsEquivalent) ||\n input.stabilizationStopsEquivalent < 0\n ) {\n throw new InvalidScientificInputError(\n \"stabilizationStopsEquivalent must be finite and greater than or equal to zero.\"\n );\n }\n\n if (input.pixelPitchMicrometers !== undefined) {\n requirePositiveFinite(\n \"pixelPitchMicrometers\",\n input.pixelPitchMicrometers\n );\n }\n\n const focusProjection =\n input.focusDistanceM === undefined\n ? undefined\n : calculateThinLensImageDistance({\n focalLengthMm: input.focalLengthMm,\n objectDistanceM: input.focusDistanceM\n });\n const projectionDistanceMm =\n focusProjection?.value.imageDistanceMm ?? input.focalLengthMm;\n const yawRad =\n input.angularVelocityRadPerSec.yaw * input.shutterSeconds;\n const pitchRad =\n input.angularVelocityRadPerSec.pitch * input.shutterSeconds;\n // tan() flips sign past a quarter turn, which would break the sign\n // convention. The rotation mapping likewise rejects a world ray that maps to\n // or behind the camera plane.\n if (Math.abs(yawRad) >= Math.PI / 2 || Math.abs(pitchRad) >= Math.PI / 2) {\n throw new InvalidScientificInputError(\n \"Camera-shake rotation over the exposure must stay below a quarter turn (|angular velocity × shutterSeconds| < π/2 rad) on each axis.\"\n );\n }\n const residualMotionFactor =\n 2 ** -input.stabilizationStopsEquivalent;\n\n return approximationResult(\n {\n residualMotionFactor,\n projectionDistanceMm,\n unstabilized: projectAngularDisplacement(\n projectionDistanceMm,\n yawRad,\n pitchRad,\n input.pixelPitchMicrometers\n ),\n stabilized: projectAngularDisplacement(\n projectionDistanceMm,\n yawRad * residualMotionFactor,\n pitchRad * residualMotionFactor,\n input.pixelPitchMicrometers\n )\n },\n focusProjection === undefined\n ? \"constant-angular-velocity-stabilization-equivalent\"\n : \"focus-aware-constant-angular-velocity-stabilization-equivalent\",\n \"2.0.0\",\n [\n \"Camera rotation is constant during the exposure\",\n focusProjection === undefined\n ? \"Nominal focal length is used as the infinity-focus projection distance\"\n : \"Thin-lens image distance for the selected focus plane is used as the projection distance\",\n \"Yaw and pitch are treated independently\",\n \"Positive yaw/pitch follow the physical right-hand rule about camera +Y/+X (yaw left moves content toward +X; pitch up moves content toward -Y)\",\n \"Equivalent stabilization stops attenuate angular displacement by 2^-stops\",\n \"This is not a CIPA DC-011 compliance measurement\",\n \"Roll, translation, subject motion, and stabilization limits are not modeled\"\n ]\n );\n}\n"]}
@@ -17,7 +17,8 @@ import { approximationResult } from "../core/calculation-result.js";
17
17
  import { InvalidConfigurationError } from "../core/configuration-error.js";
18
18
  import { parseEvidenceList } from "../core/evidence-provenance.js";
19
19
  import { InvalidScientificInputError, requirePositiveInteger } from "../core/validation.js";
20
- import { requireInputKeys, requireInputObject, rejectUnknownFields, rejectUnknownVariantFields } from "../core/record-validation.js";
20
+ import { requireInputKeys, requireInputObject, rejectUnknownVariantFields, requireConfigRecord as requireRecord, requireTrimmedConfigString as requireNonEmptyString, requireConfigFinite as requireFinite, parseConfigLimitations as parseLimitations, requireConfigFiniteAtLeastZero as requireNonNegativeFinite, requireConfigUnitInterval as requireFraction } from "../core/record-validation.js";
21
+ import { parseNonNegativeFact, parsePositiveFact } from "../sensor/raw-readout.js";
21
22
  export const STABILIZATION_SYSTEM_PROFILE_SCHEMA_VERSION = "0.1.0";
22
23
  export const STABILIZATION_DISTURBANCE_TRAJECTORY_VERSION = "0.1.0";
23
24
  const AXES = new Set([
@@ -35,71 +36,6 @@ const CAPTURE_KINDS = new Set([
35
36
  "still",
36
37
  "video"
37
38
  ]);
38
- function requireRecord(value, path, allowedKeys) {
39
- if (typeof value !== "object" ||
40
- value === null ||
41
- Array.isArray(value)) {
42
- throw new InvalidConfigurationError(path + " must be an object.");
43
- }
44
- if (allowedKeys !== null)
45
- rejectUnknownFields(value, allowedKeys, path);
46
- return value;
47
- }
48
- function requireNonEmptyString(value, path) {
49
- if (typeof value !== "string" ||
50
- value.trim().length === 0) {
51
- throw new InvalidConfigurationError(path +
52
- " must be a non-empty string.");
53
- }
54
- return value.trim();
55
- }
56
- function requireFinite(value, path) {
57
- if (typeof value !== "number" ||
58
- !Number.isFinite(value)) {
59
- throw new InvalidConfigurationError(path + " must be finite.");
60
- }
61
- return value;
62
- }
63
- function requireNonNegativeFinite(value, path) {
64
- const parsed = requireFinite(value, path);
65
- if (parsed < 0) {
66
- throw new InvalidConfigurationError(path +
67
- " must be greater than or equal to zero.");
68
- }
69
- return parsed;
70
- }
71
- function requirePositiveFinite(value, path) {
72
- const parsed = requireFinite(value, path);
73
- if (parsed <= 0) {
74
- throw new InvalidConfigurationError(path +
75
- " must be greater than zero.");
76
- }
77
- return parsed;
78
- }
79
- function requireFraction(value, path) {
80
- const parsed = requireFinite(value, path);
81
- if (parsed < 0 ||
82
- parsed > 1) {
83
- throw new InvalidConfigurationError(path +
84
- " must lie from zero through one.");
85
- }
86
- return parsed;
87
- }
88
- function parseLimitations(value, path) {
89
- if (!Array.isArray(value)) {
90
- throw new InvalidConfigurationError(path + " must be an array.");
91
- }
92
- const parsed = value.map((entry, index) => requireNonEmptyString(entry, path +
93
- "[" +
94
- index +
95
- "]"));
96
- if (new Set(parsed).size !==
97
- parsed.length) {
98
- throw new InvalidConfigurationError(path +
99
- " must not contain duplicates.");
100
- }
101
- return parsed;
102
- }
103
39
  function parseFractionFact(value, path) {
104
40
  const record = requireRecord(value, path, ["value", "evidence"]);
105
41
  return {
@@ -107,20 +43,6 @@ function parseFractionFact(value, path) {
107
43
  evidence: parseEvidenceList(record.evidence, path + ".evidence")
108
44
  };
109
45
  }
110
- function parseNonNegativeFact(value, path) {
111
- const record = requireRecord(value, path, ["value", "evidence"]);
112
- return {
113
- value: requireNonNegativeFinite(record.value, path + ".value"),
114
- evidence: parseEvidenceList(record.evidence, path + ".evidence")
115
- };
116
- }
117
- function parsePositiveFact(value, path) {
118
- const record = requireRecord(value, path, ["value", "evidence"]);
119
- return {
120
- value: requirePositiveFinite(record.value, path + ".value"),
121
- evidence: parseEvidenceList(record.evidence, path + ".evidence")
122
- };
123
- }
124
46
  function parseAxis(value, path) {
125
47
  if (typeof value !== "string" ||
126
48
  !AXES.has(value)) {