@photivra/engine 1.5.0 → 2.0.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 (1078) hide show
  1. package/CHANGELOG.md +393 -6
  2. package/CITATION.cff +1 -1
  3. package/NODE_SUPPORT.md +64 -12
  4. package/README.md +301 -182
  5. package/SECURITY.md +37 -28
  6. package/THIRD_PARTY.md +148 -4
  7. package/dist/camera/equivalent-focal-length.d.ts +13 -14
  8. package/dist/camera/equivalent-focal-length.js +15 -13
  9. package/dist/camera/equivalent-focal-length.js.map +1 -1
  10. package/dist/camera/field-of-view.d.ts +30 -19
  11. package/dist/camera/field-of-view.js +67 -18
  12. package/dist/camera/field-of-view.js.map +1 -1
  13. package/dist/camera/lens-projection.d.ts +170 -0
  14. package/dist/camera/lens-projection.js +444 -0
  15. package/dist/camera/lens-projection.js.map +1 -0
  16. package/dist/camera/projected-object-size.d.ts +10 -11
  17. package/dist/camera/projected-object-size.js +12 -10
  18. package/dist/camera/projected-object-size.js.map +1 -1
  19. package/dist/capture/capture-export-metadata.d.ts +9 -10
  20. package/dist/capture/capture-export-metadata.js +8 -5
  21. package/dist/capture/capture-export-metadata.js.map +1 -1
  22. package/dist/capture/dng-import.d.ts +91 -0
  23. package/dist/capture/dng-import.js +343 -0
  24. package/dist/capture/dng-import.js.map +1 -0
  25. package/dist/capture/environment-raw-producer.d.ts +37 -17
  26. package/dist/capture/environment-raw-producer.js +43 -19
  27. package/dist/capture/environment-raw-producer.js.map +1 -1
  28. package/dist/capture/export-jpeg.d.ts +35 -4
  29. package/dist/capture/export-jpeg.js +133 -47
  30. package/dist/capture/export-jpeg.js.map +1 -1
  31. package/dist/capture/export-tiff.d.ts +48 -24
  32. package/dist/capture/export-tiff.js +91 -31
  33. package/dist/capture/export-tiff.js.map +1 -1
  34. package/dist/capture/focus-control.d.ts +42 -65
  35. package/dist/capture/focus-control.js +56 -75
  36. package/dist/capture/focus-control.js.map +1 -1
  37. package/dist/capture/host-yield-cadence.d.ts +25 -0
  38. package/dist/capture/host-yield-cadence.js +25 -0
  39. package/dist/capture/host-yield-cadence.js.map +1 -0
  40. package/dist/capture/linear-encoding.d.ts +14 -12
  41. package/dist/capture/linear-encoding.js +14 -13
  42. package/dist/capture/linear-encoding.js.map +1 -1
  43. package/dist/capture/lossless-jpeg.d.ts +27 -0
  44. package/dist/capture/lossless-jpeg.js +237 -0
  45. package/dist/capture/lossless-jpeg.js.map +1 -0
  46. package/dist/capture/native-environment-lean.d.ts +227 -0
  47. package/dist/capture/native-environment-lean.js +974 -0
  48. package/dist/capture/native-environment-lean.js.map +1 -0
  49. package/dist/capture/native-environment-optical-stage.d.ts +97 -0
  50. package/dist/capture/native-environment-optical-stage.js +340 -0
  51. package/dist/capture/native-environment-optical-stage.js.map +1 -0
  52. package/dist/capture/native-environment-raw.d.ts +140 -6
  53. package/dist/capture/native-environment-raw.js +435 -34
  54. package/dist/capture/native-environment-raw.js.map +1 -1
  55. package/dist/capture/native-raw-development.d.ts +116 -9
  56. package/dist/capture/native-raw-development.js +221 -62
  57. package/dist/capture/native-raw-development.js.map +1 -1
  58. package/dist/capture/native-raw-dng.d.ts +60 -0
  59. package/dist/capture/native-raw-dng.js +167 -0
  60. package/dist/capture/native-raw-dng.js.map +1 -0
  61. package/dist/capture/native-raw.d.ts +65 -12
  62. package/dist/capture/native-raw.js +81 -19
  63. package/dist/capture/native-raw.js.map +1 -1
  64. package/dist/capture/packed-sensor-raw.d.ts +160 -0
  65. package/dist/capture/packed-sensor-raw.js +221 -0
  66. package/dist/capture/packed-sensor-raw.js.map +1 -0
  67. package/dist/capture/photographic-export.d.ts +58 -35
  68. package/dist/capture/photographic-export.js +177 -70
  69. package/dist/capture/photographic-export.js.map +1 -1
  70. package/dist/capture/raw-development-kernel.d.ts +52 -0
  71. package/dist/capture/raw-development-kernel.js +88 -0
  72. package/dist/capture/raw-development-kernel.js.map +1 -0
  73. package/dist/capture/raw-frame-limits.d.ts +7 -7
  74. package/dist/capture/raw-frame-limits.js +7 -6
  75. package/dist/capture/raw-frame-limits.js.map +1 -1
  76. package/dist/capture/raw-frame-reconstruction.d.ts +8 -9
  77. package/dist/capture/raw-frame-reconstruction.js +9 -8
  78. package/dist/capture/raw-frame-reconstruction.js.map +1 -1
  79. package/dist/capture/raw-white-level.d.ts +55 -0
  80. package/dist/capture/raw-white-level.js +165 -0
  81. package/dist/capture/raw-white-level.js.map +1 -0
  82. package/dist/capture/release-sequence.d.ts +5 -7
  83. package/dist/capture/release-sequence.js +15 -13
  84. package/dist/capture/release-sequence.js.map +1 -1
  85. package/dist/capture/sensor-raw-frame.d.ts +5 -6
  86. package/dist/capture/sensor-raw-frame.js +26 -9
  87. package/dist/capture/sensor-raw-frame.js.map +1 -1
  88. package/dist/capture/sensor-raw-producer.d.ts +21 -13
  89. package/dist/capture/sensor-raw-producer.js +86 -25
  90. package/dist/capture/sensor-raw-producer.js.map +1 -1
  91. package/dist/capture/simulated-capture.d.ts +15 -16
  92. package/dist/capture/simulated-capture.js +22 -12
  93. package/dist/capture/simulated-capture.js.map +1 -1
  94. package/dist/color/camera-color-matrix.d.ts +69 -0
  95. package/dist/color/camera-color-matrix.js +132 -0
  96. package/dist/color/camera-color-matrix.js.map +1 -0
  97. package/dist/color/capture-color.d.ts +17 -10
  98. package/dist/color/capture-color.js +50 -17
  99. package/dist/color/capture-color.js.map +1 -1
  100. package/dist/color/chromatic-adaptation.d.ts +63 -0
  101. package/dist/color/chromatic-adaptation.js +91 -0
  102. package/dist/color/chromatic-adaptation.js.map +1 -0
  103. package/dist/color/color-difference.d.ts +104 -0
  104. package/dist/color/color-difference.js +205 -0
  105. package/dist/color/color-difference.js.map +1 -0
  106. package/dist/color/color-matching-functions.d.ts +168 -0
  107. package/dist/color/color-matching-functions.js +309 -0
  108. package/dist/color/color-matching-functions.js.map +1 -0
  109. package/dist/color/colorimetry-math.d.ts +52 -0
  110. package/dist/color/colorimetry-math.js +144 -0
  111. package/dist/color/colorimetry-math.js.map +1 -0
  112. package/dist/color/gamut-mapping.d.ts +54 -0
  113. package/dist/color/gamut-mapping.js +107 -0
  114. package/dist/color/gamut-mapping.js.map +1 -0
  115. package/dist/color/illuminants.d.ts +182 -0
  116. package/dist/color/illuminants.js +434 -0
  117. package/dist/color/illuminants.js.map +1 -0
  118. package/dist/color/rgb-color-spaces.d.ts +79 -0
  119. package/dist/color/rgb-color-spaces.js +115 -0
  120. package/dist/color/rgb-color-spaces.js.map +1 -0
  121. package/dist/color/sensor-color-development.d.ts +33 -11
  122. package/dist/color/sensor-color-development.js +104 -30
  123. package/dist/color/sensor-color-development.js.map +1 -1
  124. package/dist/color/srgb-icc.d.ts +9 -2
  125. package/dist/color/srgb-icc.js +16 -5
  126. package/dist/color/srgb-icc.js.map +1 -1
  127. package/dist/color/srgb-transfer.d.ts +1 -2
  128. package/dist/color/srgb-transfer.js +1 -1
  129. package/dist/color/srgb-transfer.js.map +1 -1
  130. package/dist/color/white-balance.d.ts +68 -19
  131. package/dist/color/white-balance.js +168 -35
  132. package/dist/color/white-balance.js.map +1 -1
  133. package/dist/composition/image-formation-plan.d.ts +152 -128
  134. package/dist/composition/image-formation-plan.js +292 -138
  135. package/dist/composition/image-formation-plan.js.map +1 -1
  136. package/dist/composition/plan-consumer.d.ts +6 -7
  137. package/dist/composition/plan-consumer.js +14 -11
  138. package/dist/composition/plan-consumer.js.map +1 -1
  139. package/dist/core/calculation-result.d.ts +21 -14
  140. package/dist/core/calculation-result.js +209 -11
  141. package/dist/core/calculation-result.js.map +1 -1
  142. package/dist/core/canonical-json.d.ts +13 -8
  143. package/dist/core/canonical-json.js +34 -15
  144. package/dist/core/canonical-json.js.map +1 -1
  145. package/dist/core/configuration-error.d.ts +5 -4
  146. package/dist/core/configuration-error.js +5 -3
  147. package/dist/core/configuration-error.js.map +1 -1
  148. package/dist/core/evidence-provenance.d.ts +12 -12
  149. package/dist/core/evidence-provenance.js +10 -9
  150. package/dist/core/evidence-provenance.js.map +1 -1
  151. package/dist/core/image-formation.d.ts +11 -10
  152. package/dist/core/image-formation.js +11 -9
  153. package/dist/core/image-formation.js.map +1 -1
  154. package/dist/core/owned-data.d.ts +9 -17
  155. package/dist/core/owned-data.js +9 -16
  156. package/dist/core/owned-data.js.map +1 -1
  157. package/dist/core/portable-math.d.ts +15 -0
  158. package/dist/core/portable-math.js +56 -0
  159. package/dist/core/portable-math.js.map +1 -0
  160. package/dist/core/record-validation.d.ts +48 -9
  161. package/dist/core/record-validation.js +93 -19
  162. package/dist/core/record-validation.js.map +1 -1
  163. package/dist/core/scientific-assurance.d.ts +16 -14
  164. package/dist/core/scientific-assurance.js +22 -18
  165. package/dist/core/scientific-assurance.js.map +1 -1
  166. package/dist/core/spectral-composition.d.ts +13 -14
  167. package/dist/core/spectral-composition.js +30 -27
  168. package/dist/core/spectral-composition.js.map +1 -1
  169. package/dist/core/spectral.d.ts +5 -6
  170. package/dist/core/spectral.js +9 -7
  171. package/dist/core/spectral.js.map +1 -1
  172. package/dist/core/validation.d.ts +3 -6
  173. package/dist/core/validation.js +3 -5
  174. package/dist/core/validation.js.map +1 -1
  175. package/dist/core/version.d.ts +6 -6
  176. package/dist/core/version.js +6 -5
  177. package/dist/core/version.js.map +1 -1
  178. package/dist/equipment/exposure-capabilities.d.ts +10 -10
  179. package/dist/equipment/exposure-capabilities.js +40 -30
  180. package/dist/equipment/exposure-capabilities.js.map +1 -1
  181. package/dist/equipment/generic-tier-assets.d.ts +13 -6
  182. package/dist/equipment/generic-tier-assets.js +34 -15
  183. package/dist/equipment/generic-tier-assets.js.map +1 -1
  184. package/dist/equipment/generic-tier-presets.d.ts +5 -6
  185. package/dist/equipment/generic-tier-presets.js +15 -13
  186. package/dist/equipment/generic-tier-presets.js.map +1 -1
  187. package/dist/equipment/iso-capabilities.d.ts +21 -22
  188. package/dist/equipment/iso-capabilities.js +49 -44
  189. package/dist/equipment/iso-capabilities.js.map +1 -1
  190. package/dist/equipment/metering-capabilities.d.ts +5 -6
  191. package/dist/equipment/metering-capabilities.js +22 -19
  192. package/dist/equipment/metering-capabilities.js.map +1 -1
  193. package/dist/equipment/release-capabilities.d.ts +3 -4
  194. package/dist/equipment/release-capabilities.js +18 -14
  195. package/dist/equipment/release-capabilities.js.map +1 -1
  196. package/dist/exposure/exposure-duration-control.d.ts +19 -17
  197. package/dist/exposure/exposure-duration-control.js +27 -23
  198. package/dist/exposure/exposure-duration-control.js.map +1 -1
  199. package/dist/exposure/exposure-mode-resolver.d.ts +42 -39
  200. package/dist/exposure/exposure-mode-resolver.js +127 -82
  201. package/dist/exposure/exposure-mode-resolver.js.map +1 -1
  202. package/dist/exposure/exposure.d.ts +17 -16
  203. package/dist/exposure/exposure.js +22 -15
  204. package/dist/exposure/exposure.js.map +1 -1
  205. package/dist/exposure/flash-ttl.d.ts +135 -0
  206. package/dist/exposure/flash-ttl.js +193 -0
  207. package/dist/exposure/flash-ttl.js.map +1 -0
  208. package/dist/exposure/flash.d.ts +144 -36
  209. package/dist/exposure/flash.js +259 -59
  210. package/dist/exposure/flash.js.map +1 -1
  211. package/dist/exposure/metering-scene-radiance.d.ts +10 -10
  212. package/dist/exposure/metering-scene-radiance.js +42 -23
  213. package/dist/exposure/metering-scene-radiance.js.map +1 -1
  214. package/dist/exposure/metering-target.d.ts +8 -8
  215. package/dist/exposure/metering-target.js +30 -20
  216. package/dist/exposure/metering-target.js.map +1 -1
  217. package/dist/exposure/metering-temporal.d.ts +16 -15
  218. package/dist/exposure/metering-temporal.js +41 -19
  219. package/dist/exposure/metering-temporal.js.map +1 -1
  220. package/dist/exposure/metering.d.ts +64 -17
  221. package/dist/exposure/metering.js +159 -30
  222. package/dist/exposure/metering.js.map +1 -1
  223. package/dist/exposure/program-line.d.ts +5 -5
  224. package/dist/exposure/program-line.js +20 -13
  225. package/dist/exposure/program-line.js.map +1 -1
  226. package/dist/facade/camera-preset.d.ts +8 -0
  227. package/dist/facade/camera-preset.js +102 -0
  228. package/dist/facade/camera-preset.js.map +1 -0
  229. package/dist/facade/chart-radiance-batch.d.ts +57 -0
  230. package/dist/facade/chart-radiance-batch.js +121 -0
  231. package/dist/facade/chart-radiance-batch.js.map +1 -0
  232. package/dist/facade/chart-scenes.d.ts +177 -0
  233. package/dist/facade/chart-scenes.js +232 -0
  234. package/dist/facade/chart-scenes.js.map +1 -0
  235. package/dist/facade/host-yield.d.ts +7 -0
  236. package/dist/facade/host-yield.js +23 -0
  237. package/dist/facade/host-yield.js.map +1 -0
  238. package/dist/facade/scene-radiance.d.ts +117 -0
  239. package/dist/facade/scene-radiance.js +205 -0
  240. package/dist/facade/scene-radiance.js.map +1 -0
  241. package/dist/facade/simulate-capture.d.ts +284 -0
  242. package/dist/facade/simulate-capture.js +1026 -0
  243. package/dist/facade/simulate-capture.js.map +1 -0
  244. package/dist/gpu/chart-radiance-gpu.d.ts +17 -0
  245. package/dist/gpu/chart-radiance-gpu.js +176 -0
  246. package/dist/gpu/chart-radiance-gpu.js.map +1 -0
  247. package/dist/gpu/image-kernel-accelerator.d.ts +132 -0
  248. package/dist/gpu/image-kernel-accelerator.js +720 -0
  249. package/dist/gpu/image-kernel-accelerator.js.map +1 -0
  250. package/dist/gpu/image-kernel-execution.d.ts +92 -0
  251. package/dist/gpu/image-kernel-execution.js +84 -0
  252. package/dist/gpu/image-kernel-execution.js.map +1 -0
  253. package/dist/gpu/sensor-stage-gpu.d.ts +44 -0
  254. package/dist/gpu/sensor-stage-gpu.js +317 -0
  255. package/dist/gpu/sensor-stage-gpu.js.map +1 -0
  256. package/dist/gpu/temporal-sum-gpu.d.ts +23 -0
  257. package/dist/gpu/temporal-sum-gpu.js +150 -0
  258. package/dist/gpu/temporal-sum-gpu.js.map +1 -0
  259. package/dist/gpu/webgpu-runtime.d.ts +63 -0
  260. package/dist/gpu/webgpu-runtime.js +215 -0
  261. package/dist/gpu/webgpu-runtime.js.map +1 -0
  262. package/dist/gpu/wgsl-chart-kernel.d.ts +30 -0
  263. package/dist/gpu/wgsl-chart-kernel.js +165 -0
  264. package/dist/gpu/wgsl-chart-kernel.js.map +1 -0
  265. package/dist/gpu/wgsl-kernels.d.ts +53 -0
  266. package/dist/gpu/wgsl-kernels.js +365 -0
  267. package/dist/gpu/wgsl-kernels.js.map +1 -0
  268. package/dist/gpu/wgsl-sensor-kernel.d.ts +64 -0
  269. package/dist/gpu/wgsl-sensor-kernel.js +409 -0
  270. package/dist/gpu/wgsl-sensor-kernel.js.map +1 -0
  271. package/dist/index.d.ts +89 -25
  272. package/dist/index.js +59 -15
  273. package/dist/index.js.map +1 -1
  274. package/dist/motion/camera-rotation.d.ts +92 -40
  275. package/dist/motion/camera-rotation.js +84 -47
  276. package/dist/motion/camera-rotation.js.map +1 -1
  277. package/dist/motion/capture-rotation-inverse-mapping.d.ts +37 -34
  278. package/dist/motion/capture-rotation-inverse-mapping.js +32 -24
  279. package/dist/motion/capture-rotation-inverse-mapping.js.map +1 -1
  280. package/dist/motion/capture-rotation-temporal-quadrature.d.ts +39 -35
  281. package/dist/motion/capture-rotation-temporal-quadrature.js +37 -28
  282. package/dist/motion/capture-rotation-temporal-quadrature.js.map +1 -1
  283. package/dist/motion/capture-rotation-trajectory.d.ts +32 -28
  284. package/dist/motion/capture-rotation-trajectory.js +36 -26
  285. package/dist/motion/capture-rotation-trajectory.js.map +1 -1
  286. package/dist/motion/capture-translation-parallax-temporal-quadrature.d.ts +23 -21
  287. package/dist/motion/capture-translation-parallax-temporal-quadrature.js +33 -14
  288. package/dist/motion/capture-translation-parallax-temporal-quadrature.js.map +1 -1
  289. package/dist/motion/extended-object-projection.d.ts +19 -16
  290. package/dist/motion/extended-object-projection.js +38 -18
  291. package/dist/motion/extended-object-projection.js.map +1 -1
  292. package/dist/motion/projected-motion.d.ts +18 -17
  293. package/dist/motion/projected-motion.js +25 -17
  294. package/dist/motion/projected-motion.js.map +1 -1
  295. package/dist/motion/temporal-exposure-sum.d.ts +55 -0
  296. package/dist/motion/temporal-exposure-sum.js +141 -0
  297. package/dist/motion/temporal-exposure-sum.js.map +1 -0
  298. package/dist/motion/video-sequence.d.ts +111 -0
  299. package/dist/motion/video-sequence.js +179 -0
  300. package/dist/motion/video-sequence.js.map +1 -0
  301. package/dist/optics/aperture.d.ts +14 -15
  302. package/dist/optics/aperture.js +13 -11
  303. package/dist/optics/aperture.js.map +1 -1
  304. package/dist/optics/atmospheric-haze.d.ts +83 -0
  305. package/dist/optics/atmospheric-haze.js +113 -0
  306. package/dist/optics/atmospheric-haze.js.map +1 -0
  307. package/dist/optics/brown-conrady-distortion.d.ts +116 -0
  308. package/dist/optics/brown-conrady-distortion.js +291 -0
  309. package/dist/optics/brown-conrady-distortion.js.map +1 -0
  310. package/dist/optics/circle-of-confusion.d.ts +10 -10
  311. package/dist/optics/circle-of-confusion.js +15 -11
  312. package/dist/optics/circle-of-confusion.js.map +1 -1
  313. package/dist/optics/complex-pupil-psf.d.ts +64 -33
  314. package/dist/optics/complex-pupil-psf.js +190 -98
  315. package/dist/optics/complex-pupil-psf.js.map +1 -1
  316. package/dist/optics/depth-of-field.d.ts +25 -15
  317. package/dist/optics/depth-of-field.js +67 -27
  318. package/dist/optics/depth-of-field.js.map +1 -1
  319. package/dist/optics/diffraction.d.ts +14 -13
  320. package/dist/optics/diffraction.js +14 -10
  321. package/dist/optics/diffraction.js.map +1 -1
  322. package/dist/optics/focus-breathing.d.ts +88 -27
  323. package/dist/optics/focus-breathing.js +114 -23
  324. package/dist/optics/focus-breathing.js.map +1 -1
  325. package/dist/optics/focus-state.d.ts +18 -18
  326. package/dist/optics/focus-state.js +21 -15
  327. package/dist/optics/focus-state.js.map +1 -1
  328. package/dist/optics/front-of-lens-filter.d.ts +45 -14
  329. package/dist/optics/front-of-lens-filter.js +259 -88
  330. package/dist/optics/front-of-lens-filter.js.map +1 -1
  331. package/dist/optics/illumination-vignetting.d.ts +16 -17
  332. package/dist/optics/illumination-vignetting.js +40 -17
  333. package/dist/optics/illumination-vignetting.js.map +1 -1
  334. package/dist/optics/irradiance-convolution.d.ts +59 -0
  335. package/dist/optics/irradiance-convolution.js +138 -0
  336. package/dist/optics/irradiance-convolution.js.map +1 -0
  337. package/dist/optics/lateral-chromatic-aberration.d.ts +33 -30
  338. package/dist/optics/lateral-chromatic-aberration.js +33 -23
  339. package/dist/optics/lateral-chromatic-aberration.js.map +1 -1
  340. package/dist/optics/lens-psf-kernels.d.ts +89 -0
  341. package/dist/optics/lens-psf-kernels.js +175 -0
  342. package/dist/optics/lens-psf-kernels.js.map +1 -0
  343. package/dist/optics/lens-psf-profile.d.ts +78 -46
  344. package/dist/optics/lens-psf-profile.js +335 -124
  345. package/dist/optics/lens-psf-profile.js.map +1 -1
  346. package/dist/optics/longitudinal-chromatic-aberration.d.ts +102 -0
  347. package/dist/optics/longitudinal-chromatic-aberration.js +210 -0
  348. package/dist/optics/longitudinal-chromatic-aberration.js.map +1 -0
  349. package/dist/optics/polygon-diffraction.d.ts +80 -23
  350. package/dist/optics/polygon-diffraction.js +130 -25
  351. package/dist/optics/polygon-diffraction.js.map +1 -1
  352. package/dist/optics/profile-contract.d.ts +5 -6
  353. package/dist/optics/profile-contract.js +14 -11
  354. package/dist/optics/profile-contract.js.map +1 -1
  355. package/dist/optics/psf-foundation.d.ts +23 -23
  356. package/dist/optics/psf-foundation.js +24 -21
  357. package/dist/optics/psf-foundation.js.map +1 -1
  358. package/dist/optics/pupil-vignetting.d.ts +118 -0
  359. package/dist/optics/pupil-vignetting.js +264 -0
  360. package/dist/optics/pupil-vignetting.js.map +1 -0
  361. package/dist/optics/radial-distortion.d.ts +34 -33
  362. package/dist/optics/radial-distortion.js +37 -30
  363. package/dist/optics/radial-distortion.js.map +1 -1
  364. package/dist/optics/scene-to-sensor-irradiance.d.ts +47 -33
  365. package/dist/optics/scene-to-sensor-irradiance.js +73 -45
  366. package/dist/optics/scene-to-sensor-irradiance.js.map +1 -1
  367. package/dist/optics/scene-to-sensor-quadrature.d.ts +23 -18
  368. package/dist/optics/scene-to-sensor-quadrature.js +16 -11
  369. package/dist/optics/scene-to-sensor-quadrature.js.map +1 -1
  370. package/dist/optics/sensor-aperture-rays.d.ts +20 -4
  371. package/dist/optics/sensor-aperture-rays.js +32 -14
  372. package/dist/optics/sensor-aperture-rays.js.map +1 -1
  373. package/dist/optics/sensor-environment-query.d.ts +81 -16
  374. package/dist/optics/sensor-environment-query.js +145 -12
  375. package/dist/optics/sensor-environment-query.js.map +1 -1
  376. package/dist/optics/sensor-psf-quadrature.d.ts +12 -10
  377. package/dist/optics/sensor-psf-quadrature.js +18 -11
  378. package/dist/optics/sensor-psf-quadrature.js.map +1 -1
  379. package/dist/optics/sensor-psf-support.d.ts +8 -6
  380. package/dist/optics/sensor-psf-support.js +8 -5
  381. package/dist/optics/sensor-psf-support.js.map +1 -1
  382. package/dist/optics/stray-light.d.ts +22 -19
  383. package/dist/optics/stray-light.js +18 -12
  384. package/dist/optics/stray-light.js.map +1 -1
  385. package/dist/optics/thin-lens.d.ts +10 -9
  386. package/dist/optics/thin-lens.js +12 -8
  387. package/dist/optics/thin-lens.js.map +1 -1
  388. package/dist/optics/zernike-wavefront.d.ts +154 -0
  389. package/dist/optics/zernike-wavefront.js +352 -0
  390. package/dist/optics/zernike-wavefront.js.map +1 -0
  391. package/dist/output/capture-corrected-sdr.d.ts +16 -8
  392. package/dist/output/capture-corrected-sdr.js +16 -8
  393. package/dist/output/capture-corrected-sdr.js.map +1 -1
  394. package/dist/output/capture-geometry.d.ts +34 -36
  395. package/dist/output/capture-geometry.js +52 -32
  396. package/dist/output/capture-geometry.js.map +1 -1
  397. package/dist/output/capture-sdr.d.ts +7 -6
  398. package/dist/output/capture-sdr.js +11 -8
  399. package/dist/output/capture-sdr.js.map +1 -1
  400. package/dist/output/crop.d.ts +22 -6
  401. package/dist/output/crop.js +40 -6
  402. package/dist/output/crop.js.map +1 -1
  403. package/dist/output/geometric-transforms.d.ts +60 -23
  404. package/dist/output/geometric-transforms.js +111 -24
  405. package/dist/output/geometric-transforms.js.map +1 -1
  406. package/dist/output/lens-corrections.d.ts +27 -19
  407. package/dist/output/lens-corrections.js +29 -13
  408. package/dist/output/lens-corrections.js.map +1 -1
  409. package/dist/output/native-capture-sdr.d.ts +71 -16
  410. package/dist/output/native-capture-sdr.js +190 -33
  411. package/dist/output/native-capture-sdr.js.map +1 -1
  412. package/dist/output/png-encoder.d.ts +65 -0
  413. package/dist/output/png-encoder.js +461 -0
  414. package/dist/output/png-encoder.js.map +1 -0
  415. package/dist/output/print-contrast-reference.d.ts +10 -9
  416. package/dist/output/print-contrast-reference.js +10 -8
  417. package/dist/output/print-contrast-reference.js.map +1 -1
  418. package/dist/output/print-detail.d.ts +18 -17
  419. package/dist/output/print-detail.js +12 -10
  420. package/dist/output/print-detail.js.map +1 -1
  421. package/dist/output/print-jpeg.d.ts +26 -5
  422. package/dist/output/print-jpeg.js +117 -50
  423. package/dist/output/print-jpeg.js.map +1 -1
  424. package/dist/output/print-plan.d.ts +19 -19
  425. package/dist/output/print-plan.js +11 -10
  426. package/dist/output/print-plan.js.map +1 -1
  427. package/dist/output/print-region-statistics.d.ts +14 -15
  428. package/dist/output/print-region-statistics.js +11 -11
  429. package/dist/output/print-region-statistics.js.map +1 -1
  430. package/dist/output/sdr-rendering.d.ts +48 -10
  431. package/dist/output/sdr-rendering.js +114 -25
  432. package/dist/output/sdr-rendering.js.map +1 -1
  433. package/dist/output/subject-framing-crop.d.ts +6 -10
  434. package/dist/output/subject-framing-crop.js +23 -13
  435. package/dist/output/subject-framing-crop.js.map +1 -1
  436. package/dist/processing/demosaic.d.ts +90 -0
  437. package/dist/processing/demosaic.js +275 -0
  438. package/dist/processing/demosaic.js.map +1 -0
  439. package/dist/processing/denoise.d.ts +68 -0
  440. package/dist/processing/denoise.js +214 -0
  441. package/dist/processing/denoise.js.map +1 -0
  442. package/dist/processing/focus-stacking.d.ts +17 -0
  443. package/dist/processing/focus-stacking.js +80 -0
  444. package/dist/processing/focus-stacking.js.map +1 -0
  445. package/dist/processing/hdr.d.ts +54 -0
  446. package/dist/processing/hdr.js +172 -0
  447. package/dist/processing/hdr.js.map +1 -0
  448. package/dist/processing/image-ops.d.ts +66 -0
  449. package/dist/processing/image-ops.js +192 -0
  450. package/dist/processing/image-ops.js.map +1 -0
  451. package/dist/processing/panorama.d.ts +51 -0
  452. package/dist/processing/panorama.js +305 -0
  453. package/dist/processing/panorama.js.map +1 -0
  454. package/dist/processing/processing-task.d.ts +85 -0
  455. package/dist/processing/processing-task.js +164 -0
  456. package/dist/processing/processing-task.js.map +1 -0
  457. package/dist/processing/tone-mapping.d.ts +69 -0
  458. package/dist/processing/tone-mapping.js +310 -0
  459. package/dist/processing/tone-mapping.js.map +1 -0
  460. package/dist/schema/camera.d.ts +5 -4
  461. package/dist/schema/illumination-spectral.d.ts +13 -19
  462. package/dist/schema/illumination-spectral.js +23 -19
  463. package/dist/schema/illumination-spectral.js.map +1 -1
  464. package/dist/schema/illumination-temporal.d.ts +12 -15
  465. package/dist/schema/illumination-temporal.js +43 -28
  466. package/dist/schema/illumination-temporal.js.map +1 -1
  467. package/dist/schema/illumination.d.ts +5 -4
  468. package/dist/schema/illumination.js +55 -20
  469. package/dist/schema/illumination.js.map +1 -1
  470. package/dist/schema/scene-radiance-temporal.d.ts +9 -11
  471. package/dist/schema/scene-radiance-temporal.js +26 -23
  472. package/dist/schema/scene-radiance-temporal.js.map +1 -1
  473. package/dist/schema/scene-radiance.d.ts +17 -17
  474. package/dist/schema/scene-radiance.js +56 -34
  475. package/dist/schema/scene-radiance.js.map +1 -1
  476. package/dist/schema/scene.d.ts +14 -13
  477. package/dist/schema/validation.d.ts +0 -1
  478. package/dist/schema/validation.js +17 -15
  479. package/dist/schema/validation.js.map +1 -1
  480. package/dist/sensor/accumulated-charge.d.ts +45 -64
  481. package/dist/sensor/accumulated-charge.js +41 -47
  482. package/dist/sensor/accumulated-charge.js.map +1 -1
  483. package/dist/sensor/architecture.d.ts +22 -25
  484. package/dist/sensor/architecture.js +17 -16
  485. package/dist/sensor/architecture.js.map +1 -1
  486. package/dist/sensor/camera-saturation-capacity.d.ts +21 -28
  487. package/dist/sensor/camera-saturation-capacity.js +36 -34
  488. package/dist/sensor/camera-saturation-capacity.js.map +1 -1
  489. package/dist/sensor/capture-color-sampling-binding.d.ts +46 -51
  490. package/dist/sensor/capture-color-sampling-binding.js +39 -41
  491. package/dist/sensor/capture-color-sampling-binding.js.map +1 -1
  492. package/dist/sensor/capture-mode-timing.d.ts +8 -9
  493. package/dist/sensor/capture-mode-timing.js +24 -21
  494. package/dist/sensor/capture-mode-timing.js.map +1 -1
  495. package/dist/sensor/capture-mode.d.ts +28 -30
  496. package/dist/sensor/capture-mode.js +33 -31
  497. package/dist/sensor/capture-mode.js.map +1 -1
  498. package/dist/sensor/color-sampling.d.ts +30 -33
  499. package/dist/sensor/color-sampling.js +32 -37
  500. package/dist/sensor/color-sampling.js.map +1 -1
  501. package/dist/sensor/constant-rate-temporal-integration.d.ts +19 -23
  502. package/dist/sensor/constant-rate-temporal-integration.js +28 -26
  503. package/dist/sensor/constant-rate-temporal-integration.js.map +1 -1
  504. package/dist/sensor/dark-current.d.ts +53 -49
  505. package/dist/sensor/dark-current.js +87 -46
  506. package/dist/sensor/dark-current.js.map +1 -1
  507. package/dist/sensor/environment-photo-signal.d.ts +71 -30
  508. package/dist/sensor/environment-photo-signal.js +127 -46
  509. package/dist/sensor/environment-photo-signal.js.map +1 -1
  510. package/dist/sensor/eqe-electron-rate.d.ts +14 -13
  511. package/dist/sensor/eqe-electron-rate.js +34 -14
  512. package/dist/sensor/eqe-electron-rate.js.map +1 -1
  513. package/dist/sensor/eqe-local-exposure.d.ts +24 -24
  514. package/dist/sensor/eqe-local-exposure.js +15 -11
  515. package/dist/sensor/eqe-local-exposure.js.map +1 -1
  516. package/dist/sensor/eqe-temporal-exposure.d.ts +11 -11
  517. package/dist/sensor/eqe-temporal-exposure.js +15 -10
  518. package/dist/sensor/eqe-temporal-exposure.js.map +1 -1
  519. package/dist/sensor/exposure-window.d.ts +48 -34
  520. package/dist/sensor/exposure-window.js +77 -36
  521. package/dist/sensor/exposure-window.js.map +1 -1
  522. package/dist/sensor/fixed-pattern-noise.d.ts +108 -0
  523. package/dist/sensor/fixed-pattern-noise.js +166 -0
  524. package/dist/sensor/fixed-pattern-noise.js.map +1 -0
  525. package/dist/sensor/iso-gain.d.ts +44 -0
  526. package/dist/sensor/iso-gain.js +119 -0
  527. package/dist/sensor/iso-gain.js.map +1 -0
  528. package/dist/sensor/iso-signal-chain.d.ts +25 -48
  529. package/dist/sensor/iso-signal-chain.js +52 -66
  530. package/dist/sensor/iso-signal-chain.js.map +1 -1
  531. package/dist/sensor/local-exposure-binding.d.ts +21 -21
  532. package/dist/sensor/local-exposure-binding.js +22 -18
  533. package/dist/sensor/local-exposure-binding.js.map +1 -1
  534. package/dist/sensor/optical-stack.d.ts +36 -40
  535. package/dist/sensor/optical-stack.js +34 -37
  536. package/dist/sensor/optical-stack.js.map +1 -1
  537. package/dist/sensor/photon-energy.d.ts +22 -28
  538. package/dist/sensor/photon-energy.js +42 -34
  539. package/dist/sensor/photon-energy.js.map +1 -1
  540. package/dist/sensor/physical-charge-capacity.d.ts +20 -30
  541. package/dist/sensor/physical-charge-capacity.js +35 -36
  542. package/dist/sensor/physical-charge-capacity.js.map +1 -1
  543. package/dist/sensor/pixel-pitch.d.ts +2 -3
  544. package/dist/sensor/pixel-pitch.js +4 -2
  545. package/dist/sensor/pixel-pitch.js.map +1 -1
  546. package/dist/sensor/radiometry-readiness.d.ts +13 -15
  547. package/dist/sensor/radiometry-readiness.js +20 -17
  548. package/dist/sensor/radiometry-readiness.js.map +1 -1
  549. package/dist/sensor/raw-readout.d.ts +173 -83
  550. package/dist/sensor/raw-readout.js +425 -160
  551. package/dist/sensor/raw-readout.js.map +1 -1
  552. package/dist/sensor/raw-reconstruction.d.ts +112 -44
  553. package/dist/sensor/raw-reconstruction.js +200 -47
  554. package/dist/sensor/raw-reconstruction.js.map +1 -1
  555. package/dist/sensor/readout-exposure-linkage.d.ts +103 -34
  556. package/dist/sensor/readout-exposure-linkage.js +125 -27
  557. package/dist/sensor/readout-exposure-linkage.js.map +1 -1
  558. package/dist/sensor/readout-timing.d.ts +43 -44
  559. package/dist/sensor/readout-timing.js +38 -30
  560. package/dist/sensor/readout-timing.js.map +1 -1
  561. package/dist/sensor/response-application-compatibility.d.ts +21 -28
  562. package/dist/sensor/response-application-compatibility.js +35 -34
  563. package/dist/sensor/response-application-compatibility.js.map +1 -1
  564. package/dist/sensor/response-operating-range.d.ts +26 -35
  565. package/dist/sensor/response-operating-range.js +40 -39
  566. package/dist/sensor/response-operating-range.js.map +1 -1
  567. package/dist/sensor/responsivity-photocurrent.d.ts +21 -29
  568. package/dist/sensor/responsivity-photocurrent.js +57 -40
  569. package/dist/sensor/responsivity-photocurrent.js.map +1 -1
  570. package/dist/sensor/sampling-aperture.d.ts +21 -25
  571. package/dist/sensor/sampling-aperture.js +29 -28
  572. package/dist/sensor/sampling-aperture.js.map +1 -1
  573. package/dist/sensor/scene-eqe-temporal-exposure.d.ts +14 -12
  574. package/dist/sensor/scene-eqe-temporal-exposure.js +13 -8
  575. package/dist/sensor/scene-eqe-temporal-exposure.js.map +1 -1
  576. package/dist/sensor/sensor-geometry.d.ts +28 -32
  577. package/dist/sensor/sensor-geometry.js +35 -20
  578. package/dist/sensor/sensor-geometry.js.map +1 -1
  579. package/dist/sensor/signal-noise.d.ts +14 -16
  580. package/dist/sensor/signal-noise.js +15 -13
  581. package/dist/sensor/signal-noise.js.map +1 -1
  582. package/dist/sensor/spatial-sample-reduction.d.ts +45 -60
  583. package/dist/sensor/spatial-sample-reduction.js +39 -48
  584. package/dist/sensor/spatial-sample-reduction.js.map +1 -1
  585. package/dist/sensor/spatial-sampling-quadrature.d.ts +46 -54
  586. package/dist/sensor/spatial-sampling-quadrature.js +31 -36
  587. package/dist/sensor/spatial-sampling-quadrature.js.map +1 -1
  588. package/dist/sensor/spatio-spectral-reduction.d.ts +32 -42
  589. package/dist/sensor/spatio-spectral-reduction.js +24 -29
  590. package/dist/sensor/spatio-spectral-reduction.js.map +1 -1
  591. package/dist/sensor/spectral-coverage.d.ts +6 -9
  592. package/dist/sensor/spectral-coverage.js +9 -6
  593. package/dist/sensor/spectral-coverage.js.map +1 -1
  594. package/dist/sensor/spectral-quadrature.d.ts +46 -60
  595. package/dist/sensor/spectral-quadrature.js +38 -48
  596. package/dist/sensor/spectral-quadrature.js.map +1 -1
  597. package/dist/sensor/spectral-response.d.ts +53 -41
  598. package/dist/sensor/spectral-response.js +80 -47
  599. package/dist/sensor/spectral-response.js.map +1 -1
  600. package/dist/sensor/stochastic-seed.d.ts +50 -0
  601. package/dist/sensor/stochastic-seed.js +123 -0
  602. package/dist/sensor/stochastic-seed.js.map +1 -0
  603. package/dist/sensor/temporal-eqe-sum.d.ts +2 -4
  604. package/dist/sensor/temporal-eqe-sum.js +5 -6
  605. package/dist/sensor/temporal-eqe-sum.js.map +1 -1
  606. package/dist/sensor/temporal-photo-signal.d.ts +5 -6
  607. package/dist/sensor/temporal-photo-signal.js +11 -10
  608. package/dist/sensor/temporal-photo-signal.js.map +1 -1
  609. package/dist/sensor/temporal-sample-binding.d.ts +1 -2
  610. package/dist/sensor/temporal-sample-binding.js +3 -3
  611. package/dist/sensor/temporal-sample-binding.js.map +1 -1
  612. package/dist/simulation/poc-simulation.d.ts +16 -16
  613. package/dist/simulation/poc-simulation.js +20 -15
  614. package/dist/simulation/poc-simulation.js.map +1 -1
  615. package/dist/stabilization/camera-shake.d.ts +42 -18
  616. package/dist/stabilization/camera-shake.js +43 -20
  617. package/dist/stabilization/camera-shake.js.map +1 -1
  618. package/dist/stabilization/system.d.ts +44 -41
  619. package/dist/stabilization/system.js +196 -108
  620. package/dist/stabilization/system.js.map +1 -1
  621. package/docs/AGENT_GUIDE.md +183 -0
  622. package/docs/API_REFERENCE.md +761 -2
  623. package/docs/QUICK_START.md +495 -0
  624. package/docs/USAGE.md +1719 -714
  625. package/docs/api/camera-equivalent-focal-length.md +6 -7
  626. package/docs/api/camera-field-of-view.md +16 -8
  627. package/docs/api/camera-lens-projection.md +246 -0
  628. package/docs/api/camera-projected-object-size.md +6 -5
  629. package/docs/api/capture-capture-export-metadata.md +10 -10
  630. package/docs/api/capture-dng-import.md +134 -0
  631. package/docs/api/capture-environment-raw-producer.md +17 -11
  632. package/docs/api/capture-export-jpeg.md +16 -0
  633. package/docs/api/capture-focus-control.md +25 -47
  634. package/docs/api/capture-linear-encoding.md +21 -12
  635. package/docs/api/capture-lossless-jpeg.md +49 -0
  636. package/docs/api/capture-native-environment-optical-stage.md +75 -0
  637. package/docs/api/capture-native-environment-raw.md +161 -4
  638. package/docs/api/capture-native-raw-development.md +69 -8
  639. package/docs/api/capture-native-raw-dng.md +84 -0
  640. package/docs/api/capture-native-raw.md +21 -10
  641. package/docs/api/capture-packed-sensor-raw.md +156 -0
  642. package/docs/api/capture-photographic-export.md +69 -31
  643. package/docs/api/capture-raw-frame-reconstruction.md +8 -8
  644. package/docs/api/capture-raw-white-level.md +37 -0
  645. package/docs/api/capture-release-sequence.md +6 -7
  646. package/docs/api/capture-sensor-raw-frame.md +6 -6
  647. package/docs/api/capture-sensor-raw-producer.md +17 -11
  648. package/docs/api/capture-simulated-capture.md +15 -15
  649. package/docs/api/color-camera-color-matrix.md +103 -0
  650. package/docs/api/color-capture-color.md +22 -11
  651. package/docs/api/color-chromatic-adaptation.md +82 -0
  652. package/docs/api/color-color-difference.md +134 -0
  653. package/docs/api/color-color-matching-functions.md +259 -0
  654. package/docs/api/color-colorimetry-math.md +54 -0
  655. package/docs/api/color-gamut-mapping.md +72 -0
  656. package/docs/api/color-illuminants.md +280 -0
  657. package/docs/api/color-rgb-color-spaces.md +151 -0
  658. package/docs/api/color-sensor-color-development.md +27 -0
  659. package/docs/api/color-srgb-icc.md +2 -4
  660. package/docs/api/color-white-balance.md +95 -19
  661. package/docs/api/composition-image-formation-plan.md +124 -95
  662. package/docs/api/composition-plan-consumer.md +7 -7
  663. package/docs/api/core-calculation-result.md +18 -14
  664. package/docs/api/core-configuration-error.md +1 -1
  665. package/docs/api/core-image-formation.md +7 -7
  666. package/docs/api/core-scientific-assurance.md +12 -10
  667. package/docs/api/core-spectral-composition.md +14 -14
  668. package/docs/api/core-spectral.md +6 -6
  669. package/docs/api/core-validation.md +3 -3
  670. package/docs/api/core-version.md +7 -6
  671. package/docs/api/equipment-exposure-capabilities.md +11 -10
  672. package/docs/api/equipment-generic-tier-assets.md +7 -2
  673. package/docs/api/equipment-generic-tier-presets.md +6 -6
  674. package/docs/api/equipment-iso-capabilities.md +18 -18
  675. package/docs/api/equipment-metering-capabilities.md +6 -6
  676. package/docs/api/equipment-release-capabilities.md +4 -4
  677. package/docs/api/exports.json +2213 -8
  678. package/docs/api/exposure-exposure-duration-control.md +9 -9
  679. package/docs/api/exposure-exposure-mode-resolver.md +35 -31
  680. package/docs/api/exposure-exposure.md +10 -10
  681. package/docs/api/exposure-flash-ttl.md +182 -0
  682. package/docs/api/exposure-flash.md +183 -28
  683. package/docs/api/exposure-metering-scene-radiance.md +11 -10
  684. package/docs/api/exposure-metering-target.md +9 -8
  685. package/docs/api/exposure-metering-temporal.md +8 -8
  686. package/docs/api/exposure-metering.md +69 -9
  687. package/docs/api/exposure-program-line.md +6 -5
  688. package/docs/api/facade-chart-radiance-batch.md +96 -0
  689. package/docs/api/facade-chart-scenes.md +240 -0
  690. package/docs/api/facade-scene-radiance.md +122 -0
  691. package/docs/api/facade-simulate-capture.md +324 -0
  692. package/docs/api/gpu-image-kernel-accelerator.md +157 -0
  693. package/docs/api/gpu-image-kernel-execution.md +152 -0
  694. package/docs/api/motion-camera-rotation.md +43 -26
  695. package/docs/api/motion-capture-rotation-inverse-mapping.md +24 -23
  696. package/docs/api/motion-capture-rotation-temporal-quadrature.md +22 -22
  697. package/docs/api/motion-capture-rotation-trajectory.md +15 -15
  698. package/docs/api/motion-capture-translation-parallax-temporal-quadrature.md +15 -14
  699. package/docs/api/motion-extended-object-projection.md +10 -8
  700. package/docs/api/motion-projected-motion.md +8 -8
  701. package/docs/api/motion-temporal-exposure-sum.md +98 -0
  702. package/docs/api/motion-video-sequence.md +159 -0
  703. package/docs/api/optics-aperture.md +8 -8
  704. package/docs/api/optics-atmospheric-haze.md +104 -0
  705. package/docs/api/optics-brown-conrady-distortion.md +123 -0
  706. package/docs/api/optics-circle-of-confusion.md +5 -4
  707. package/docs/api/optics-complex-pupil-psf.md +57 -16
  708. package/docs/api/optics-depth-of-field.md +12 -6
  709. package/docs/api/optics-diffraction.md +8 -7
  710. package/docs/api/optics-focus-breathing.md +104 -16
  711. package/docs/api/optics-focus-state.md +9 -10
  712. package/docs/api/optics-front-of-lens-filter.md +72 -14
  713. package/docs/api/optics-illumination-vignetting.md +10 -10
  714. package/docs/api/optics-irradiance-convolution.md +95 -0
  715. package/docs/api/optics-lateral-chromatic-aberration.md +22 -21
  716. package/docs/api/optics-lens-psf-kernels.md +72 -0
  717. package/docs/api/optics-lens-psf-profile.md +48 -26
  718. package/docs/api/optics-longitudinal-chromatic-aberration.md +128 -0
  719. package/docs/api/optics-polygon-diffraction.md +78 -13
  720. package/docs/api/optics-profile-contract.md +4 -4
  721. package/docs/api/optics-psf-foundation.md +12 -12
  722. package/docs/api/optics-pupil-vignetting.md +132 -0
  723. package/docs/api/optics-radial-distortion.md +12 -12
  724. package/docs/api/optics-scene-to-sensor-irradiance.md +25 -14
  725. package/docs/api/optics-scene-to-sensor-quadrature.md +15 -12
  726. package/docs/api/optics-sensor-aperture-rays.md +3 -2
  727. package/docs/api/optics-sensor-environment-query.md +52 -9
  728. package/docs/api/optics-sensor-psf-quadrature.md +13 -10
  729. package/docs/api/optics-stray-light.md +12 -11
  730. package/docs/api/optics-thin-lens.md +4 -3
  731. package/docs/api/optics-zernike-wavefront.md +229 -0
  732. package/docs/api/output-capture-corrected-sdr.md +10 -7
  733. package/docs/api/output-capture-geometry.md +35 -36
  734. package/docs/api/output-capture-sdr.md +4 -4
  735. package/docs/api/output-crop.md +7 -4
  736. package/docs/api/output-geometric-transforms.md +65 -19
  737. package/docs/api/output-lens-corrections.md +17 -15
  738. package/docs/api/output-native-capture-sdr.md +26 -13
  739. package/docs/api/output-png-encoder.md +105 -0
  740. package/docs/api/output-print-contrast-reference.md +8 -8
  741. package/docs/api/output-print-detail.md +14 -14
  742. package/docs/api/output-print-jpeg.md +22 -5
  743. package/docs/api/output-print-plan.md +18 -18
  744. package/docs/api/output-print-region-statistics.md +14 -14
  745. package/docs/api/output-sdr-rendering.md +52 -10
  746. package/docs/api/output-subject-framing-crop.md +5 -6
  747. package/docs/api/processing-demosaic.md +120 -0
  748. package/docs/api/processing-denoise.md +148 -0
  749. package/docs/api/processing-focus-stacking.md +41 -0
  750. package/docs/api/processing-hdr.md +113 -0
  751. package/docs/api/processing-panorama.md +82 -0
  752. package/docs/api/processing-processing-task.md +107 -0
  753. package/docs/api/processing-tone-mapping.md +142 -0
  754. package/docs/api/schema-camera.md +6 -4
  755. package/docs/api/schema-illumination-spectral.md +10 -11
  756. package/docs/api/schema-illumination-temporal.md +13 -15
  757. package/docs/api/schema-illumination.md +4 -4
  758. package/docs/api/schema-scene-radiance-temporal.md +10 -11
  759. package/docs/api/schema-scene-radiance.md +18 -17
  760. package/docs/api/schema-scene.md +9 -9
  761. package/docs/api/schema-validation.md +1 -1
  762. package/docs/api/sensor-accumulated-charge.md +12 -20
  763. package/docs/api/sensor-architecture.md +15 -16
  764. package/docs/api/sensor-camera-saturation-capacity.md +9 -11
  765. package/docs/api/sensor-capture-color-sampling-binding.md +47 -51
  766. package/docs/api/sensor-capture-mode-timing.md +9 -9
  767. package/docs/api/sensor-capture-mode.md +29 -30
  768. package/docs/api/sensor-color-sampling.md +31 -33
  769. package/docs/api/sensor-constant-rate-temporal-integration.md +10 -11
  770. package/docs/api/sensor-dark-current.md +30 -14
  771. package/docs/api/sensor-environment-photo-signal.md +13 -6
  772. package/docs/api/sensor-eqe-electron-rate.md +9 -8
  773. package/docs/api/sensor-eqe-local-exposure.md +18 -17
  774. package/docs/api/sensor-eqe-temporal-exposure.md +12 -11
  775. package/docs/api/sensor-exposure-window.md +24 -24
  776. package/docs/api/sensor-fixed-pattern-noise.md +104 -0
  777. package/docs/api/sensor-iso-gain.md +63 -0
  778. package/docs/api/sensor-iso-signal-chain.md +13 -22
  779. package/docs/api/sensor-local-exposure-binding.md +12 -13
  780. package/docs/api/sensor-optical-stack.md +37 -40
  781. package/docs/api/sensor-photon-energy.md +10 -11
  782. package/docs/api/sensor-physical-charge-capacity.md +8 -13
  783. package/docs/api/sensor-pixel-pitch.md +1 -1
  784. package/docs/api/sensor-radiometry-readiness.md +9 -9
  785. package/docs/api/sensor-raw-readout.md +118 -52
  786. package/docs/api/sensor-raw-reconstruction.md +97 -20
  787. package/docs/api/sensor-readout-exposure-linkage.md +120 -24
  788. package/docs/api/sensor-readout-timing.md +33 -35
  789. package/docs/api/sensor-response-application-compatibility.md +9 -11
  790. package/docs/api/sensor-response-operating-range.md +14 -18
  791. package/docs/api/sensor-responsivity-photocurrent.md +9 -12
  792. package/docs/api/sensor-sampling-aperture.md +22 -25
  793. package/docs/api/sensor-scene-eqe-temporal-exposure.md +9 -8
  794. package/docs/api/sensor-sensor-geometry.md +23 -24
  795. package/docs/api/sensor-signal-noise.md +10 -10
  796. package/docs/api/sensor-spatial-sample-reduction.md +27 -30
  797. package/docs/api/sensor-spatial-sampling-quadrature.md +37 -39
  798. package/docs/api/sensor-spatio-spectral-reduction.md +27 -29
  799. package/docs/api/sensor-spectral-coverage.md +5 -5
  800. package/docs/api/sensor-spectral-quadrature.md +33 -35
  801. package/docs/api/sensor-spectral-response.md +28 -16
  802. package/docs/api/sensor-temporal-photo-signal.md +6 -6
  803. package/docs/api/simulation-poc-simulation.md +17 -16
  804. package/docs/api/stabilization-camera-shake.md +29 -10
  805. package/docs/api/stabilization-system.md +25 -21
  806. package/docs/examples/production-request.json +3821 -2157
  807. package/docs/examples/simulate-capture.mjs +14 -0
  808. package/llms.txt +35 -0
  809. package/package.json +32 -12
  810. package/AGENTS.md +0 -674
  811. package/CONTRIBUTING.md +0 -76
  812. package/DCO +0 -33
  813. package/dist/camera/equivalent-focal-length.d.ts.map +0 -1
  814. package/dist/camera/field-of-view.d.ts.map +0 -1
  815. package/dist/camera/projected-object-size.d.ts.map +0 -1
  816. package/dist/capture/capture-export-metadata.d.ts.map +0 -1
  817. package/dist/capture/environment-raw-producer.d.ts.map +0 -1
  818. package/dist/capture/export-jpeg.d.ts.map +0 -1
  819. package/dist/capture/export-tiff.d.ts.map +0 -1
  820. package/dist/capture/focus-control.d.ts.map +0 -1
  821. package/dist/capture/linear-encoding.d.ts.map +0 -1
  822. package/dist/capture/native-environment-raw.d.ts.map +0 -1
  823. package/dist/capture/native-raw-development.d.ts.map +0 -1
  824. package/dist/capture/native-raw.d.ts.map +0 -1
  825. package/dist/capture/photographic-export.d.ts.map +0 -1
  826. package/dist/capture/raw-frame-limits.d.ts.map +0 -1
  827. package/dist/capture/raw-frame-reconstruction.d.ts.map +0 -1
  828. package/dist/capture/release-sequence.d.ts.map +0 -1
  829. package/dist/capture/sensor-raw-frame.d.ts.map +0 -1
  830. package/dist/capture/sensor-raw-producer.d.ts.map +0 -1
  831. package/dist/capture/simulated-capture.d.ts.map +0 -1
  832. package/dist/color/capture-color.d.ts.map +0 -1
  833. package/dist/color/sensor-color-development.d.ts.map +0 -1
  834. package/dist/color/srgb-icc.d.ts.map +0 -1
  835. package/dist/color/srgb-transfer.d.ts.map +0 -1
  836. package/dist/color/white-balance.d.ts.map +0 -1
  837. package/dist/composition/image-formation-plan.d.ts.map +0 -1
  838. package/dist/composition/plan-consumer.d.ts.map +0 -1
  839. package/dist/core/calculation-result.d.ts.map +0 -1
  840. package/dist/core/canonical-json.d.ts.map +0 -1
  841. package/dist/core/configuration-error.d.ts.map +0 -1
  842. package/dist/core/evidence-provenance.d.ts.map +0 -1
  843. package/dist/core/image-formation.d.ts.map +0 -1
  844. package/dist/core/owned-data.d.ts.map +0 -1
  845. package/dist/core/record-validation.d.ts.map +0 -1
  846. package/dist/core/scientific-assurance.d.ts.map +0 -1
  847. package/dist/core/spectral-composition.d.ts.map +0 -1
  848. package/dist/core/spectral.d.ts.map +0 -1
  849. package/dist/core/validation.d.ts.map +0 -1
  850. package/dist/core/version.d.ts.map +0 -1
  851. package/dist/equipment/exposure-capabilities.d.ts.map +0 -1
  852. package/dist/equipment/generic-tier-assets.d.ts.map +0 -1
  853. package/dist/equipment/generic-tier-presets.d.ts.map +0 -1
  854. package/dist/equipment/iso-capabilities.d.ts.map +0 -1
  855. package/dist/equipment/metering-capabilities.d.ts.map +0 -1
  856. package/dist/equipment/release-capabilities.d.ts.map +0 -1
  857. package/dist/exposure/exposure-duration-control.d.ts.map +0 -1
  858. package/dist/exposure/exposure-mode-resolver.d.ts.map +0 -1
  859. package/dist/exposure/exposure.d.ts.map +0 -1
  860. package/dist/exposure/flash.d.ts.map +0 -1
  861. package/dist/exposure/metering-scene-radiance.d.ts.map +0 -1
  862. package/dist/exposure/metering-target.d.ts.map +0 -1
  863. package/dist/exposure/metering-temporal.d.ts.map +0 -1
  864. package/dist/exposure/metering.d.ts.map +0 -1
  865. package/dist/exposure/program-line.d.ts.map +0 -1
  866. package/dist/index.d.ts.map +0 -1
  867. package/dist/motion/camera-rotation.d.ts.map +0 -1
  868. package/dist/motion/capture-rotation-inverse-mapping.d.ts.map +0 -1
  869. package/dist/motion/capture-rotation-temporal-quadrature.d.ts.map +0 -1
  870. package/dist/motion/capture-rotation-trajectory.d.ts.map +0 -1
  871. package/dist/motion/capture-translation-parallax-temporal-quadrature.d.ts.map +0 -1
  872. package/dist/motion/extended-object-projection.d.ts.map +0 -1
  873. package/dist/motion/projected-motion.d.ts.map +0 -1
  874. package/dist/optics/aperture.d.ts.map +0 -1
  875. package/dist/optics/circle-of-confusion.d.ts.map +0 -1
  876. package/dist/optics/complex-pupil-psf.d.ts.map +0 -1
  877. package/dist/optics/depth-of-field.d.ts.map +0 -1
  878. package/dist/optics/diffraction.d.ts.map +0 -1
  879. package/dist/optics/focus-breathing.d.ts.map +0 -1
  880. package/dist/optics/focus-state.d.ts.map +0 -1
  881. package/dist/optics/front-of-lens-filter.d.ts.map +0 -1
  882. package/dist/optics/illumination-vignetting.d.ts.map +0 -1
  883. package/dist/optics/lateral-chromatic-aberration.d.ts.map +0 -1
  884. package/dist/optics/lens-psf-profile.d.ts.map +0 -1
  885. package/dist/optics/polygon-diffraction.d.ts.map +0 -1
  886. package/dist/optics/profile-contract.d.ts.map +0 -1
  887. package/dist/optics/psf-foundation.d.ts.map +0 -1
  888. package/dist/optics/radial-distortion.d.ts.map +0 -1
  889. package/dist/optics/scene-to-sensor-irradiance.d.ts.map +0 -1
  890. package/dist/optics/scene-to-sensor-quadrature.d.ts.map +0 -1
  891. package/dist/optics/sensor-aperture-rays.d.ts.map +0 -1
  892. package/dist/optics/sensor-environment-query.d.ts.map +0 -1
  893. package/dist/optics/sensor-psf-quadrature.d.ts.map +0 -1
  894. package/dist/optics/sensor-psf-support.d.ts.map +0 -1
  895. package/dist/optics/stray-light.d.ts.map +0 -1
  896. package/dist/optics/thin-lens.d.ts.map +0 -1
  897. package/dist/output/capture-corrected-sdr.d.ts.map +0 -1
  898. package/dist/output/capture-geometry.d.ts.map +0 -1
  899. package/dist/output/capture-sdr.d.ts.map +0 -1
  900. package/dist/output/crop.d.ts.map +0 -1
  901. package/dist/output/geometric-transforms.d.ts.map +0 -1
  902. package/dist/output/lens-corrections.d.ts.map +0 -1
  903. package/dist/output/native-capture-sdr.d.ts.map +0 -1
  904. package/dist/output/print-contrast-reference.d.ts.map +0 -1
  905. package/dist/output/print-detail.d.ts.map +0 -1
  906. package/dist/output/print-jpeg.d.ts.map +0 -1
  907. package/dist/output/print-plan.d.ts.map +0 -1
  908. package/dist/output/print-region-statistics.d.ts.map +0 -1
  909. package/dist/output/sdr-rendering.d.ts.map +0 -1
  910. package/dist/output/subject-framing-crop.d.ts.map +0 -1
  911. package/dist/schema/camera.d.ts.map +0 -1
  912. package/dist/schema/camera.js +0 -3
  913. package/dist/schema/camera.js.map +0 -1
  914. package/dist/schema/illumination-spectral.d.ts.map +0 -1
  915. package/dist/schema/illumination-temporal.d.ts.map +0 -1
  916. package/dist/schema/illumination.d.ts.map +0 -1
  917. package/dist/schema/scene-radiance-temporal.d.ts.map +0 -1
  918. package/dist/schema/scene-radiance.d.ts.map +0 -1
  919. package/dist/schema/scene.d.ts.map +0 -1
  920. package/dist/schema/scene.js +0 -3
  921. package/dist/schema/scene.js.map +0 -1
  922. package/dist/schema/validation.d.ts.map +0 -1
  923. package/dist/sensor/accumulated-charge.d.ts.map +0 -1
  924. package/dist/sensor/architecture.d.ts.map +0 -1
  925. package/dist/sensor/camera-saturation-capacity.d.ts.map +0 -1
  926. package/dist/sensor/capture-color-sampling-binding.d.ts.map +0 -1
  927. package/dist/sensor/capture-mode-timing.d.ts.map +0 -1
  928. package/dist/sensor/capture-mode.d.ts.map +0 -1
  929. package/dist/sensor/color-sampling.d.ts.map +0 -1
  930. package/dist/sensor/constant-rate-temporal-integration.d.ts.map +0 -1
  931. package/dist/sensor/dark-current.d.ts.map +0 -1
  932. package/dist/sensor/environment-photo-signal.d.ts.map +0 -1
  933. package/dist/sensor/eqe-electron-rate.d.ts.map +0 -1
  934. package/dist/sensor/eqe-local-exposure.d.ts.map +0 -1
  935. package/dist/sensor/eqe-temporal-exposure.d.ts.map +0 -1
  936. package/dist/sensor/exposure-window.d.ts.map +0 -1
  937. package/dist/sensor/iso-signal-chain.d.ts.map +0 -1
  938. package/dist/sensor/local-exposure-binding.d.ts.map +0 -1
  939. package/dist/sensor/optical-stack.d.ts.map +0 -1
  940. package/dist/sensor/photon-energy.d.ts.map +0 -1
  941. package/dist/sensor/physical-charge-capacity.d.ts.map +0 -1
  942. package/dist/sensor/pixel-pitch.d.ts.map +0 -1
  943. package/dist/sensor/radiometry-readiness.d.ts.map +0 -1
  944. package/dist/sensor/raw-readout.d.ts.map +0 -1
  945. package/dist/sensor/raw-reconstruction.d.ts.map +0 -1
  946. package/dist/sensor/readout-exposure-linkage.d.ts.map +0 -1
  947. package/dist/sensor/readout-timing.d.ts.map +0 -1
  948. package/dist/sensor/response-application-compatibility.d.ts.map +0 -1
  949. package/dist/sensor/response-operating-range.d.ts.map +0 -1
  950. package/dist/sensor/responsivity-photocurrent.d.ts.map +0 -1
  951. package/dist/sensor/sampling-aperture.d.ts.map +0 -1
  952. package/dist/sensor/scene-eqe-temporal-exposure.d.ts.map +0 -1
  953. package/dist/sensor/sensor-geometry.d.ts.map +0 -1
  954. package/dist/sensor/signal-noise.d.ts.map +0 -1
  955. package/dist/sensor/spatial-sample-reduction.d.ts.map +0 -1
  956. package/dist/sensor/spatial-sampling-quadrature.d.ts.map +0 -1
  957. package/dist/sensor/spatio-spectral-reduction.d.ts.map +0 -1
  958. package/dist/sensor/spectral-coverage.d.ts.map +0 -1
  959. package/dist/sensor/spectral-quadrature.d.ts.map +0 -1
  960. package/dist/sensor/spectral-response.d.ts.map +0 -1
  961. package/dist/sensor/temporal-eqe-sum.d.ts.map +0 -1
  962. package/dist/sensor/temporal-photo-signal.d.ts.map +0 -1
  963. package/dist/sensor/temporal-sample-binding.d.ts.map +0 -1
  964. package/dist/simulation/poc-simulation.d.ts.map +0 -1
  965. package/dist/stabilization/camera-shake.d.ts.map +0 -1
  966. package/dist/stabilization/system.d.ts.map +0 -1
  967. package/docs/API_STYLE.md +0 -235
  968. package/docs/ARCHITECTURE.md +0 -356
  969. package/docs/AUTOMATION_COORDINATION.md +0 -190
  970. package/docs/CAPTURE_COLOR.md +0 -25
  971. package/docs/CAPTURE_CORRECTED_SDR.md +0 -25
  972. package/docs/CAPTURE_EXPORT_METADATA.md +0 -27
  973. package/docs/CAPTURE_SDR.md +0 -22
  974. package/docs/DENSE_PUPIL_QUALIFICATION.md +0 -121
  975. package/docs/DEVELOPERS.md +0 -68
  976. package/docs/DIGITAL_OPTICS_FOUNDATION.md +0 -69
  977. package/docs/ENVIRONMENT_RAW_CAPTURE.md +0 -208
  978. package/docs/EXPORT_ACCEPTANCE.md +0 -133
  979. package/docs/GENERIC_TIER_PRESETS.md +0 -269
  980. package/docs/GEOMETRIC_PREPARATION_REUSE.md +0 -141
  981. package/docs/IMAGE_FORMATION.md +0 -247
  982. package/docs/LINEAR_CAPTURE_ENCODING.md +0 -25
  983. package/docs/MOTION_AND_SIGNAL.md +0 -250
  984. package/docs/NATIVE_CAPTURE_SDR.md +0 -70
  985. package/docs/NATIVE_CAPTURE_SDR_REVIEW.md +0 -37
  986. package/docs/NATIVE_CAPTURE_SDR_VALIDATION.md +0 -34
  987. package/docs/NATIVE_RAW_AND_PRINT.md +0 -54
  988. package/docs/NUMERICAL_CORRECTNESS.md +0 -191
  989. package/docs/PERFORMANCE_BASELINE.md +0 -56
  990. package/docs/PHOTOGRAPHIC_EXPORT.md +0 -206
  991. package/docs/PHYSICS_FOUNDATION.md +0 -426
  992. package/docs/POC_API.md +0 -278
  993. package/docs/POC_PROJECTION_PLATFORM_REFERENCE.md +0 -12
  994. package/docs/POC_PROJECTION_REUSE.md +0 -124
  995. package/docs/PRINT_195_196_REVIEW.md +0 -93
  996. package/docs/PRINT_196_ENGINE_ACCEPTANCE.md +0 -45
  997. package/docs/PRINT_CONTRAST_REFERENCE.md +0 -43
  998. package/docs/PRINT_DETAIL_ASSESSMENT.md +0 -81
  999. package/docs/PRINT_DETAIL_JPEG_EVIDENCE.md +0 -36
  1000. package/docs/PRINT_DETAIL_SDR_EVIDENCE.md +0 -29
  1001. package/docs/PRINT_DETAIL_VALIDATION.md +0 -36
  1002. package/docs/PRINT_PLANNING.md +0 -75
  1003. package/docs/PRINT_REGION_STATISTICS.md +0 -38
  1004. package/docs/PROCESSED_OUTPUT.md +0 -157
  1005. package/docs/PRODUCTION_COMPOSITION.md +0 -373
  1006. package/docs/PRODUCTION_ENVIRONMENT_CAPTURE.md +0 -131
  1007. package/docs/PROVENANCE.md +0 -115
  1008. package/docs/PSF_FOUNDATION.md +0 -165
  1009. package/docs/RAW_FRAME_ENVELOPE.md +0 -56
  1010. package/docs/RAW_FRAME_RECONSTRUCTION.md +0 -30
  1011. package/docs/RELEASE_1_0.md +0 -66
  1012. package/docs/RELEASE_1_0_1.md +0 -61
  1013. package/docs/RELEASE_1_1_0.md +0 -25
  1014. package/docs/RELEASE_1_2_0.md +0 -51
  1015. package/docs/RELEASE_1_3_0.md +0 -15
  1016. package/docs/RELEASE_1_4_0.md +0 -23
  1017. package/docs/RELEASE_1_5_0.md +0 -105
  1018. package/docs/RELEASE_AUDIT.md +0 -39
  1019. package/docs/SCENE_RADIANCE_AND_ILLUMINATION.md +0 -298
  1020. package/docs/SCENE_SENSOR_QUADRATURE.md +0 -98
  1021. package/docs/SCENE_SENSOR_TEMPORAL_EXPOSURE.md +0 -86
  1022. package/docs/SCIENTIFIC_ASSURANCE.md +0 -280
  1023. package/docs/SCIENTIFIC_CONFORMANCE.md +0 -83
  1024. package/docs/SDR_RENDERING.md +0 -33
  1025. package/docs/SENSOR_APERTURE_RAYS.md +0 -23
  1026. package/docs/SENSOR_ENVIRONMENT_QUERY.md +0 -86
  1027. package/docs/SENSOR_EQE_LOCAL_EXPOSURE.md +0 -104
  1028. package/docs/SENSOR_EQE_TEMPORAL_EXPOSURE.md +0 -74
  1029. package/docs/SENSOR_PSF_QUADRATURE.md +0 -90
  1030. package/docs/SENSOR_RAW_FRAME.md +0 -20
  1031. package/docs/SENSOR_RAW_PRODUCER.md +0 -77
  1032. package/docs/SEPARABLE_EMISSION_DRAFT.md +0 -103
  1033. package/docs/SIGNAL_CONFORMANCE.md +0 -23
  1034. package/docs/SIMULATED_CAPTURE.md +0 -65
  1035. package/docs/STABILIZATION.md +0 -234
  1036. package/docs/TEMPORAL_PHOTO_RAW.md +0 -83
  1037. package/docs/TIER_ACCEPTANCE.md +0 -129
  1038. package/docs/TIER_PRESET_ACCEPTANCE.md +0 -59
  1039. package/docs/V1_COMPOSITION_MAP.md +0 -123
  1040. package/docs/V1_HELPER_REUSE_REVIEW.md +0 -60
  1041. package/docs/V1_PERFORMANCE_DISPOSITION.md +0 -88
  1042. package/docs/benchmarks/2026-10-01-main-baseline.json +0 -694
  1043. package/docs/benchmarks/2026-10-02-geometric-preparation-reuse.json +0 -2190
  1044. package/docs/benchmarks/2026-10-02-poc-projection-reuse.json +0 -2423
  1045. package/docs/benchmarks/2026-10-02-v1-environment-gc-profile.json +0 -210
  1046. package/docs/benchmarks/2026-10-02-v1-environment-ordinary.json +0 -187
  1047. package/docs/benchmarks/2026-10-02-v1-geometry-gc-profile.json +0 -756
  1048. package/docs/benchmarks/2026-10-02-v1-geometry-ordinary-1.json +0 -711
  1049. package/docs/benchmarks/2026-10-02-v1-geometry-ordinary-2.json +0 -711
  1050. package/docs/benchmarks/2026-10-02-v1-poc-gc-profile.json +0 -885
  1051. package/docs/benchmarks/2026-10-02-v1-poc-ordinary-1.json +0 -868
  1052. package/docs/benchmarks/2026-10-02-v1-poc-ordinary-2.json +0 -868
  1053. package/docs/licenses/castleCSF-MIT.txt +0 -21
  1054. package/docs/validation/dense-pupil-2026-10-06/pilot.jsonl +0 -6
  1055. package/docs/validation/documentation-fragments.json +0 -212
  1056. package/docs/validation/native-capture-sdr-darwin-arm64-node26-review.jsonl +0 -4
  1057. package/docs/validation/native-capture-sdr-darwin-arm64.jsonl +0 -3
  1058. package/docs/validation/native-raw-print-2026-10-04/fractional-asymmetric-crop.jpg +0 -0
  1059. package/docs/validation/native-raw-print-2026-10-04/gray-ramp.jpg +0 -0
  1060. package/docs/validation/native-raw-print-2026-10-04/linear-area-checker.jpg +0 -0
  1061. package/docs/validation/native-raw-print-2026-10-04/manifest.json +0 -218
  1062. package/docs/validation/native-raw-print-2026-10-04/megapixel-native.jpg +0 -0
  1063. package/docs/validation/native-raw-print-2026-10-04/native-color.jpg +0 -0
  1064. package/docs/validation/native-raw-print-2026-10-04/photivra-native-full-megapixel-validation.json +0 -9
  1065. package/docs/validation/native-raw-print-2026-10-04/photivra-native-full-megapixel.json +0 -29
  1066. package/docs/validation/native-raw-print-2026-10-04/photivra-native-full-megapixel.json.codes.u16le.gz +0 -0
  1067. package/docs/validation/native-raw-print-2026-10-04/photivra-native-full-megapixel.json.jpg +0 -0
  1068. package/docs/validation/native-raw-print-2026-10-04/photivra-native-full-megapixel.json.rgb.gz +0 -0
  1069. package/docs/validation/native-raw-print-2026-10-04/srgb.icc +0 -0
  1070. package/docs/validation/native-raw-print-2026-10-04/validation.json +0 -41
  1071. package/docs/validation/print-196-candidate-capabilities.json +0 -28
  1072. package/docs/validation/print-196-candidate-evidence.json +0 -356
  1073. package/docs/validation/print-196-engine-acceptance.json +0 -81
  1074. package/docs/validation/print-detail-capabilities.json +0 -103
  1075. package/docs/validation/print-detail-jpeg-evidence.json +0 -116
  1076. package/docs/validation/print-detail-sdr-evidence.json +0 -81
  1077. package/docs/validation/release-audit-inventory.json +0 -1306
  1078. package/docs/validation/tier-acceptance-report-v1.json +0 -2538
package/docs/USAGE.md CHANGED
@@ -1,22 +1,25 @@
1
1
  # Usage Guide
2
2
 
3
- Release context: **package 1.2.0 candidate / root API 1.2.0**. Subsystem/model versions and dated introduction or measurement records below are independent historical identities; they are not distribution versions. See [developer navigation](DEVELOPERS.md) and [release contract](RELEASE_1_2_0.md).
3
+ Release context (dated historical note): this guide was written for the **package 1.2.0 candidate / root API 1.2.0** and has not been re-reviewed for later releases; see the [changelog](../CHANGELOG.md) for the current release. Subsystem and model versions, and dated introduction or measurement records below, are independent historical identities. They are not distribution versions. See [developer navigation](https://github.com/Photivra/photivra/blob/main/docs/DEVELOPERS.md) and the [release contract](https://github.com/Photivra/photivra/blob/main/docs/RELEASE_1_2_0.md).
4
4
 
5
- For bounded capture-owned charge/noise/ADC → native RAW generation, see
6
- [Sensor RAW producer](SENSOR_RAW_PRODUCER.md). Upstream EQE/dark exposure results
7
- remain declared; the returned frame can feed the same-RAW photographic exporter.
5
+ New to the engine? Start with [`simulateCapture()`](#simulate-a-capture-with-the-facade). It builds
6
+ and runs a complete capture from a few options. The [Quick Start](QUICK_START.md) explains it in full.
8
7
 
9
- For bounded committed capture → correction → SDR output, see
10
- [Capture-bound corrected SDR](CAPTURE_CORRECTED_SDR.md). It returns a derived output
11
- view without changing physical capture geometry or activating production stages.
8
+ This guide shows how to call the capabilities exported by the root `@photivra/engine` package.
12
9
 
13
- For bounded same-RAW paired DNG/JPEG byte export, see
14
- [Photographic export](PHOTOGRAPHIC_EXPORT.md). It requires an explicit approximate
15
- sensor-color profile; bounded external acceptance and its evidence limits are recorded in [Export acceptance](EXPORT_ACCEPTANCE.md).
10
+ Related guides for later pipeline stages:
16
11
 
17
- This guide shows how to call the capabilities exported by the root `@photivra/engine` package.
12
+ - **Charge, noise and ADC to native RAW:** see [Sensor RAW producer](https://github.com/Photivra/photivra/blob/main/docs/SENSOR_RAW_PRODUCER.md).
13
+ It generates RAW within stated limits for one capture. The upstream EQE and dark-exposure results stay
14
+ as declared inputs. The returned frame can feed the photographic exporter, which writes DNG and JPEG from the same RAW.
15
+ - **Committed capture → correction → SDR output:** see [Capture-bound corrected SDR](https://github.com/Photivra/photivra/blob/main/docs/CAPTURE_CORRECTED_SDR.md).
16
+ It returns a derived output view without changing the physical capture geometry or turning on production stages.
17
+ - **Paired DNG/JPEG byte export from the same RAW:** see [Photographic export](https://github.com/Photivra/photivra/blob/main/docs/PHOTOGRAPHIC_EXPORT.md).
18
+ It needs an explicit, approximate sensor-colour profile. Bounded external acceptance, and the limits of that evidence, are recorded in [Export acceptance](https://github.com/Photivra/photivra/blob/main/docs/EXPORT_ACCEPTANCE.md).
19
+
20
+ Common abbreviations: FOV (field of view), PSF (point-spread function — how a lens spreads one point of light), CFA (colour filter array, such as a Bayer mosaic), QE/EQE (quantum efficiency / external quantum efficiency), ADC (analog-to-digital converter), AE/AF/WB (auto-exposure / autofocus / white balance), SDR (standard dynamic range).
18
21
 
19
- The package is ESM-only. Primitive scientific calculations generally return a `CalculationResult<T>`:
22
+ The package is ESM. CommonJS code on Node 22.12 or later can also `require()` it. Primitive scientific calculations usually return a `CalculationResult<T>`:
20
23
 
21
24
  ```ts
22
25
  const result = calculateSomething(input);
@@ -26,11 +29,13 @@ console.log(result.provenance);
26
29
  console.log(result.quality);
27
30
  ```
28
31
 
29
- `quality` is optional and is omitted when the engine does not have defensible quantified uncertainty/accuracy metadata to report.
32
+ `quality` is optional. It is left out when the engine has no defensible, quantified uncertainty or accuracy to report.
33
+
34
+ From 2.0.0 results are read-only: the envelope, `provenance` and `quality` are frozen, and so is the top level of `value`. Copy a value (`{ ...result.value }`) before changing it. Every calculator that takes an input object rejects unknown fields with `InvalidConfigurationError`. A wrong shape throws `InvalidConfigurationError` and a bad number throws `InvalidScientificInputError`; see [Error classes](https://github.com/Photivra/photivra/blob/main/docs/API_STYLE.md#error-classes) and [Read-only results](https://github.com/Photivra/photivra/blob/main/docs/API_STYLE.md#read-only-results).
30
35
 
31
36
  ## Standalone digital optics
32
37
 
33
- See [Digital optics foundations](DIGITAL_OPTICS_FOUNDATION.md) for exact-state stray-light/correction profiles and scientific boundaries. A compatible map can be prepared separately from its sampling filter:
38
+ See [Digital optics foundations](https://github.com/Photivra/photivra/blob/main/docs/DIGITAL_OPTICS_FOUNDATION.md) for stray-light and correction profiles tied to an exact lens state, and for their scientific limits. You can prepare a compatible geometric map separately from its sampling filter:
34
39
 
35
40
  ```ts
36
41
  import {
@@ -67,7 +72,12 @@ const warped = calculateGeometricResampling({
67
72
  // plan.jointCrop and validSourceMask preserve the actual support.
68
73
  ```
69
74
 
70
- This warps captured samples; it does not recalculate the physical lens PSF or regenerate noise. Compression requires explicitly source-prefiltered data; incompatible domains require separate stages. Stray light instead adds to incident spectral irradiance before the sensor. RAW-like correction intent can remain metadata-only.
75
+ This warps samples that were already captured. It does not recalculate the physical lens PSF or regenerate noise.
76
+
77
+ - Compression (shrinking) needs source data that was explicitly prefiltered.
78
+ - Incompatible domains need separate stages.
79
+ - Stray light is different: it adds to the incident spectral irradiance before the sensor.
80
+ - A correction intended for RAW-like data can stay as metadata only.
71
81
 
72
82
  ## Image-formation ownership
73
83
 
@@ -87,11 +97,12 @@ console.log(
87
97
  );
88
98
  ```
89
99
 
90
- The contract is descriptive metadata. It does not imply that every reserved stage is implemented, and it does not change the composed POC request/response contract.
100
+ The contract is descriptive metadata. It does not mean every reserved stage is implemented, and it does not change the composed POC (proof-of-concept) request/response contract.
91
101
 
92
- Use `requiredUpstreamStages` for hard scientific dependencies and `coupledStages` for shared state/interactions that must not be treated as independent renderer filters.
102
+ - `requiredUpstreamStages` lists hard scientific dependencies.
103
+ - `coupledStages` lists shared state and interactions. Do not treat coupled stages as independent renderer filters.
93
104
 
94
- See [Image-Formation Contract](IMAGE_FORMATION.md) for coordinate, temporal, renderer, and reserved sensor-stage semantics.
105
+ See [Image-Formation Contract](https://github.com/Photivra/photivra/blob/main/docs/IMAGE_FORMATION.md) for coordinate, temporal, renderer, and reserved sensor-stage semantics.
95
106
 
96
107
  ## Ideal regular-polygon diffraction
97
108
 
@@ -110,11 +121,18 @@ const polygon = calculateIdealPolygonDiffractionPsf({
110
121
  console.log(polygon.value.samples[0]?.intensityDensityPerSquareMicrometer);
111
122
  ```
112
123
 
113
- This is a uniform-amplitude, zero-phase, on-axis, in-focus scalar approximation. Equal-area diameter is not polygon circumdiameter. Image points are PSF displacements (+X right / +Y up), not source-field positions. Density integrates to one over the **infinite** plane; supplied samples do not sum to one. Finite image-plane quadrature needs explicit area measures, support and convergence checks. No air/vacuum conversion, clipping, aberration, throughput or sensor response is inferred. Circular Airy and already-diffracted #113 kernels remain separate; do not stack diffraction twice.
124
+ This is a scalar approximation: uniform amplitude, zero phase, on-axis and in focus.
125
+
126
+ - The equal-area diameter is not the polygon's circumdiameter.
127
+ - Image points are displacements within the PSF (+X right / +Y up), not positions in the source field.
128
+ - The density integrates to one over the **infinite** plane. The samples you request do not sum to one.
129
+ - Integrating over a finite image area (quadrature) needs explicit area measures, support and convergence checks.
130
+ - Nothing is inferred: no air/vacuum conversion, clipping, aberration, throughput or sensor response.
131
+ - The circular Airy diagnostic and the real-lens kernels (which already include diffraction) are separate. Do not apply diffraction twice.
114
132
 
115
133
  ## Explicit finite and infinity focus state
116
134
 
117
- Use `FocusPlane` when a caller must distinguish a real finite focus plane from optical infinity without JavaScript `Infinity` or an arbitrary very-large distance.
135
+ Use `FocusPlane` when you must tell a real finite focus plane apart from optical infinity, without using JavaScript `Infinity` or an arbitrary very large distance.
118
136
 
119
137
  ```ts
120
138
  import {
@@ -140,23 +158,23 @@ console.log(projection.value.imageDistanceMm); // 50
140
158
  console.log(projection.value.infinityProjectionScale); // 1
141
159
  ```
142
160
 
143
- For finite focus, `distanceM` is the **longitudinal object-plane distance along the camera optical axis from the ideal lens principal plane**. A renderer/raycast layer must not pass raw Euclidean ray length directly for an off-axis hit. Points on the same fronto-parallel plane should resolve to the same finite focus distance in the current paraxial model.
144
-
145
- The finite branch delegates to the existing Gaussian thin-lens calculation, so existing finite-focus numeric behavior and provenance are preserved. The infinity branch resolves exactly to nominal focal length with zero limiting magnification.
161
+ For finite focus, `distanceM` is the **distance of the object plane along the camera's optical axis, measured from the ideal lens principal plane**. It is not the straight-line ray length. A renderer or raycaster must not pass the raw Euclidean ray length for an off-axis hit. In the current paraxial model, points on the same plane facing the camera (fronto-parallel) resolve to the same finite focus distance.
146
162
 
147
- Missing or unknown focus is not interpreted as infinity. It must be supplied explicitly as either `{ kind: "finite", distanceM }` or `{ kind: "infinity" }`.
163
+ - The finite branch uses the existing Gaussian thin-lens calculation, so existing finite-focus numbers and provenance are unchanged.
164
+ - The infinity branch resolves exactly to the nominal focal length, with zero limiting magnification.
165
+ - Missing or unknown focus is never treated as infinity. Supply it explicitly as `{ kind: "finite", distanceM }` or `{ kind: "infinity" }`.
148
166
 
149
- Existing APIs that accept `focusDistanceM` remain supported; this tagged contract is additive and intended for newer composition/control paths that need an explicit infinity state.
167
+ Existing APIs that take `focusDistanceM` are still supported. This tagged contract is additive. It is meant for newer composition and control paths that need an explicit infinity state.
150
168
 
151
169
  ## Focus control: MF, single AF, continuous AF, and lock
152
170
 
153
- The #104 focus-control layer sits above the #103 optical `FocusPlane` contract.
171
+ The focus-control layer (#104) sits on top of the optical `FocusPlane` contract described above (#103). (MF = manual focus, AF = autofocus.)
154
172
 
155
- It deliberately separates:
173
+ It keeps three things separate:
156
174
 
157
- 1. **target observation** — which scene surface/environment target was measured and its longitudinal camera-space distance;
158
- 2. **focus-control policy** — manual, single AF, continuous AF, or locked behavior;
159
- 3. **resolved optical focus** — the finite/infinity `FocusPlane` passed into downstream optics.
175
+ 1. **target observation** — which scene surface or environment target was measured, and its distance along the camera axis;
176
+ 2. **focus-control policy** — manual, single AF, continuous AF, or locked;
177
+ 3. **resolved optical focus** — the finite or infinity `FocusPlane` passed on to the optics.
160
178
 
161
179
  Use a generic profile:
162
180
 
@@ -223,13 +241,13 @@ const acquired =
223
241
  });
224
242
  ```
225
243
 
226
- Target observations contain no screen/canvas coordinates. `focusAreaId` is an opaque application/renderer-neutral selection identity. Finite target distance is #103's **longitudinal camera-space/conjugate distance**, not Euclidean ray length.
244
+ Target observations contain no screen or canvas coordinates. `focusAreaId` is an opaque selection ID that does not depend on the application or renderer. A finite target distance is the `FocusPlane` **distance along the camera axis (the conjugate distance)**, not the Euclidean ray length.
227
245
 
228
246
  ### Manual focus
229
247
 
230
248
  `setManualFocusState()` changes an explicit finite/infinity focus state only when the active mode is `manual`.
231
249
 
232
- No universal focus-ring scale or lens-specific stepping curve is invented. The caller supplies the next explicit #103 focus state; future equipment profiles may map real focus-ring/actuator behavior into that same contract.
250
+ No universal focus-ring scale or lens-specific stepping curve is invented. The caller supplies the next explicit `FocusPlane` state. Future equipment profiles may map real focus-ring or actuator behaviour onto that same contract.
233
251
 
234
252
  MF does not automatically consume autofocus target observations.
235
253
 
@@ -237,7 +255,7 @@ MF does not automatically consume autofocus target observations.
237
255
 
238
256
  A `single-af` `acquire` event resolves the target once and then holds that focus plane.
239
257
 
240
- Later `update` observations do not follow subject motion. If the subject moves after acquisition, downstream defocus changes because subject depth changes while the camera focus plane remains fixed.
258
+ Later `update` observations do not follow subject motion. If the subject moves after acquisition, its depth changes while the focus plane stays fixed, so the defocus calculated downstream changes.
241
259
 
242
260
  An unavailable target during acquisition produces explicit `target-lost` state without moving focus.
243
261
 
@@ -245,13 +263,13 @@ An unavailable target during acquisition produces explicit `target-lost` state w
245
263
 
246
264
  `continuous-af` requires an explicit initial `acquire` event.
247
265
 
248
- Subsequent `update` events may move focus only for the same active `targetId`. Another identity cannot silently replace the subject.
266
+ After that, `update` events may move focus only for the same active `targetId`. A different target cannot silently replace the subject.
249
267
 
250
268
  The first actuator is:
251
269
 
252
270
  `ideal-instantaneous`
253
271
 
254
- This is a declared approximation over known simulation geometry. It does **not** claim realistic AF motor speed, phase/contrast detection, hunting, recognition, low-light failure, or commercial-camera tracking behavior.
272
+ This is a declared approximation that uses the known simulation geometry. It does **not** claim realistic AF motor speed, phase or contrast detection, hunting, subject recognition, low-light failure, or the tracking behaviour of any commercial camera.
255
273
 
256
274
  When the target becomes unavailable, the first policy is:
257
275
 
@@ -261,23 +279,23 @@ Focus is held, state becomes `target-lost`, and an ordinary update cannot silent
261
279
 
262
280
  ### Focus lock
263
281
 
264
- `setFocusLock()` freezes the resolved focus plane while preserving the active mode and the acquisition state to resume later.
282
+ `setFocusLock()` freezes the resolved focus plane. It keeps the active mode and the acquisition state, so they can resume later.
265
283
 
266
- Target updates while locked do not change focus. Unlocking restores the prior acquisition state; a later continuous-AF update may then move focus again.
284
+ Target updates while locked do not change focus. Unlocking restores the previous acquisition state; a later continuous-AF update may then move focus again.
267
285
 
268
- Focus lock does not engage AE lock, move a metering spot, change WB, or alter aperture/shutter/ISO.
286
+ Focus lock does not engage AE lock, move a metering spot, change WB, or change aperture, shutter or ISO.
269
287
 
270
288
  ### Focus vs release priority
271
289
 
272
- Use `assessFocusReleaseGate()` as the capture-gating seam for #105 release sequencing.
290
+ Use `assessFocusReleaseGate()` to decide whether the shutter may release, as input to [release sequencing](#logical-release-sequences) (#105).
273
291
 
274
- - `focus-priority` authorizes release for manual focus, acquired AF, or locked focus and otherwise blocks;
275
- - `release-priority` authorizes release without changing focus;
276
- - `balanced` is reserved but fails closed in schema 0.1.0 until a specific balanced policy is defined.
292
+ - `focus-priority` allows release for manual focus, acquired AF or locked focus, and blocks otherwise.
293
+ - `release-priority` allows release without changing focus.
294
+ - `balanced` is reserved. In schema 0.1.0 it fails closed (blocks) until a specific balanced policy is defined.
277
295
 
278
- The gate never changes the optical focus calculation or exposure state.
296
+ The gate never changes the optical focus calculation or the exposure state.
279
297
 
280
- The acquisition-state type also reserves `acquiring` for future non-instantaneous actuator models. The current ideal actuator resolves an available acquisition directly to `acquired`.
298
+ The acquisition-state type also reserves `acquiring` for future actuator models that take time. The current ideal actuator moves an available acquisition straight to `acquired`.
281
299
 
282
300
  ## Field of view
283
301
 
@@ -307,7 +325,7 @@ console.log(focused.value.projectionDistanceMm);
307
325
  console.log(focused.value.degrees);
308
326
  ```
309
327
 
310
- Without `focusDistanceM`, nominal focal length is used as the infinity-focus/pinhole projection distance. Neither mode models real-lens distortion or focus breathing.
328
+ Without `focusDistanceM`, the nominal focal length is used as the projection distance (infinity focus, like a pinhole). Neither mode models real-lens distortion or focus breathing.
311
329
 
312
330
  For off-center sensor regions, use `calculateFieldOfViewBounds()` with signed sensor-plane coordinates relative to the optical axis:
313
331
 
@@ -325,13 +343,13 @@ console.log(leftHalf.value.maximumDegrees);
325
343
  console.log(leftHalf.value.degrees);
326
344
  ```
327
345
 
328
- Unlike the centered `2 × atan(size / 2d)` form, the bounds API preserves asymmetric angular limits and is used by active-capture geometry for off-center crops.
346
+ Unlike the centered `2 × atan(size / 2d)` form, the bounds API keeps asymmetric angular limits. Active-capture geometry uses it for off-center crops.
329
347
 
330
- See [Physics Foundation](PHYSICS_FOUNDATION.md#rectilinear-field-of-view).
348
+ See [Physics Foundation](https://github.com/Photivra/photivra/blob/main/docs/PHYSICS_FOUNDATION.md#rectilinear-field-of-view).
331
349
 
332
350
  ## Actual and 35 mm-equivalent focal length
333
351
 
334
- Use `calculateEquivalentFocalLength35Mm()` when you need the conventional diagonal-based 35 mm-equivalent value while preserving the physical optical focal length:
352
+ Use `calculateEquivalentFocalLength35Mm()` when you need the conventional 35 mm-equivalent value (based on the diagonal), while keeping the physical optical focal length:
335
353
 
336
354
  ```ts
337
355
  import { calculateEquivalentFocalLength35Mm } from "@photivra/engine";
@@ -349,17 +367,18 @@ console.log(equivalent.value.equivalentFocalLength35Mm);
349
367
  console.log(equivalent.value.basis); // "diagonal"
350
368
  ```
351
369
 
352
- The actual focal length remains the physical optical quantity used by projection and depth-of-field calculations. The equivalent value is derived from the active physical capture diagonal relative to a 36 × 24 mm reference frame.
353
-
354
- If an active sensor crop is in use, pass the physical active area resolved by `resolveCaptureGeometry()`. Physical camera orientation does not change the equivalent value because the active diagonal is unchanged.
370
+ The actual focal length stays the physical optical quantity used by projection and depth-of-field calculations. The equivalent value comes from the diagonal of the active physical capture area, compared with a 36 × 24 mm reference frame.
355
371
 
356
- Focus distance is not part of conventional 35 mm-equivalent focal length. Later digital/output crop and output resolution are also excluded. If a product wants to describe final digital framing, it should label that as output framing rather than silently replacing the optical focal length or capture-equivalent focal length.
372
+ - If an active sensor crop is in use, pass the physical active area resolved by `resolveCaptureGeometry()`.
373
+ - Physical camera orientation does not change the equivalent value, because the active diagonal does not change.
374
+ - Focus distance is not part of conventional 35 mm-equivalent focal length. A later digital/output crop and the output resolution are also excluded.
375
+ - If a product wants to describe the final digital framing, it should label it as output framing. It should not silently replace the optical focal length or the capture-equivalent focal length.
357
376
 
358
- `calculateImagingAreaMetrics()` exposes the same canonical diagonal crop-factor calculation independently of raster density so other engine modules do not need to duplicate that formula.
377
+ `calculateImagingAreaMetrics()` provides the same canonical diagonal crop-factor calculation, independent of raster density, so other engine modules do not need to repeat that formula.
359
378
 
360
379
  ## Focus-breathing projection
361
380
 
362
- Use `calculateFocusBreathingProjection()` when a caller has an explicit projection-scale value for the selected focus state.
381
+ Focus breathing is the change in framing that some lenses show when focus changes. Use `calculateFocusBreathingProjection()` when you have an explicit projection-scale value for the selected focus state.
363
382
 
364
383
  ```ts
365
384
  import {
@@ -391,18 +410,19 @@ console.log(horizontalFov.value.effectiveDegrees);
391
410
  - values above `1` narrow framing / increase magnification;
392
411
  - values below `1` widen framing / reduce magnification.
393
412
 
394
- Photivra does **not** infer this scale from focal length, focus distance, lens identity, sensor format, or marketing specifications. Supplying the same scale at two focus distances means exactly that: the caller explicitly declared the same relative projection scale at both states.
413
+ Photivra does **not** infer this scale from focal length, focus distance, lens identity, sensor format or marketing specifications. If you supply the same scale at two focus distances, that is exactly what you get: the same relative projection scale at both states.
395
414
 
396
- The physical focal length remains authoritative. The effective projection distance is not a renamed focal length and later digital/output crop does not redefine the breathing scale.
415
+ The physical focal length stays authoritative. The effective projection distance is not a renamed focal length, and a later digital/output crop does not change the breathing scale.
397
416
 
398
- This generic model is returned as an `approximation`. A future calibrated lens profile would need defensible provenance, reuse rights, and uncertainty/limitation metadata.
417
+ This generic model is returned as an `approximation`. A future calibrated lens profile would need defensible provenance, reuse rights, and uncertainty and limitation metadata.
399
418
 
400
419
  ## Oriented raster to image-plane coordinates
401
420
 
402
- Use the coordinate bridge before calling image-plane lens or camera-mapping APIs
403
- from an oriented/output renderer. The capture/output physical region uses the
404
- raster-style +Y-down basis, while lens-field APIs use the pre-orientation
405
- optical image plane with +Y up.
421
+ Use this coordinate bridge before a renderer working in oriented/output coordinates calls
422
+ an image-plane lens or camera-mapping API. The two sides use different conventions:
423
+
424
+ - the capture/output physical region uses a raster-style basis, +Y down;
425
+ - lens-field APIs use the optical image plane before orientation, +Y up.
406
426
 
407
427
  ```ts
408
428
  import {
@@ -425,14 +445,17 @@ const roundTrip = mapImagePlanePointToOrientedPhysicalUv({
425
445
  });
426
446
  ```
427
447
 
428
- These functions only transform coordinate bases. They do not apply projection,
429
- distortion, focus breathing, crop, or resampling. Asymmetric/off-axis physical
430
- bounds are preserved rather than recentered, and reverse mapping fails closed
448
+ These functions only change the coordinate basis. They do not apply projection,
449
+ distortion, focus breathing, crop or resampling. Asymmetric or off-axis physical
450
+ bounds are kept as they are, not recentered. The reverse mapping fails closed
431
451
  when the image-plane point lies outside the supplied physical region.
432
452
 
433
453
  ## Radial lens-distortion mapping
434
454
 
435
- Use `calculateRadialDistortionMapping()` for a generic rotationally symmetric ideal-to-distorted image-plane mapping and `calculateInverseRadialDistortionMapping()` for the destination-to-source mapping a renderer needs for inverse sampling.
455
+ Two functions cover generic, rotationally symmetric distortion:
456
+
457
+ - `calculateRadialDistortionMapping()` maps an ideal image-plane point to its distorted position.
458
+ - `calculateInverseRadialDistortionMapping()` goes the other way, from destination to source. This is what a renderer needs for inverse sampling.
436
459
 
437
460
  ```ts
438
461
  import {
@@ -472,17 +495,22 @@ scale = 1 + k1 r² + k2 r⁴ + k3 r⁶
472
495
  p_distorted = p_ideal × scale
473
496
  ```
474
497
 
475
- The coefficients are dimensionless **only together with the declared physical normalization radius**. Reusing coefficients with a different normalization changes the model.
498
+ The coefficients are dimensionless **only together with the declared physical normalization radius**. Reusing the coefficients with a different normalization radius gives a different model.
476
499
 
477
- `maximumNormalizedRadius` is the caller-declared valid operating envelope. Photivra analytically checks that radial distance remains strictly monotonic over that interval, including near-linear higher-order coefficient cases; profiles that fold over are rejected so inverse mapping is unique. Derived normalized radii must remain finite, and malformed runtime point/batch inputs fail closed rather than entering the inverse solver.
500
+ `maximumNormalizedRadius` is the valid operating range you declare.
478
501
 
479
- This first field-mapping slice is radial-only and centered on the optical axis. It does not model tangential/decentered distortion, anamorphic mapping, wavelength dependence, or a calibrated named lens.
502
+ - Photivra checks analytically that the radial distance stays strictly monotonic over that range, including cases where higher-order coefficients make it nearly linear.
503
+ - Profiles that fold back on themselves are rejected, so the inverse mapping is unique.
504
+ - Derived normalized radii must stay finite.
505
+ - Malformed point or batch inputs fail closed before they reach the inverse solver.
480
506
 
481
- The engine returns an `approximation` because the polynomial is a generic caller-parameterized lens model. Test Fixture grid/fiducials can provide renderer regression evidence, but they do not calibrate real-lens coefficients.
507
+ This first field mapping is radial only and centered on the optical axis. It does not model tangential/decentered distortion, anamorphic mapping, wavelength dependence or a calibrated named lens.
508
+
509
+ The engine returns an `approximation`, because the polynomial is a generic lens model with caller-chosen parameters. A test-fixture grid or fiducials can provide regression evidence for a renderer, but they do not calibrate real-lens coefficients.
482
510
 
483
511
  ## Lateral chromatic-aberration mapping
484
512
 
485
- Use `calculateLateralChromaticAberrationMapping()` to map one ideal image-plane point through a shared **green-reference base distortion** plus red/blue radial coefficient offsets.
513
+ Lateral chromatic aberration (CA) is the sideways colour fringing caused by red, green and blue light landing at slightly different image positions. Use `calculateLateralChromaticAberrationMapping()` to map one ideal image-plane point through a shared **base distortion, referenced to green**, plus radial coefficient offsets for red and blue.
486
514
 
487
515
  Use `calculateInverseLateralChromaticAberrationMapping()` when a renderer needs the ideal source coordinate for each output channel at one distorted destination.
488
516
 
@@ -530,19 +558,19 @@ console.log(inverse.value.channels.blue.sourceImagePointMm);
530
558
 
531
559
  This is **field mapping, not a blur kernel**.
532
560
 
533
- The green channel carries the common base geometric distortion. Red and blue offsets are added to that base before mapping. Therefore:
561
+ The green channel carries the common base geometric distortion. Red and blue offsets are added to that base before mapping. So:
534
562
 
535
- - zero red/blue offsets mean no lateral CA, while base distortion can still exist;
536
- - callers should not apply the same base distortion as a second renderer warp;
537
- - all three combined channel profiles share one physical normalization radius and operating envelope;
538
- - every combined channel profile must remain individually invertible over that envelope.
563
+ - zero red/blue offsets mean no lateral CA, though base distortion can still exist;
564
+ - do not apply the same base distortion again as a second renderer warp;
565
+ - all three combined channel profiles share one physical normalization radius and operating range;
566
+ - each combined channel profile must be invertible on its own over that range.
539
567
 
540
- The channel labels are representative renderer RGB channels. They are not wavelength samples, a spectral lens model, sensor CFA primaries, or a camera colorimetric profile. Longitudinal chromatic aberration and wavelength-dependent PSF behavior remain separate future work.
568
+ The channel labels stand for renderer RGB channels. They are not wavelength samples, a spectral lens model, sensor CFA primaries or a camera colour profile. Longitudinal chromatic aberration and wavelength-dependent PSF behaviour are separate future work.
541
569
 
542
- A renderer should inverse-map each destination channel to the source coordinate returned by the engine instead of adding a backend-specific RGB offset or finished-image fringe blur.
570
+ A renderer should inverse-map each destination channel to the source coordinate the engine returns. It should not add its own RGB offset or blur fringes onto the finished image.
543
571
 
544
- For renderer grids or other multi-point evaluation, use the batch inverse APIs so
545
- one profile validation/provenance boundary serves the full point set:
572
+ For renderer grids or other multi-point evaluation, use the batch inverse APIs. The profile is
573
+ then validated once, with one provenance record, for the whole point set:
546
574
 
547
575
  Composition fragment: requires previously validated bindings `caProfile`, `points`, `radialProfile`. This is an integration sketch, not a standalone runnable program.
548
576
 
@@ -563,13 +591,13 @@ const caGrid = calculateInverseLateralChromaticAberrationMappings({
563
591
  });
564
592
  ```
565
593
 
566
- Batch per-point values are defined to match the corresponding scalar inverse
567
- APIs. Batching changes repeated validation/allocation structure, not the lens
568
- model, operating envelope, coordinate basis, or numerical solver.
594
+ Each value in a batch matches what the single-point inverse API returns. Batching only
595
+ avoids repeated validation and allocation. It does not change the lens model, operating
596
+ range, coordinate basis or numerical solver.
569
597
 
570
598
  ## Illumination vignetting
571
599
 
572
- Use `calculateIlluminationVignetting()` when a generic virtual lens needs field-dependent **relative linear throughput** without changing geometry or pupil/PSF shape.
600
+ Vignetting is the darkening towards the edges of the frame. Use `calculateIlluminationVignetting()` when a generic virtual lens needs a **relative linear throughput** that varies across the field, without changing geometry or the pupil/PSF shape.
573
601
 
574
602
  ```ts
575
603
  import { calculateIlluminationVignetting } from "@photivra/engine";
@@ -598,9 +626,9 @@ rho = image-plane radius / normalizationRadiusMm
598
626
  T(rho) = 1 + r2*rho² + r4*rho⁴ + r6*rho⁶
599
627
  ```
600
628
 
601
- `T` is a multiplicative **scene-linear/channel-linear** throughput factor. Apply it before display transfer/gamma encoding.
629
+ `T` is a throughput factor that multiplies **linear** scene light, per channel. Apply it before display transfer or gamma encoding.
602
630
 
603
- The profile declares `maximumNormalizedRadius`. Photivra analytically checks the complete interval, including internal extrema, and rejects profiles that:
631
+ The profile declares `maximumNormalizedRadius`. Photivra checks the whole range analytically, including interior minima and maxima, and rejects profiles that:
604
632
 
605
633
  - reach zero or negative throughput; or
606
634
  - amplify above the optical-axis normalization of `1`.
@@ -620,11 +648,11 @@ This model changes throughput only. It does not change:
620
648
  - bokeh;
621
649
  - channel geometry.
622
650
 
623
- Mechanical/pupil vignetting and cat's-eye bokeh belong to the later pupil/PSF foundation. This generic profile is also not calibrated radiometry or a named-lens measurement.
651
+ Mechanical/pupil vignetting and cat's-eye bokeh belong to the later pupil/PSF foundation. This generic profile is not calibrated radiometry, and not a measurement of a named lens.
624
652
 
625
653
  ## Production image-formation plan
626
654
 
627
- The production composition API is separate from `simulatePocCamera()`.
655
+ The production composition API is separate from `simulatePocCamera()`. It works in three steps: prepare static context, commit one capture, then create the plan.
628
656
 
629
657
  Prepare static context once:
630
658
 
@@ -747,7 +775,7 @@ const capture =
747
775
 
748
776
  ### Capture directly from a resolved release frame
749
777
 
750
- When #105 has already resolved a logical frame, prefer the dedicated handoff instead of copying exposure/seed values manually:
778
+ When [release sequencing](#logical-release-sequences) has already resolved a logical frame, use this dedicated handoff instead of copying exposure and seed values by hand:
751
779
 
752
780
  Composition fragment: requires previously validated bindings `lockedWhiteBalance`, `physicalSceneSample`, `sequence`. This is an integration sketch, not a standalone runnable program.
753
781
 
@@ -768,7 +796,7 @@ const capture =
768
796
  });
769
797
  ```
770
798
 
771
- The helper binds the release frame's exposure, focus, automation policy, timing, release identity and stochastic seed. If the frame names a WB state, the complete committed #108 state must match that ID. Both are frozen into the capture fingerprint and later surfaced in `plan.captureIdentity`.
799
+ The helper binds the release frame's exposure, focus, automation policy, timing, release identity and random seed. If the frame names a WB state, the complete committed [white-balance state](#white-balance-and-auto-wb) must match that ID. Both are frozen into the capture fingerprint and later shown in `plan.captureIdentity`.
772
800
 
773
801
  Finally create the semantic plan:
774
802
 
@@ -802,7 +830,7 @@ scene-ray-projection
802
830
  -> lens-field-pupil-evaluation
803
831
  ```
804
832
 
805
- The order comes from `getImageFormationContract()`, not a second planner-specific stage list.
833
+ The order comes from `getImageFormationContract()`. The planner keeps no second stage list of its own.
806
834
 
807
835
  ### Explicit stage/effect state
808
836
 
@@ -814,22 +842,22 @@ Every stage/effect is visible as one of:
814
842
  - `unsupported`;
815
843
  - `blocked`.
816
844
 
817
- For example, unity illumination throughput can remain an explicitly modeled zero/no-loss vignetting effect rather than disappearing from provenance.
845
+ For example, a unity illumination throughput stays visible as an explicitly modelled zero-loss vignetting effect, instead of disappearing from provenance.
818
846
 
819
847
  ### Structured blockers
820
848
 
821
- Scientifically incomplete but structurally valid requests return a blocked plan.
849
+ A request that is structurally valid but scientifically incomplete returns a blocked plan.
822
850
 
823
851
  Examples:
824
852
 
825
- - required stage not yet composed;
826
- - renderer lacks required stage/effect capability;
827
- - renderer lacks wavelength/depth/sensor-domain fidelity;
828
- - missing #110 optical profile;
829
- - missing physical scene sample;
830
- - #110 wavelength/applicability failure.
853
+ - a required stage is not composed yet;
854
+ - the renderer lacks a required stage or effect capability;
855
+ - the renderer lacks wavelength, depth or sensor-domain fidelity;
856
+ - the optical profile for the [scene-radiance-to-sensor bridge](#scene-radiance-to-sensor-irradiance) is missing;
857
+ - the physical scene sample is missing;
858
+ - that bridge fails a wavelength or applicability check.
831
859
 
832
- Malformed config, invalid selected camera settings, identity drift and tampered fingerprints still fail fast.
860
+ Malformed configuration, invalid selected camera settings, identity drift and tampered fingerprints still fail fast (throw).
833
861
 
834
862
  ### Deterministic identity
835
863
 
@@ -841,7 +869,7 @@ The algorithm is explicitly:
841
869
  fnv1a-32-non-cryptographic
842
870
  ```
843
871
 
844
- This is a reproducibility key, not a security/integrity checksum.
872
+ This is a reproducibility key. It is not a security or integrity checksum.
845
873
 
846
874
  Use:
847
875
 
@@ -855,7 +883,7 @@ when a canonical deterministic serialization is needed.
855
883
 
856
884
  ### Temporal capture input
857
885
 
858
- A production capture snapshot may include authoritative timing/readout state:
886
+ A production capture snapshot may include authoritative timing and readout state:
859
887
 
860
888
  ```ts
861
889
  const capture =
@@ -906,11 +934,11 @@ The plan may then expose:
906
934
  - optional `sensorReadoutTiming`;
907
935
  - optional `rotationQuadrature`.
908
936
 
909
- Sensor readout is not silently synchronized to exposure boundaries.
937
+ Sensor readout is never silently synchronized to exposure boundaries.
910
938
 
911
- If a requested temporal quadrature has more nodes than the renderer declares it can consume, the plan reports `renderer-temporal-sampling-insufficient` without changing the committed quadrature.
939
+ If the requested temporal quadrature (time sampling) has more nodes than the renderer says it can handle, the plan reports `renderer-temporal-sampling-insufficient`. It does not change the committed quadrature.
912
940
 
913
- A zero rotation model and global readout remain explicitly `modeled-zero` effects.
941
+ A zero rotation model and global readout stay visible as explicit `modeled-zero` effects.
914
942
 
915
943
  ### Consumer manifests
916
944
 
@@ -931,21 +959,21 @@ const reference =
931
959
  });
932
960
  ```
933
961
 
934
- Both views preserve the same semantic plan fingerprint, stage/effect identities, physical/temporal results, blockers and capture seed.
962
+ Both views keep the same plan fingerprint, stage and effect identities, physical and temporal results, blockers and capture seed.
935
963
 
936
- The consumer role does not permit the backend to alter scientific inputs or reduce committed temporal samples.
964
+ The consumer role does not let the backend change scientific inputs or reduce the committed temporal samples.
937
965
 
938
966
  ### Renderer/reference split
939
967
 
940
968
  Renderer declarations contain semantic capabilities only. They do not include WebGPU, Three.js, React, DOM or server objects.
941
969
 
942
- An optimized interactive renderer and a slower reference evaluator consume the same engine-owned stage/effect plan structure and declare what they can faithfully represent.
970
+ A fast interactive renderer and a slower reference evaluator read the same engine-owned stage/effect plan, and each declares what it can faithfully represent.
943
971
 
944
972
  ### Current first-slice boundary
945
973
 
946
- Plan schema `0.1.0` deliberately composes only the initial primary spectral path through #110.
974
+ Plan schema `0.1.0` deliberately composes only the first primary spectral path, through the [scene-radiance-to-sensor bridge](#scene-radiance-to-sensor-irradiance).
947
975
 
948
- Later requested stages remain explicit blockers until intentionally integrated, including:
976
+ Later stages stay explicit blockers until they are deliberately integrated, including:
949
977
 
950
978
  - PSF;
951
979
  - temporal exposure/readout composition;
@@ -956,15 +984,15 @@ Later requested stages remain explicit blockers until intentionally integrated,
956
984
  - reconstruction;
957
985
  - final output/display processing.
958
986
 
959
- The production plan therefore does not turn standalone foundations into silently active pipeline stages.
987
+ So the production plan never silently turns standalone foundations into active pipeline stages.
960
988
 
961
- See [Production Image-Formation Plan](PRODUCTION_COMPOSITION.md).
989
+ See [Production Image-Formation Plan](https://github.com/Photivra/photivra/blob/main/docs/PRODUCTION_COMPOSITION.md).
962
990
 
963
991
  ## Front-of-lens filter transmission
964
992
 
965
993
  Use `parseFrontOfLensFilterProfile()`, `resolveFrontOfLensFilterTransmission()`, and `composeFrontOfLensFilterTransmission()` for generic passive filters placed **in front of the lens**.
966
994
 
967
- OpenSource V1 supports three generic transmission declarations:
995
+ These cover neutral-density (ND) and wavelength-dependent filters. OpenSource V1 supports three generic ways to declare transmission:
968
996
 
969
997
  - `neutral-linear-transmission` — explicit wavelength-independent transmission over a declared wavelength range;
970
998
  - `neutral-optical-density` — base-10 optical density with `T = 10^-OD`;
@@ -1018,7 +1046,7 @@ const nd = parseFrontOfLensFilterProfile({
1018
1046
  });
1019
1047
  ```
1020
1048
 
1021
- A 0.5 transmission factor is exactly one stop of attenuation. Base-10 optical density is reported alongside stop attenuation for diagnostics.
1049
+ A transmission factor of 0.5 is exactly one stop of attenuation. Base-10 optical density is reported next to the stop attenuation, for diagnostics.
1022
1050
 
1023
1051
  Multiple filters compose multiplicatively:
1024
1052
 
@@ -1026,19 +1054,19 @@ Multiple filters compose multiplicatively:
1026
1054
  T_stack = T_1 * T_2 * ... * T_n
1027
1055
  ```
1028
1056
 
1029
- The stack preserves each component's identity/evidence. Aggregate numeric uncertainty is deliberately reported as **not propagated** unless correlation/independence semantics are established; individual filter uncertainty remains attached to each component.
1057
+ The stack keeps each filter's identity and evidence. The combined numeric uncertainty is deliberately reported as **not propagated**, unless it is established whether the filters' errors are correlated or independent. Each filter's own uncertainty stays attached to that filter.
1030
1058
 
1031
1059
  ### Spectral boundary
1032
1060
 
1033
- Spectral filter data remains wavelength-resolved through #110. Photivra does not convert a spectral filter into an undocumented RGB tint.
1061
+ Spectral filter data stays wavelength-resolved all the way through the [scene-radiance-to-sensor bridge](#scene-radiance-to-sensor-irradiance). Photivra does not turn a spectral filter into an undocumented RGB tint.
1034
1062
 
1035
- The requested scene wavelength and filter profile must use the same resolved air/vacuum basis and lie inside the declared wavelength range.
1063
+ The requested scene wavelength and the filter profile must use the same resolved air/vacuum basis, and the wavelength must lie inside the declared range.
1036
1064
 
1037
- Embedded numeric spectral curves require `reusable-data` or `photivra-owned` provenance. Factual-reference-only publication does not authorize copying numeric curve data into the open-source package.
1065
+ Embedded numeric spectral curves need `reusable-data` or `photivra-owned` provenance. A curve that was only published as a factual reference may not be copied into the open-source package.
1038
1066
 
1039
1067
  ### Optical integration
1040
1068
 
1041
- When `frontOfLensFilters` is supplied to `calculateSceneRadianceToSensorIrradiance()`, the bridge applies the filter stack **exactly once**, independently from:
1069
+ When you pass `frontOfLensFilters` to `calculateSceneRadianceToSensorIrradiance()`, the bridge applies the filter stack **exactly once**, separately from:
1042
1070
 
1043
1071
  - lens spectral transmission or effective T-stop;
1044
1072
  - field/vignetting throughput;
@@ -1062,19 +1090,21 @@ A selected front filter does **not** change:
1062
1090
  - geometric defocus;
1063
1091
  - diffraction.
1064
1092
 
1065
- The production capture snapshot/plan also carries the filter stack. Changing the selected filter changes immutable capture/plan identity and the composed #110 physical result.
1093
+ The production capture snapshot and plan also carry the filter stack. Changing the selected filter changes the immutable capture/plan identity and the composed physical result of the bridge.
1066
1094
 
1067
1095
  ### Polarization boundary
1068
1096
 
1069
- CPL/polarizer behavior is not modeled here. Correct polarization would require an end-to-end polarization representation across scene radiance, materials/reflections, atmosphere, optics and potentially sensor response.
1097
+ Circular polarizer (CPL) and polarizer behaviour is not modelled in schema 0.1.0. Correct polarization would need polarization to be represented end to end: scene radiance, materials and reflections, atmosphere, optics, and possibly sensor response.
1070
1098
 
1071
- Schema 0.1.0 therefore rejects filter profiles that claim `polarizationModeled: true`.
1099
+ Schema 0.1.0 therefore rejects filter profiles that claim `polarizationModeled: true`. (Filter schema 0.2.0 adds a linear polarizer; see [Polarizer and haze](#polarizer-and-haze).)
1072
1100
 
1073
1101
  ## Scene radiance to sensor irradiance
1074
1102
 
1075
- Use `calculateSceneRadianceToSensorIrradiance()` when you need the first physical primary-optics bridge from one validated #85 outgoing spectral-radiance sample to **pre-sensor-stack sensor-plane spectral irradiance**.
1103
+ `calculateSceneRadianceToSensorIrradiance()` (#110) is the first physical optics bridge. It takes one validated sample of spectral radiance leaving the scene (from a [scene-radiance provider](#scene-radiance-provider-and-material-response), #85) and returns the **spectral irradiance at the sensor plane, before the sensor's own optical stack**.
1104
+
1105
+ (Radiance is light leaving a surface in a given direction; irradiance is light arriving on a surface.)
1076
1106
 
1077
- The bridge operates on one explicit wavelength/time/field sample at a time.
1107
+ The bridge works on one explicit wavelength/time/field sample at a time.
1078
1108
 
1079
1109
  Composition fragment: requires previously validated bindings `sceneRadianceRequest`, `sceneRadianceResult`. This is an integration sketch, not a standalone runnable program.
1080
1110
 
@@ -1196,20 +1226,20 @@ sensor spectral irradiance =
1196
1226
  * field throughput
1197
1227
  ```
1198
1228
 
1199
- where the effective optical acceptance includes exactly one optical-transmission path.
1229
+ where the effective optical acceptance includes exactly one optical-transmission path. (N_working is the working f-number.)
1200
1230
 
1201
1231
  This is a **paraxial approximation**, not a ray-traced pupil solution.
1202
1232
 
1203
1233
  ### f-number, working f-number, transmission, and T-stop
1204
1234
 
1205
- These quantities remain separate.
1235
+ These quantities stay separate:
1206
1236
 
1207
- - nominal f-number describes geometric aperture;
1208
- - working f-number describes the finite-focus geometric aperture relation used by the bridge;
1209
- - spectral transmission is a wavelength-dependent fractional throughput;
1210
- - effective working T-stop is an alternative approximation that already incorporates transmission loss.
1237
+ - the nominal f-number describes the geometric aperture;
1238
+ - the working f-number is the geometric aperture relation at finite focus that the bridge uses;
1239
+ - spectral transmission is the fraction of light passed, per wavelength;
1240
+ - an effective working T-stop is an alternative approximation that already includes the transmission loss.
1211
1241
 
1212
- Do not apply a spectral transmission curve and T-stop simultaneously.
1242
+ Do not apply a spectral transmission curve and a T-stop together.
1213
1243
 
1214
1244
  For a spectral-transmission profile:
1215
1245
 
@@ -1244,9 +1274,9 @@ N_working = N_nominal * (1 + magnification)
1244
1274
 
1245
1275
  but only when unity pupil magnification is explicitly declared.
1246
1276
 
1247
- This is not assumed to be universally correct for real lenses.
1277
+ This is not assumed to be correct for all real lenses.
1248
1278
 
1249
- A generic/real lens profile may instead supply an evidence-backed working f-number:
1279
+ A generic or real lens profile may instead supply an evidence-backed working f-number:
1250
1280
 
1251
1281
  ```ts
1252
1282
  focus: {
@@ -1283,7 +1313,7 @@ Supply either:
1283
1313
 
1284
1314
  That factor is applied **exactly once**.
1285
1315
 
1286
- Mechanical pupil clipping / cat-eye behavior remains a later pupil/PSF concern and must not be stacked as a second hidden illumination loss.
1316
+ Mechanical pupil clipping and cat's-eye behaviour belong to later pupil/PSF work. Do not stack them as a second, hidden illumination loss.
1287
1317
 
1288
1318
  ### Wavelength basis and provenance
1289
1319
 
@@ -1294,7 +1324,7 @@ Scene radiance and optical transmission must use the same resolved wavelength ba
1294
1324
 
1295
1325
  Photivra does not silently convert between them.
1296
1326
 
1297
- Numeric wavelength-transmission curves are reusable data, not merely factual metadata. Their evidence must therefore be `reusable-data` or `photivra-owned`; `factual-reference-only` is insufficient for copied numeric curves.
1327
+ Numeric wavelength-transmission curves are reusable data, not just factual metadata. Their evidence must therefore be `reusable-data` or `photivra-owned`. `factual-reference-only` is not enough for copied numeric curves.
1298
1328
 
1299
1329
  ### Boundary of the result
1300
1330
 
@@ -1326,11 +1356,11 @@ The bridge also does not apply:
1326
1356
  - volumetric scattering;
1327
1357
  - wavelength-changing transport.
1328
1358
 
1329
- Those remain separately owned so transmission or energy loss cannot be counted twice.
1359
+ Each of those has its own owner, so transmission or energy loss cannot be counted twice.
1330
1360
 
1331
1361
  ### Calibration status
1332
1362
 
1333
- Schema 0.1.0 can preserve calibrated optical transmission metadata, but current #85 scene-radiance evaluation is still approximation-only.
1363
+ Schema 0.1.0 can carry calibrated optical transmission metadata. But scene-radiance evaluation (#85) is still approximation-only.
1334
1364
 
1335
1365
  Therefore the combined result currently reports:
1336
1366
 
@@ -1342,7 +1372,7 @@ even when the optical profile itself is calibrated.
1342
1372
 
1343
1373
  ## Thin-lens image distance and magnification
1344
1374
 
1345
- Use `calculateThinLensImageDistance()` to calculate ideal Gaussian thin-lens image distance for an object/focus plane.
1375
+ Use `calculateThinLensImageDistance()` to calculate the ideal Gaussian thin-lens image distance for an object (focus) plane.
1346
1376
 
1347
1377
  ```ts
1348
1378
  import { calculateThinLensImageDistance } from "@photivra/engine";
@@ -1357,13 +1387,13 @@ console.log(projection.value.magnification);
1357
1387
  console.log(projection.value.infinityProjectionScale);
1358
1388
  ```
1359
1389
 
1360
- This is a paraxial ideal-lens model. It does not model real-lens focus breathing, pupil magnification, principal-plane movement, or aberrations.
1390
+ This is a paraxial, ideal-lens model ("paraxial" means it assumes rays close to the optical axis). It does not model real-lens focus breathing, pupil magnification, principal-plane movement or aberrations.
1361
1391
 
1362
- See [Physics Foundation](PHYSICS_FOUNDATION.md#projection-focus-extension-object-size-and-motion).
1392
+ See [Physics Foundation](https://github.com/Photivra/photivra/blob/main/docs/PHYSICS_FOUNDATION.md#projection-focus-extension-object-size-and-motion).
1363
1393
 
1364
1394
  ## Projected object size and sensor sampling
1365
1395
 
1366
- Use `calculateProjectedObjectSize()` for a fronto-parallel object with known physical dimensions and distance.
1396
+ Use `calculateProjectedObjectSize()` for an object facing the camera squarely (fronto-parallel), with known physical dimensions and distance.
1367
1397
 
1368
1398
  ```ts
1369
1399
  import { calculateProjectedObjectSize } from "@photivra/engine";
@@ -1387,11 +1417,11 @@ Omit `pixelPitchMicrometers` when only physical image-plane dimensions are neede
1387
1417
 
1388
1418
  The object plane is assumed to be fronto-parallel to the sensor.
1389
1419
 
1390
- See [Physics Foundation](PHYSICS_FOUNDATION.md#projection-focus-extension-object-size-and-motion).
1420
+ See [Physics Foundation](https://github.com/Photivra/photivra/blob/main/docs/PHYSICS_FOUNDATION.md#projection-focus-extension-object-size-and-motion).
1391
1421
 
1392
1422
  ## Depth of field and defocus
1393
1423
 
1394
- Use `calculateDepthOfField()` for conventional geometric near/far limits:
1424
+ Use `calculateDepthOfField()` for the exact thin-lens near/far limits for a caller-supplied circle of confusion (it must be smaller than the aperture diameter `focalLengthMm / aperture`):
1395
1425
 
1396
1426
  ```ts
1397
1427
  import { calculateDepthOfField } from "@photivra/engine";
@@ -1423,34 +1453,34 @@ const blur = calculateDefocusCircle({
1423
1453
  console.log(blur.value.diameterMm);
1424
1454
  ```
1425
1455
 
1426
- The circle-of-confusion criterion used for depth-of-field is caller supplied. These are ideal geometric thin-lens models and do not incorporate diffraction or real-lens aberrations into the DOF criterion.
1456
+ You supply the circle-of-confusion criterion used for depth of field (DOF). These are ideal geometric thin-lens models. They do not include diffraction or real-lens aberrations in the DOF criterion.
1427
1457
 
1428
- See [Physics Foundation](PHYSICS_FOUNDATION.md#thin-lens-focus-and-depth-of-field).
1458
+ See [Physics Foundation](https://github.com/Photivra/photivra/blob/main/docs/PHYSICS_FOUNDATION.md#thin-lens-focus-and-depth-of-field).
1429
1459
 
1430
1460
  ## Real-lens PSF profiles and complex-pupil reference
1431
1461
 
1432
- #113 adds two explicit real-lens primary-optical response paths while preserving the legacy defocus/Airy diagnostics.
1462
+ There are two explicit ways to describe the response of a real lens: sampled PSF profiles, and a complex-pupil (wavefront) reference. Both were added in #113. The legacy defocus and Airy diagnostics are kept unchanged.
1433
1463
 
1434
1464
  ### Sampled 2D PSF profiles
1435
1465
 
1436
- Use `parseLensSampledPsfProfile()` and `resolveLensSampledPsf()` for measured, simulated, or Photivra-owned unit-energy PSF kernels on a bounded regular grid.
1466
+ Use `parseLensSampledPsfProfile()` and `resolveLensSampledPsf()` for PSF kernels that are measured, simulated or owned by Photivra. Each kernel has unit energy, and kernels sit on a bounded regular grid.
1437
1467
 
1438
1468
  The grid axes are:
1439
1469
 
1440
1470
  - physical focal length;
1441
- - focus in diopters, where `0` represents the explicit #103 infinity focus state;
1471
+ - focus in diopters, where `0` is the explicit infinity state of [`FocusPlane`](#explicit-finite-and-infinity-focus-state);
1442
1472
  - f-number;
1443
1473
  - image-plane field X/Y in millimetres;
1444
1474
  - wavelength in nanometres;
1445
1475
  - signed image-plane defocus in micrometres.
1446
1476
 
1447
- Resolution is either `exact-grid-sample` or `interpolated`. Extrapolation outside any declared axis fails closed.
1477
+ Resolution is either `exact-grid-sample` or `interpolated`. Any request outside a declared axis fails closed; there is no extrapolation.
1448
1478
 
1449
- All nodes must use the same physical kernel sampling grid so multilinear interpolation does not silently resample different PSF coordinate systems. The returned full 2D kernel preserves +X right / +Y up orientation; no radial or sagittal/tangential averaging is forced.
1479
+ All nodes must use the same physical kernel sampling grid, so multilinear interpolation never silently mixes different PSF coordinate systems. The returned full 2D kernel keeps the +X right / +Y up orientation. No radial or sagittal/tangential averaging is forced.
1450
1480
 
1451
- Every kernel is a **unit-energy PSF shape**. `relativePupilThroughputFactor` remains separate and belongs to the optical-throughput path. A mechanically clipped/cat-eye pupil may therefore change both PSF shape and throughput without multiplying throughput into the normalized PSF and then applying it again in #110.
1481
+ Every kernel is a **unit-energy PSF shape**. `relativePupilThroughputFactor` is kept separate and belongs to the optical-throughput path. So a mechanically clipped (cat's-eye) pupil can change both the PSF shape and the throughput, without the throughput being multiplied into the normalized PSF and then applied a second time in the [scene-radiance-to-sensor bridge](#scene-radiance-to-sensor-irradiance).
1452
1482
 
1453
- `bestFocusImagePlaneOffsetMicrometers` may vary across field and wavelength. This is the profile seam for field curvature and longitudinal/axial chromatic focus. It does not alter geometric distortion or perform lateral channel displacement.
1483
+ `bestFocusImagePlaneOffsetMicrometers` may vary across the field and with wavelength. This is where a profile describes field curvature and longitudinal (axial) chromatic focus shift. It does not change geometric distortion or shift channels sideways.
1454
1484
 
1455
1485
  Signed defocus is preserved, so foreground and background defocus may use different profiled PSFs.
1456
1486
 
@@ -1461,32 +1491,37 @@ Use `parseLensComplexPupilProfile()` and `calculateLensComplexPupilPsf()` when t
1461
1491
  - relative pupil amplitude; plus
1462
1492
  - optical-path difference in micrometres.
1463
1493
 
1464
- The reference evaluator performs deterministic scalar Fraunhofer propagation on the explicit pupil grid. Diffraction and aberration phase are evaluated **together** in that pupil calculation. Do not add another independent diffraction blur to the result.
1494
+ The reference evaluator runs deterministic scalar Fraunhofer propagation on the explicit pupil grid.
1495
+
1496
+ - Each axis is zero-padded to at least 2S − 1 samples, where S is the number of samples in the non-zero support. This makes the intensity PSF meet Goodman's sampling condition Q = λN/pitch > 2 (stated freely in Ban et al., [arXiv:2511.06936](https://arxiv.org/abs/2511.06936), eqs. 8–9). `result.value.sampling` reports the achieved Q and padding.
1497
+ - A pupil support wider than the declared f/N aperture is rejected.
1498
+ - Diffraction and aberration phase are evaluated **together** in that pupil calculation. Do not add a separate diffraction blur to the result.
1465
1499
 
1466
- The resulting intensity PSF is normalized to unit energy. The profile's relative pupil throughput is preserved separately and is not multiplied into the kernel.
1500
+ The resulting intensity PSF is normalized to unit energy. The profile's relative pupil throughput is kept separately and is not multiplied into the kernel.
1467
1501
 
1468
- The complex-pupil evaluator is a bounded scalar reference model. It does not include polarization/vector diffraction, ghosting/flare, sensor OLPF/microlenses/CFA, sampling, demosaic, sharpening, or display processing.
1502
+ The complex-pupil evaluator is a bounded, scalar reference model. It does not include polarization or vector diffraction, ghosting or flare, the sensor's OLPF (optical low-pass filter), microlenses or CFA, sampling, demosaicing, sharpening or display processing.
1469
1503
 
1470
1504
  ### MTF magnitude is not a PSF
1471
1505
 
1472
- `parseLensMtfDiagnosticProfile()` stores sagittal/tangential MTF magnitude diagnostics.
1506
+ The MTF (modulation transfer function) describes how much contrast a lens keeps at each level of detail. `parseLensMtfDiagnosticProfile()` stores sagittal/tangential MTF magnitude diagnostics.
1473
1507
 
1474
- `assessMtfOnlyPsfRenderability()` always reports that unique PSF reconstruction is unauthorized because MTF magnitude lacks phase information. MTF remains useful for validation, not as a magic blur-kernel generator.
1508
+ `assessMtfOnlyPsfRenderability()` always reports that a unique PSF cannot be reconstructed, because the MTF magnitude has no phase information. MTF is useful for validation, not as a way to generate a blur kernel.
1475
1509
 
1476
1510
  ### Ownership boundaries
1477
1511
 
1478
- - geometric distortion remains a field mapping;
1479
- - lateral CA remains a wavelength/channel field mapping;
1480
- - field curvature and longitudinal CA live in focus/PSF semantics;
1481
- - pupil clipping can affect PSF shape while throughput remains separately owned;
1482
- - #1 owns regular-polygon/non-circular diffraction generation;
1483
- - #114 owns stray-light/ghosting/veiling glare;
1484
- - sensor OLPF/CFA/reconstruction remain downstream;
1485
- - no scalar `lensSharpness` or `bokehQuality` is a scientific primitive.
1512
+ - Geometric distortion stays a field mapping.
1513
+ - Lateral CA stays a wavelength/channel field mapping.
1514
+ - Field curvature and longitudinal CA belong to focus/PSF.
1515
+ - Pupil clipping can change PSF shape; throughput is owned separately.
1516
+ - Regular-polygon and other non-circular diffraction is owned by [ideal polygon diffraction](#ideal-regular-polygon-diffraction) (#1).
1517
+ - Stray light, ghosting and veiling glare are owned separately (#114; see [Digital optics foundations](https://github.com/Photivra/photivra/blob/main/docs/DIGITAL_OPTICS_FOUNDATION.md)).
1518
+ - Sensor OLPF, CFA and reconstruction come later in the pipeline.
1519
+ - There is no scalar `lensSharpness` or `bokehQuality` primitive; such scores are not scientific quantities.
1486
1520
 
1487
1521
  ## PSF and pupil foundation
1488
1522
 
1489
- Use `getPsfFoundationContract()` to inspect current/reserved PSF contribution ownership and `calculatePsfFoundationComponents()` to evaluate the currently implemented diagnostics in one explicit field/depth/spectral/pupil context.
1523
+ - `getPsfFoundationContract()` shows which PSF contributions exist or are reserved, and who owns them.
1524
+ - `calculatePsfFoundationComponents()` evaluates the diagnostics implemented today, in one explicit field/depth/spectral/pupil context.
1490
1525
 
1491
1526
  ```ts
1492
1527
  import {
@@ -1515,17 +1550,17 @@ console.log(components.value.contributions.circularDiffraction);
1515
1550
  console.log(components.value.composition.status); // "not-composed"
1516
1551
  ```
1517
1552
 
1518
- The current foundation intentionally **does not combine** geometric defocus and circular diffraction into a synthetic PSF, MTF, convolution kernel, combined blur radius, or “lens sharpness” score.
1553
+ The foundation deliberately **does not combine** geometric defocus and circular diffraction into a synthetic PSF, MTF, convolution kernel, combined blur radius or "lens sharpness" score.
1519
1554
 
1520
- It records field position now even though the current defocus/Airy diagnostics are field invariant. Future field-curvature, pupil-clipping, bokeh, and aberration contributions can therefore consume the same context without reinterpreting existing outputs.
1555
+ It already records field position, even though the current defocus and Airy diagnostics do not vary across the field. Future field-curvature, pupil-clipping, bokeh and aberration contributions can then use the same context without reinterpreting existing outputs.
1521
1556
 
1522
- The current spectral basis is explicitly monochromatic for the Airy diagnostic. This does not create a spectral lens or sensor-color model.
1557
+ The spectral basis is explicitly monochromatic (one wavelength) for the Airy diagnostic. This does not create a spectral lens or sensor-colour model.
1523
1558
 
1524
- See [PSF and Pupil Foundation](PSF_FOUNDATION.md).
1559
+ See [PSF and Pupil Foundation](https://github.com/Photivra/photivra/blob/main/docs/PSF_FOUNDATION.md).
1525
1560
 
1526
1561
  ## Circular-aperture diffraction
1527
1562
 
1528
- Use `calculateAiryDisk()` for the ideal monochromatic circular-pupil first-zero Airy diameter:
1563
+ Use `calculateAiryDisk()` for the diameter of the Airy disk to its first dark ring (first zero), for an ideal circular pupil at one wavelength:
1529
1564
 
1530
1565
  ```ts
1531
1566
  import { calculateAiryDisk } from "@photivra/engine";
@@ -1538,9 +1573,9 @@ const diffraction = calculateAiryDisk({
1538
1573
  console.log(diffraction.value.firstZeroDiameterMicrometers);
1539
1574
  ```
1540
1575
 
1541
- This calculation uses `d = 2.44 λ N`. It is not a polygon-aperture diffraction PSF.
1576
+ This calculation uses `d = 2.44 λ N` (λ = wavelength, N = f-number). It is not a diffraction PSF for a polygon aperture.
1542
1577
 
1543
- See [Physics Foundation](PHYSICS_FOUNDATION.md#diffraction).
1578
+ See [Physics Foundation](https://github.com/Photivra/photivra/blob/main/docs/PHYSICS_FOUNDATION.md#diffraction).
1544
1579
 
1545
1580
  ## Aperture geometry and sunstar directions
1546
1581
 
@@ -1559,9 +1594,9 @@ console.log(aperture.value.sunstarRayCount);
1559
1594
  console.log(aperture.value.sunstarRayAnglesDegrees);
1560
1595
  ```
1561
1596
 
1562
- The output describes normalized aperture geometry and idealized ray-direction symmetry only. It does not calculate diffraction intensity, star length, flare, coatings, or a full PSF.
1597
+ The output describes only the normalized aperture shape and the ideal symmetry of the sunstar ray directions. It does not calculate diffraction intensity, star length, flare, coatings or a full PSF.
1563
1598
 
1564
- See [Physics Foundation](PHYSICS_FOUNDATION.md#ideal-diaphragm-geometry-and-sunstar-symmetry).
1599
+ See [Physics Foundation](https://github.com/Photivra/photivra/blob/main/docs/PHYSICS_FOUNDATION.md#ideal-diaphragm-geometry-and-sunstar-symmetry).
1565
1600
 
1566
1601
  ## Pixel pitch
1567
1602
 
@@ -1579,25 +1614,30 @@ console.log(pitch.value.micrometers);
1579
1614
  console.log(pitch.value.millimeters);
1580
1615
  ```
1581
1616
 
1582
- The current primitive calculates horizontal pixel pitch. It assumes the supplied sensor width and pixel count describe the same active dimension.
1617
+ This primitive calculates horizontal pixel pitch. It assumes the sensor width and pixel count you supply describe the same active dimension.
1583
1618
 
1584
- See [Physics Foundation](PHYSICS_FOUNDATION.md#sensor-geometry-sampling-capture-and-crop).
1619
+ See [Physics Foundation](https://github.com/Photivra/photivra/blob/main/docs/PHYSICS_FOUNDATION.md#sensor-geometry-sampling-capture-and-crop).
1585
1620
 
1586
1621
  ## White balance and Auto WB
1587
1622
 
1588
- Photivra keeps **scene illumination**, **camera white-balance state**, and **render/export color transforms** separate.
1623
+ Photivra keeps three things separate: **scene illumination**, **camera white-balance state**, and **render/export colour transforms**.
1589
1624
 
1590
- The #108 white-balance foundation operates on an explicit pre-WB camera-observable domain:
1625
+ The white-balance foundation (#108) works on an explicit domain: the linear camera RGB a camera could observe before white balance:
1591
1626
 
1592
1627
  ```text
1593
1628
  relative-pre-wb-camera-linear-rgb
1594
1629
  ```
1595
1630
 
1596
- It does not inspect authoritative scene-illuminant metadata as an AWB oracle, modify #85 scene lighting, destructively alter RAW-like sensor capture, or change aperture/shutter/ISO/focus.
1631
+ It does not:
1632
+
1633
+ - peek at the authoritative scene-illuminant metadata to "cheat" auto white balance (AWB);
1634
+ - change the scene lighting;
1635
+ - destructively alter the RAW-like sensor capture;
1636
+ - change aperture, shutter, ISO or focus.
1597
1637
 
1598
1638
  ### Generic presets
1599
1639
 
1600
- Preset labels are resolved through a declared profile rather than universal hard-coded Kelvin aliases:
1640
+ Preset labels are resolved through a declared profile, not through universal hard-coded Kelvin values:
1601
1641
 
1602
1642
  Composition fragment: requires previously validated bindings `evidence`. This is an integration sketch, not a standalone runnable program.
1603
1643
 
@@ -1665,7 +1705,7 @@ const daylight =
1665
1705
  });
1666
1706
  ```
1667
1707
 
1668
- The label, nominal CCT, tint, and actual channel gains are separate profile facts. A camera-specific profile requires defensible evidence; no manufacturer-specific WB behavior is inferred from a common preset name.
1708
+ The label, the nominal CCT (correlated colour temperature), the tint and the actual channel gains are separate facts in the profile. A camera-specific profile needs defensible evidence. No manufacturer-specific WB behaviour is guessed from a common preset name.
1669
1709
 
1670
1710
  ### Manual gains and CCT+tint intent
1671
1711
 
@@ -1682,13 +1722,22 @@ const intent =
1682
1722
  });
1683
1723
  ```
1684
1724
 
1685
- The result keeps `channelGainsResolved: false` and `requiresCameraProfileColorimetry: true`. CCT alone is not treated as a complete chromatic adaptation model, and tint remains an independent axis.
1725
+ The intent keeps `channelGainsResolved: false` and `requiresCameraProfileColorimetry: true`. CCT alone is not treated as a complete chromatic adaptation model, and tint stays an independent axis.
1726
+
1727
+ `resolveColorTemperatureWhiteBalance()` resolves gains once the intent is paired with an explicit camera colour model:
1728
+
1729
+ 1. It takes the white point on the Planckian or CIE-daylight locus.
1730
+ 2. It offsets that white by the tint, read as Duv in CIE 1960 uv.
1731
+ 3. It maps the white through the camera's XYZ→camera matrix.
1732
+ 4. It neutralizes the result.
1733
+
1734
+ The state it returns has `source: "color-temperature"`. See [Kelvin + tint white balance](#kelvin--tint-white-balance) below and [Colorimetry](https://github.com/Photivra/photivra/blob/main/docs/COLORIMETRY.md#kelvin--tint-white-balance).
1686
1735
 
1687
1736
  ### Custom measured WB
1688
1737
 
1689
- `resolveCustomWhiteBalance()` accepts renderer-neutral pre-WB RGB sample results tied to a stable image-state ID.
1738
+ `resolveCustomWhiteBalance()` takes RGB samples measured before white balance, independent of any renderer, and tied to a stable image-state ID.
1690
1739
 
1691
- The selected region is assumed by the caller to be intended neutral; Photivra does **not** claim the chosen real-world object is spectrally neutral.
1740
+ You, the caller, declare that the selected region should be neutral. Photivra does **not** claim that the real-world object you chose is spectrally neutral.
1692
1741
 
1693
1742
  Custom measurement:
1694
1743
 
@@ -1699,19 +1748,19 @@ Custom measurement:
1699
1748
 
1700
1749
  ### Auto White Balance
1701
1750
 
1702
- `estimateAutoWhiteBalance()` consumes only pre-WB camera-linear samples plus a profile-owned AWB policy.
1751
+ `estimateAutoWhiteBalance()` uses only linear camera samples from before white balance, plus an AWB policy owned by the profile.
1703
1752
 
1704
- The generic first model is a deterministic gray-world-style estimate. A neutralizing gain is derived from the weighted observed channels, then the policy's `correctionStrength` is applied in logarithmic gain space:
1753
+ The first generic model is a deterministic grey-world-style estimate (it assumes the scene averages to grey). A neutralizing gain is derived from the weighted observed channels. Then the policy's `correctionStrength` is applied in logarithmic gain space:
1705
1754
 
1706
1755
  ```text
1707
1756
  resolved gain = neutralizing gain ^ correctionStrength
1708
1757
  ```
1709
1758
 
1710
- A strength of `1` represents a neutral-priority generic estimate. Lower strengths preserve more of the observed cast, supporting explicit generic standard/ambience behavior without pretending there is one universal AWB result.
1759
+ A strength of `1` is a generic neutral-priority estimate. Lower strengths keep more of the observed colour cast. This supports explicit generic "standard" or "ambience" behaviour, without pretending there is one universal AWB result.
1711
1760
 
1712
- Clipped AWB samples are excluded and reported; if no usable samples remain, estimation fails closed.
1761
+ Clipped AWB samples are left out and reported. If no usable samples remain, estimation fails closed.
1713
1762
 
1714
- One AWB result is a **global camera WB state**. In mixed illumination, different image regions can retain different local casts after the same global gains.
1763
+ One AWB result is a **global camera WB state**. Under mixed lighting, different parts of the image can keep different local casts after the same global gains.
1715
1764
 
1716
1765
  ### AWB lock
1717
1766
 
@@ -1725,7 +1774,7 @@ const locked =
1725
1774
  });
1726
1775
  ```
1727
1776
 
1728
- Later scene/image signals may produce a different AWB estimate, while the locked state's gains remain unchanged. AWB lock is independent from AE lock and focus lock.
1777
+ Later scene or image signals may give a different AWB estimate, but the locked state's gains stay unchanged. AWB lock is independent of AE lock and focus lock.
1729
1778
 
1730
1779
  ### Image-state/export boundary
1731
1780
 
@@ -1736,7 +1785,100 @@ Every resolved WB state explicitly reports that it did not:
1736
1785
  - modify physical exposure;
1737
1786
  - modify focus.
1738
1787
 
1739
- #15 remains authoritative for how resolved WB intent is applied or encoded in processed linear images, previews, TIFF, DNG metadata, or other output states.
1788
+ How a resolved WB intent is applied or encoded in processed linear images, previews, TIFF, DNG metadata or other outputs is owned by linear capture and colour (#15), not by this foundation.
1789
+
1790
+ ## Colorimetry
1791
+
1792
+ The [colorimetry primitives](https://github.com/Photivra/photivra/blob/main/docs/COLORIMETRY.md) cover colour-matching functions (CMFs), illuminants, CCT/Duv, chromatic adaptation, ΔE (colour difference), camera-matrix fitting, wide-gamut/HDR transfers and gamut mapping.
1793
+
1794
+ No CIE or ColorChecker tables are bundled; the CIE datasets are licensed CC BY-SA 4.0. Use the labelled analytic CMF approximation, or pass tables you are licensed to use.
1795
+
1796
+ ### Spectrum to XYZ, CCT and colour difference
1797
+
1798
+ ```ts
1799
+ import {
1800
+ calculateColorDifference,
1801
+ calculateCielabFromXyz,
1802
+ calculateCorrelatedColorTemperature,
1803
+ createCieIlluminantASpectrum,
1804
+ createColorimetricMidpointQuadrature,
1805
+ integrateSpectrumToXyz
1806
+ } from "@photivra/engine";
1807
+
1808
+ const quadratureNodes = createColorimetricMidpointQuadrature({
1809
+ wavelengthRangeNanometers: { minimum: 360, maximum: 830 },
1810
+ maximumSubintervalWidthNanometers: 1
1811
+ });
1812
+ const colorMatchingFunctions = { kind: "analytic-cie-1931-multi-lobe-fit" } as const;
1813
+
1814
+ const illuminantA = integrateSpectrumToXyz({
1815
+ quadratureNodes,
1816
+ spectralDistribution: createCieIlluminantASpectrum(),
1817
+ colorMatchingFunctions,
1818
+ normalization: { kind: "white-luminance-one" }
1819
+ });
1820
+ console.log(illuminantA.provenance.kind); // "approximation" (analytic CMF fit)
1821
+
1822
+ const cct = calculateCorrelatedColorTemperature({
1823
+ chromaticity: illuminantA.value.chromaticity!,
1824
+ locus: { kind: "planck-integration", colorMatchingFunctions, quadratureNodes }
1825
+ });
1826
+ console.log(cct.value.correlatedColorTemperatureKelvin, cct.value.duv); // ≈ 2855.5 K, ≈ 0
1827
+
1828
+ const white = illuminantA.value.whiteXyz;
1829
+ const reference = calculateCielabFromXyz({ xyz: { x: 0.2, y: 0.18, z: 0.05 }, referenceWhiteXyz: white }).value;
1830
+ const sample = calculateCielabFromXyz({ xyz: { x: 0.21, y: 0.18, z: 0.05 }, referenceWhiteXyz: white }).value;
1831
+ console.log(calculateColorDifference({ reference, sample, formula: "ciede2000" }).value.deltaE);
1832
+ ```
1833
+
1834
+ ### Kelvin + tint white balance
1835
+
1836
+ ```ts
1837
+ import {
1838
+ createColorTemperatureWhiteBalanceIntent,
1839
+ resolveCaptureColorModel,
1840
+ resolveColorTemperatureWhiteBalance
1841
+ } from "@photivra/engine";
1842
+
1843
+ const tungsten = resolveColorTemperatureWhiteBalance({
1844
+ stateId: "wb-3200k",
1845
+ intent: createColorTemperatureWhiteBalanceIntent({ intentId: "k3200", colorTemperatureKelvin: 3200, tint: 0 }),
1846
+ whiteLocus: "planckian",
1847
+ tintInterpretation: "duv-cie-1960-uv",
1848
+ camera: { profileId: "ideal-colorimetric", profileVersion: "0.1.0", xyzToCameraRgb: resolveCaptureColorModel().xyzToCameraRgb }
1849
+ });
1850
+ console.log(tungsten.channelGains); // red < 1 < blue for a warm declared white
1851
+ ```
1852
+
1853
+ ### Chromatic adaptation, transfer functions and gamut mapping
1854
+
1855
+ ```ts
1856
+ import {
1857
+ calculateChromaticAdaptationMatrix,
1858
+ calculateGamutMappedColor,
1859
+ evaluateSrgbEncoding,
1860
+ resolveRgbColorSpace
1861
+ } from "@photivra/engine";
1862
+
1863
+ const bradford = calculateChromaticAdaptationMatrix({
1864
+ transform: "bradford",
1865
+ sourceWhiteXyz: { x: 1.0985, y: 1, z: 0.3558 },
1866
+ destinationWhiteXyz: resolveRgbColorSpace("srgb").value.whiteXyz // D65 from its chromaticity
1867
+ }).value.matrix;
1868
+
1869
+ const rec2020 = resolveRgbColorSpace("rec2020").value.rgbToXyz;
1870
+ const srgbCodeForMidGrey = evaluateSrgbEncoding(0.18); // ≈ 0.4614
1871
+
1872
+ const mapped = calculateGamutMappedColor({
1873
+ colorSpace: "srgb",
1874
+ method: "cielab-hue-preserving-chroma-compression",
1875
+ kneeFraction: 0.9,
1876
+ xyz: { x: rec2020[0][1], y: rec2020[1][1], z: rec2020[2][1] } // Rec.2020 green primary
1877
+ });
1878
+ console.log(bradford, srgbCodeForMidGrey, mapped.value.linearRgb);
1879
+ ```
1880
+
1881
+ Scene adaptation inside `calculateCaptureColorTransform()` can opt in to a cone-space transform with `whiteBalance: { kind: "adopted-white-xyz-scaling", transform: "bradford" }`. Leaving out `transform` keeps the original diagonal scaling in XYZ.
1740
1882
 
1741
1883
  ## Sensor imaging area and native raster
1742
1884
 
@@ -1762,17 +1904,18 @@ console.log(metrics.value.sampling.pitchXMicrometers); // 6
1762
1904
  console.log(metrics.value.sampling.pitchYMicrometers); // 6
1763
1905
  ```
1764
1906
 
1765
- `SensorImagingArea` means the physical photosensitive imaging area used for image formation, not sensor package or die dimensions. `NativeImageRaster` is the effective native image-sampling grid; it does not assert a one-to-one relationship between image samples and physical photodiodes/photosites.
1907
+ - `SensorImagingArea` is the physical light-sensitive area used for image formation. It is not the size of the sensor package or die.
1908
+ - `NativeImageRaster` is the effective native grid of image samples. It does not claim that each image sample is one physical photodiode (photosite).
1909
+ - The 35 mm crop factor is based on the diagonal, compared with a 36 × 24 mm reference frame.
1910
+ - Changing the native raster density does not change the physical crop factor. Changing the physical imaging area does not, by itself, change the native megapixels.
1766
1911
 
1767
- The 35 mm crop factor is diagonal-based relative to a 36 × 24 mm reference frame. Changing native raster density does not change the physical crop factor. Changing physical imaging area does not inherently change native megapixels.
1768
-
1769
- The returned X/Y sampling pitches are geometric image-sample spacing. They are not photodiode active area, fill factor, or a photon-collection model and must not be used as those quantities.
1912
+ The returned X/Y sampling pitches are the geometric spacing of image samples. They are not photodiode active area, fill factor or a photon-collection model, and must not be used as those quantities.
1770
1913
 
1771
1914
  The existing `calculatePixelPitch()` API remains available for callers that only need horizontal pitch from sensor width and horizontal pixel count.
1772
1915
 
1773
1916
  ## Sensor architecture metadata
1774
1917
 
1775
- Use `parseSensorArchitectureProfile()` for descriptive hardware/capability metadata that crosses an untrusted JSON boundary.
1918
+ Use `parseSensorArchitectureProfile()` to read descriptive hardware and capability metadata from untrusted JSON.
1776
1919
 
1777
1920
  The current schema is exported as `SENSOR_ARCHITECTURE_PROFILE_SCHEMA_VERSION` and is `0.3.0`:
1778
1921
 
@@ -1839,12 +1982,12 @@ const architecture = parseSensorArchitectureProfile({
1839
1982
  });
1840
1983
  ```
1841
1984
 
1842
- `technologyFamily` is an optional evidence-backed coarse detector identity:
1985
+ `technologyFamily` is an optional, evidence-backed, coarse detector type:
1843
1986
 
1844
1987
  - `"cmos"`;
1845
1988
  - `"ccd"`.
1846
1989
 
1847
- It is independent from illumination architecture, integration/stacking, readout capability, shutter mechanism, color sampling, and every performance characteristic. For example:
1990
+ It is independent of illumination architecture (FSI/BSI — front- or back-side illuminated), integration/stacking, readout capability, shutter mechanism, colour sampling, and every performance characteristic. For example:
1848
1991
 
1849
1992
  - CMOS may be FSI or BSI;
1850
1993
  - CMOS may expose rolling or global readout;
@@ -1852,27 +1995,31 @@ It is independent from illumination architecture, integration/stacking, readout
1852
1995
  - CCD may be monochrome or color-filtered;
1853
1996
  - the word CMOS or CCD alone says nothing about QE, read noise, dark current, full-well capacity, dynamic range, high-ISO behavior, frame rate, or image quality.
1854
1997
 
1855
- Photivra treats sCMOS as CMOS and EMCCD as CCD at this coarse technology-family level. Those labels are not separate top-level values. Detailed CCD transfer architecture, electron multiplication, CMOS pixel/readout topology, or other technology-specific mechanisms require separate explicit profiles/models if a future scientific consumer needs them.
1998
+ At this coarse level, Photivra treats sCMOS as CMOS and EMCCD as CCD. Those labels are not separate top-level values. Detailed CCD transfer architecture, electron multiplication, CMOS pixel/readout topology and other technology-specific mechanisms would need their own explicit profiles or models, if a future scientific use needs them.
1999
+
2000
+ The other architecture axes are also independent. BSI may be stacked or monolithic. Stacking does not imply a global shutter. Global readout capability does not imply any particular stacking or technology family.
1856
2001
 
1857
- The other architecture axes remain independent. BSI may be stacked or monolithic; stacking does not imply global shutter; and global readout capability does not imply a particular stacking or technology family.
2002
+ Where evidence came from (source origin) and what Photivra may do with it (reuse rights) are independent too:
1858
2003
 
1859
- Source origin and reuse rights are also independent. Manufacturer or third-party material may be factual-reference-only or explicitly reusable when an appropriate license is present. `photivra-owned` evidence must originate from Photivra. Reusable-data evidence requires an explicit license.
2004
+ - Manufacturer or third-party material may be factual-reference-only, or explicitly reusable when a suitable licence exists.
2005
+ - `photivra-owned` evidence must originate from Photivra.
2006
+ - Reusable-data evidence needs an explicit licence.
1860
2007
 
1861
- Scalar facts may cite multiple evidence records. Multi-valued capabilities such as rolling/global readout carry evidence independently for each value so one source does not silently support another capability.
2008
+ Scalar facts may cite several evidence records. Capabilities with several values, such as rolling/global readout, carry evidence for each value separately, so one source cannot silently support a different capability.
1862
2009
 
1863
- `readoutCapabilities` describes hardware capabilities, not the mode selected for one exposure. The capture-mode profile can declare that a selected mode needs mode-specific readout timing, while the actual timing values remain owned by the separate readout-timing contract.
2010
+ `readoutCapabilities` describes what the hardware can do, not the mode selected for one exposure. The capture-mode profile can declare that a selected mode needs its own readout timing; the actual timing values belong to the separate readout-timing contract.
1864
2011
 
1865
2012
  ### Schema 0.2.0 compatibility
1866
2013
 
1867
- Existing schema `0.2.0` profiles remain accepted and preserve their `schemaVersion: "0.2.0"` identity when parsed. They do not acquire or infer a technology family.
2014
+ Existing schema `0.2.0` profiles are still accepted, and keep their `schemaVersion: "0.2.0"` identity when parsed. They do not gain or infer a technology family.
1868
2015
 
1869
- To assert `technologyFamily`, use schema `0.3.0`. A `0.2.0` document that includes the new field fails closed instead of silently applying new semantics under an older schema version.
2016
+ To declare `technologyFamily`, use schema `0.3.0`. A `0.2.0` document that includes the new field fails closed, instead of silently applying new meaning under an older schema version.
1870
2017
 
1871
- Unknown facts should be omitted instead of inferred. Architecture metadata remains descriptive only: CMOS/CCD, BSI/FSI, stacking, readout family, and CFA family do not directly change FOV, crop factor, pixel pitch, exposure, noise, readout timing, or dynamic range. A separate documented downstream physical/calibration model is required before any such effect can be claimed.
2018
+ Leave unknown facts out rather than guessing them. Architecture metadata is descriptive only. CMOS/CCD, BSI/FSI, stacking, readout family and CFA family do not directly change FOV, crop factor, pixel pitch, exposure, noise, readout timing or dynamic range. Any such effect needs a separate, documented physical or calibration model first.
1872
2019
 
1873
2020
  ## Color-sampling topology
1874
2021
 
1875
- Use `parseSensorColorSamplingProfile()` when an exact color-sampling topology is known independently from descriptive sensor-family metadata:
2022
+ The colour-sampling topology says which colour channel each sensor site measures — for example, the 2×2 tile of a Bayer mosaic. Use `parseSensorColorSamplingProfile()` when the exact topology is known, independently of the descriptive sensor-family metadata:
1876
2023
 
1877
2024
  ```ts
1878
2025
  import {
@@ -1921,11 +2068,11 @@ The topology coordinate system is a distinct **native sensor color-sampling-site
1921
2068
  - +X right and +Y down;
1922
2069
  - periodic phase anchored to absolute sensor-site indices.
1923
2070
 
1924
- It is intentionally **not** `NativeImageRaster`. The existing native image raster is an effective image-sampling grid and does not guarantee one sample per physical photodiode or one sample per color-filter site. A later explicit binding must establish how a selected capture mode's effective raster maps to this topology before RAW/CFA sampling can be simulated.
2071
+ It is deliberately **not** `NativeImageRaster`. The native image raster is an effective image-sampling grid. It does not guarantee one sample per physical photodiode, or one sample per colour-filter site. Before RAW/CFA sampling can be simulated, a separate explicit binding must say how a selected capture mode's effective raster maps onto this topology.
1925
2072
 
1926
- That distinction also protects crop phase. If an active crop begins part-way through a repeating tile, callers must preserve the original absolute native site indices. Crop-local `(0,0)` must not silently become a new CFA origin.
2073
+ This separation also protects the crop phase. If an active crop starts part-way through a repeating tile, keep the original absolute native site indices. The crop's local `(0,0)` must not silently become a new CFA origin.
1927
2074
 
1928
- `siteChannelIds` are semantic measurement-channel identifiers only. Names such as `red-like`, `green-a`, or `clear` do not establish:
2075
+ `siteChannelIds` are only names for measurement channels. Names such as `red-like`, `green-a` or `clear` do not establish:
1929
2076
 
1930
2077
  - wavelength response;
1931
2078
  - quantum efficiency;
@@ -1936,13 +2083,13 @@ That distinction also protects crop phase. If an active crop begins part-way thr
1936
2083
 
1937
2084
  Those belong to later spectral/radiometric contracts.
1938
2085
 
1939
- The periodic layout is generic. It can represent a 2×2 Bayer-like phase, larger repeating mosaics, grouped-color tiles, RGBW-like layouts, or other periodic channel arrangements without adding a manufacturer-specific layout enum. Distinct green positions can use the same channel ID or separate IDs when later calibration requires them to remain distinct.
2086
+ The periodic layout is generic. It can represent a 2×2 Bayer-like phase, larger repeating mosaics, grouped-colour tiles, RGBW-like layouts or other periodic channel arrangements, without a manufacturer-specific layout enum. The two green positions can share one channel ID, or use separate IDs when a later calibration needs to keep them apart.
1940
2087
 
1941
- The exact topology is also separate from `SensorArchitectureProfile.colorSamplingFamily`. A family such as `bayer`, `quad-bayer`, `custom-rgb-mosaic`, or `layered-color` is descriptive metadata; Photivra never invents an exact repeating tile from that family label.
2088
+ The exact topology is also separate from `SensorArchitectureProfile.colorSamplingFamily`. A family such as `bayer`, `quad-bayer`, `custom-rgb-mosaic` or `layered-color` is descriptive metadata. Photivra never invents an exact repeating tile from a family label.
1942
2089
 
1943
2090
  Monochrome is represented separately with one semantic measurement-channel ID and no spatial mosaic.
1944
2091
 
1945
- Layered color is deliberately **structural-only** in schema 0.1.0 and uses a separate unresolved spatial-reference marker rather than pretending it shares the periodic site lattice:
2092
+ Layered colour (colours separated by stacked layers rather than a side-by-side mosaic) is deliberately **structural-only** in schema 0.1.0. It uses a separate "spatial relationship not resolved" marker, instead of pretending it shares the periodic site lattice:
1946
2093
 
1947
2094
  ```ts
1948
2095
  const layered = parseSensorColorSamplingProfile({
@@ -1969,15 +2116,15 @@ const layered = parseSensorColorSamplingProfile({
1969
2116
  });
1970
2117
  ```
1971
2118
 
1972
- The engine does not expose per-site layered resolution yet. Real layered designs can use unequal per-layer spatial density or registration, so assuming one coincident equal-resolution RGB triplet at every site would be scientifically unsafe.
2119
+ The engine does not resolve layered sites one by one yet. Real layered designs can have different spatial density or registration per layer. Assuming an equal-resolution RGB triplet at every site would be scientifically unsafe.
1973
2120
 
1974
- Schema 0.1.0 also does not model sparse exceptions to an otherwise periodic layout, such as phase-detect sites, masked sites, sensor defects, or other non-periodic overrides.
2121
+ Schema 0.1.0 also does not model sparse exceptions to an otherwise periodic layout, such as phase-detect sites, masked sites, sensor defects or other non-periodic overrides.
1975
2122
 
1976
- Physical orientation remains downstream. The native topology is not rotated or re-phased merely because the camera is held vertically. Likewise, changing CFA topology does not change physical imaging area, crop factor, FOV, or output geometry.
2123
+ Physical orientation is applied later. The native topology is not rotated or re-phased just because the camera is held vertically. Likewise, changing the CFA topology does not change the physical imaging area, crop factor, FOV or output geometry.
1977
2124
 
1978
2125
  ## Capture-mode/color-sampling binding
1979
2126
 
1980
- Use `parseNativeEffectiveRasterColorSamplingBindingProfile()` when evidence establishes how one exact canonical `NativeImageRaster` relates to the separate native color-sampling-site lattice.
2127
+ Use `parseNativeEffectiveRasterColorSamplingBindingProfile()` when you have evidence for how one exact canonical `NativeImageRaster` relates to the separate lattice of native colour-sampling sites.
1981
2128
 
1982
2129
  The first binding is deliberately regular and sensor-anchored:
1983
2130
 
@@ -2023,9 +2170,9 @@ const resolvedBinding =
2023
2170
  });
2024
2171
  ```
2025
2172
 
2026
- A `1 × 1` relationship is still an **asserted/evidenced binding**. Matching dimensions, matching aspect ratio, output megapixels, or a family label such as Bayer never prove that one `NativeImageRaster` sample corresponds to one color-sampling site.
2173
+ A `1 × 1` relationship is still a **binding that must be asserted and evidenced**. Matching dimensions, matching aspect ratio, output megapixels or a family label such as Bayer never prove that one `NativeImageRaster` sample corresponds to one colour-sampling site.
2027
2174
 
2028
- The binding applies only to the exact native raster dimensions recorded in the profile. The first schema supports one native effective sample mapping to a regular rectangular block of color-sampling sites with a shared native top-left anchor. Irregular site relationships require a later explicit mapping.
2175
+ The binding applies only to the exact native raster dimensions recorded in the profile. The first schema supports one native effective sample mapping to a regular rectangular block of colour-sampling sites, with a shared native top-left anchor. Irregular relationships would need a later, explicit mapping.
2029
2176
 
2030
2177
  To compose that sensor-level relationship with a capture mode, use `resolveCaptureModeColorSamplingContributors()`:
2031
2178
 
@@ -2067,9 +2214,9 @@ console.log(sources.channelSiteCounts);
2067
2214
  console.log(sources.channelComposition);
2068
2215
  ```
2069
2216
 
2070
- `modeSampleIndexFullFrame` is always expressed in the selected mode's **full-frame per-frame effective sampling raster**. The API intentionally does not accept crop-local coordinates. If an active crop starts away from the native origin, a caller must preserve or recover the absolute full-frame mode index before resolving CFA structure. This prevents active crops from silently re-phasing the mosaic.
2217
+ `modeSampleIndexFullFrame` is always given in the selected mode's **full-frame, per-frame effective sampling raster**. The API deliberately does not accept crop-local coordinates. If an active crop starts away from the native origin, keep or recover the absolute full-frame mode index before resolving the CFA structure. This stops active crops from silently re-phasing the mosaic.
2071
2218
 
2072
- For `grouped-native-samples` modes, group width/height alone do not prove group phase. The caller must separately provide evidence that the groups are anchored at the native effective raster top-left. A future contract can add other explicitly evidenced group phases if required.
2219
+ For `grouped-native-samples` modes, the group width and height alone do not prove where the groups start (the group phase). You must separately provide evidence that the groups are anchored at the top-left of the native effective raster. A future contract can add other explicitly evidenced group phases if needed.
2073
2220
 
2074
2221
  The result is a compact **pre-reconstruction structural source region**:
2075
2222
 
@@ -2078,19 +2225,19 @@ The result is a compact **pre-reconstruction structural source region**:
2078
2225
  - `channelSiteCounts` gives exact counts per semantic channel for monochrome/periodic topology;
2079
2226
  - `channelComposition` reports whether the structural region contains one channel or multiple channels.
2080
2227
 
2081
- The bridge does **not** enumerate every contributing site. It also does not establish sum/average weights, spectral response, photon/electron values, or a complete downstream reconstructed-pixel dependency graph.
2228
+ The bridge does **not** list every contributing site. It also does not define sum or average weights, spectral response, photon or electron values, or a complete graph of which reconstructed pixels depend on which sites.
2082
2229
 
2083
- A mixed-channel region therefore remains mixed. Photivra does not collapse a red/green/blue structural region into one synthetic CFA channel merely because a capture mode groups those native effective samples. `combinationDomain` from the capture mode remains separate from any unimplemented weighting/combination equation.
2230
+ So a mixed-channel region stays mixed. Photivra does not collapse a red/green/blue region into one synthetic CFA channel just because a capture mode groups those native samples. The capture mode's `combinationDomain` stays separate from any weighting or combination equation, none of which is implemented.
2084
2231
 
2085
- `declared-effective-raster` modes fail closed even when their dimensions happen to match the native raster. Their relationship was intentionally declared non-simple in #13 and therefore needs a mode-specific color-site mapping rather than dimension inference.
2232
+ `declared-effective-raster` modes fail closed, even when their dimensions happen to match the native raster. Their relationship was deliberately declared as not simple (in the capture-mode work, #13), so they need a mode-specific colour-site mapping, not a guess from dimensions.
2086
2233
 
2087
- Layered-color topology also remains unbound because schema 0.1.0 does not yet define per-layer spatial density/registration.
2234
+ Layered-colour topology also stays unbound, because schema 0.1.0 does not yet define per-layer spatial density and registration.
2088
2235
 
2089
- Pixel-shift/sensor-shift metadata is preserved but does not change CFA assignment. The sensor and its filters move together relative to the optical image; the optical-registration effect of that shift belongs to a later image-formation contract.
2236
+ Pixel-shift (sensor-shift) metadata is kept but does not change CFA assignment. The sensor and its filters move together relative to the optical image. The effect of that shift on optical registration belongs to a later image-formation contract.
2090
2237
 
2091
2238
  ## Sensor optical stack
2092
2239
 
2093
- Use `parseSensorOpticalStackProfile()` for evidence-backed physical stack metadata and a separately declared **effective anti-aliasing spatial response**:
2240
+ The sensor optical stack is the set of layers in front of the photosites: cover glass, infrared-cut filter, anti-aliasing (AA, or optical low-pass) filter and so on. Use `parseSensorOpticalStackProfile()` for evidence-backed physical stack metadata, plus a separately declared **effective anti-aliasing spatial response**:
2094
2241
 
2095
2242
  ```ts
2096
2243
  import {
@@ -2167,16 +2314,16 @@ const aaKernel =
2167
2314
  console.log(aaKernel.components);
2168
2315
  ```
2169
2316
 
2170
- The physical component list and effective AA response are intentionally separate. A stack may contain birefringent/retarder elements while the net intentional low-pass response is cancelled or otherwise effectively absent.
2317
+ The physical component list and the effective AA response are deliberately separate. A stack may contain birefringent or retarder elements while the net intentional low-pass effect is cancelled, or otherwise absent in practice.
2171
2318
 
2172
- `effectiveAntiAliasingSpatialResponse` has three first-stage states:
2319
+ `effectiveAntiAliasingSpatialResponse` can be omitted, or take one of three kinds:
2173
2320
 
2174
2321
  - omitted — **unknown/unasserted**;
2175
2322
  - `kind: "absent"` — evidence says this model should add no intentional AA spatial-splitting term;
2176
2323
  - `kind: "present-unresolved"` — an AA effect exists but its spatial response is not defensibly known;
2177
2324
  - `kind: "normalized-point-splitting-kernel"` — the caller explicitly supplies a normalized spatial point-splitting approximation.
2178
2325
 
2179
- Unknown and absent are not interchangeable. Likewise, `absent` does **not** mean the complete sensor stack has an identity PSF, and it does not mean aliasing or moiré are impossible. Lens PSF, cover/filter behavior, microlenses, sampling topology, and scene detail still matter.
2326
+ Unknown and absent are not interchangeable. And `absent` does **not** mean the whole sensor stack has an identity PSF, nor that aliasing or moiré are impossible. Lens PSF, cover/filter behaviour, microlenses, sampling topology and scene detail still matter.
2180
2327
 
2181
2328
  The point-splitting kernel is deliberately generic:
2182
2329
 
@@ -2186,19 +2333,19 @@ The point-splitting kernel is deliberately generic:
2186
2333
  - normalized weights must sum to one;
2187
2334
  - the weights describe **spatial redistribution only**.
2188
2335
 
2189
- They do not represent total optical throughput, spectral transmission, photon efficiency, QE, or fill factor. Those belong to separate radiometric/sensor-response contracts.
2336
+ They do not represent total optical throughput, spectral transmission, photon efficiency, QE or fill factor. Those belong to separate radiometric and sensor-response contracts.
2190
2337
 
2191
- The first point-splitting scope is explicitly `field-wavelength-polarization-invariant-approximation`. Real OLPF behavior may depend on wavelength, incidence angle/field position, polarization, materials, and stack geometry. Do not upgrade this generic kernel into a calibrated whole-stack PSF.
2338
+ The first point-splitting scope is explicitly `field-wavelength-polarization-invariant-approximation`. A real OLPF (optical low-pass filter) may depend on wavelength, angle of incidence/field position, polarization, materials and stack geometry. Do not treat this generic kernel as a calibrated whole-stack PSF.
2192
2339
 
2193
- `orderedComponents` is incident-light → sensor descriptive metadata. Component roles can include cover glass, IR/UV filtering, AR treatment, birefringent low-pass elements, wave plates, and other optical filters. Schema 0.1.0 does not derive transmission, refractive focus shift, aberration, thickness effects, or a spectral response from those roles.
2340
+ `orderedComponents` is descriptive metadata, listed in order from incoming light to the sensor. Component roles can include cover glass, IR/UV filtering, anti-reflection (AR) treatment, birefringent low-pass elements, wave plates and other optical filters. Schema 0.1.0 does not derive transmission, refractive focus shift, aberration, thickness effects or a spectral response from those roles.
2194
2341
 
2195
- Microlens presence or absence is also descriptive only. A `present` microlens does not by itself specify angular acceptance, spatial concentration, fill factor, crosstalk, QE improvement, or field behavior.
2342
+ Microlens presence or absence is also descriptive only. A `present` microlens does not by itself say anything about angular acceptance, light concentration, fill factor, crosstalk, QE improvement or behaviour across the field.
2196
2343
 
2197
- The resolver therefore returns only the **effective AA spatial kernel** and explicitly reports that cover/filter effects, microlens response, throughput, spectral transmission, field dependence, wavelength dependence, and polarization dependence are not included.
2344
+ The resolver therefore returns only the **effective AA spatial kernel**. It states explicitly that cover/filter effects, microlens response, throughput, spectral transmission, and field, wavelength and polarization dependence are not included.
2198
2345
 
2199
2346
  ## Sensor sampling aperture
2200
2347
 
2201
- Use `parseSensorSamplingApertureProfile()` and `resolveSensorSamplingAperture()` when the physical color-site lattice registration and a geometric photosensitive-region approximation are known:
2348
+ Use `parseSensorSamplingApertureProfile()` and `resolveSensorSamplingAperture()` when you know where the colour-site lattice physically sits, and have a geometric approximation of each site's light-sensitive region:
2202
2349
 
2203
2350
  Composition fragment: requires previously validated bindings `binding`, `topology`. This is an integration sketch, not a standalone runnable program.
2204
2351
 
@@ -2283,9 +2430,9 @@ The site-center lattice is a distinct physical registration contract:
2283
2430
  - the first site's center is explicitly registered relative to the physical imaging area's top-left edge;
2284
2431
  - the full declared lattice must fit inside the supplied physical imaging area.
2285
2432
 
2286
- Photivra does **not** derive this site pitch or origin from `NativeImageRaster`. The native effective raster and the color-site lattice already have a structural binding, but physical placement remains a separate fact.
2433
+ Photivra does **not** derive this site pitch or origin from `NativeImageRaster`. The native effective raster and the colour-site lattice already have a structural binding, but physical placement is a separate fact.
2287
2434
 
2288
- The geometric sensitive aperture is separate from site pitch. A scalar fill fraction is not sufficient to reconstruct shape or centering, so schema 0.1.0 stores explicit rectangle dimensions and center offset.
2435
+ The geometric sensitive aperture is separate from site pitch. A single fill fraction cannot recover shape or centering, so schema 0.1.0 stores explicit rectangle dimensions and a center offset.
2289
2436
 
2290
2437
  The first resolved footprint is deliberately narrow:
2291
2438
 
@@ -2303,9 +2450,9 @@ geometric sensitive rectangle area
2303
2450
  nominal site lattice cell area
2304
2451
  ```
2305
2452
 
2306
- This is a **geometry diagnostic**, not a calibrated sensitivity result. It does not establish QE, effective radiometric collection area, photon conversion, optical throughput, or physical photodiode truth.
2453
+ This is a **geometry diagnostic**, not a calibrated sensitivity result. It does not establish QE, effective radiometric collection area, photon conversion, optical throughput or the true photodiode geometry.
2307
2454
 
2308
- Use `kind: "unresolved"` when the geometric footprint is known to be required but its shape/dimensions are not defensibly available. Resolution then fails closed instead of fabricating a rectangle from site pitch, megapixels, or a fill-factor percentage.
2455
+ Use `kind: "unresolved"` when the geometric footprint is needed but its shape and dimensions are not defensibly known. Resolution then fails closed, instead of making up a rectangle from site pitch, megapixels or a fill-factor percentage.
2309
2456
 
2310
2457
  The first sampling-aperture model explicitly excludes:
2311
2458
 
@@ -2324,7 +2471,7 @@ Physical camera orientation remains downstream. The site lattice and aperture st
2324
2471
 
2325
2472
  ## Sensor spatial-sampling quadrature
2326
2473
 
2327
- Use `calculateSensorSpatialSamplingQuadrature()` to build deterministic renderer-neutral spatial integration nodes for one resolved color-sampling site after combining:
2474
+ Use `calculateSensorSpatialSamplingQuadrature()` to build deterministic spatial integration nodes (sample points with weights) for one resolved colour-sampling site. The nodes do not depend on any renderer. They combine:
2328
2475
 
2329
2476
  1. the geometric sensitive aperture from `SensorSamplingApertureProfile`; and
2330
2477
  2. the effective anti-aliasing point-splitting response from `SensorOpticalStackProfile`.
@@ -2355,63 +2502,66 @@ const quadrature =
2355
2502
  console.log(quadrature.value.nodes);
2356
2503
  ```
2357
2504
 
2358
- The first scheme is a tensor-product uniform midpoint rule over the geometric aperture. Each AA split component multiplies that aperture quadrature.
2505
+ The first scheme is a uniform midpoint rule on a grid (tensor product) over the geometric aperture. Each AA split component multiplies that aperture quadrature.
2359
2506
 
2360
- AA composition uses **inverse source sampling**. If the optical stack declares that a component displaces optical energy by `+delta` on the sensor plane, a destination aperture point reads the pre-AA optical field at:
2507
+ AA composition uses **inverse source sampling**. If the optical stack says a component shifts optical energy by `+delta` on the sensor plane, a destination aperture point reads the optical field before the AA filter (pre-AA) at:
2361
2508
 
2362
2509
  ```text
2363
2510
  pre-AA source = destination aperture point - AA displacement
2364
2511
  ```
2365
2512
 
2366
- The destination sensor site's CFA/color channel applies to every node. A shifted pre-AA source coordinate is an optical-field location, not another CFA site, so it never reassigns the destination CFA channel.
2513
+ The destination sensor site's CFA colour channel applies to every node. A shifted pre-AA source coordinate is a location in the optical field, not another CFA site, so it never changes the destination CFA channel.
2367
2514
 
2368
2515
  Two measures are returned separately:
2369
2516
 
2370
2517
  - `combinedNormalizedSpatialWeight` — dimensionless weight for approximating a spatial average;
2371
2518
  - `combinedAreaMeasureSquareMicrometers` — geometric area measure for approximating a spatial area integral.
2372
2519
 
2373
- The area measure is **not** effective radiometric collection area, throughput, QE, or photon count.
2520
+ The area measure is **not** effective radiometric collection area, throughput, QE or photon count.
2374
2521
 
2375
- Near sensor edges, AA inverse-source coordinates may lie outside the active physical imaging area. Those nodes remain valid and are retained. Photivra does not clamp them, drop them, or renormalize the remaining weights. A renderer/optical-field provider owns source-coverage policy.
2522
+ Near the sensor edges, AA inverse-source coordinates may fall outside the active physical imaging area. Those nodes stay valid and are kept. Photivra does not clamp them, drop them or renormalize the remaining weights. The renderer or optical-field provider decides what to do about source coverage.
2376
2523
 
2377
- The API intentionally returns geometry and measures only. It does not evaluate optical-field values, radiance, spectral response, photons/electrons, temporal exposure, RAW values, or reconstruction. Spatial and temporal quadrature remain separate until a later explicit composition evaluates a common downstream field.
2524
+ The API deliberately returns only geometry and measures. It does not evaluate optical-field values, radiance, spectral response, photons/electrons, temporal exposure, RAW values or reconstruction. Spatial and temporal quadrature stay separate until a later, explicit composition evaluates a common field.
2378
2525
 
2379
- Because spatial integration error depends on the downstream optical field's spatial-frequency/content, Photivra does not report a geometry-only convergence/error estimate. Reference rendering can compare increasing spatial sample counts in the quantity actually being integrated.
2526
+ How large the integration error is depends on the detail (spatial frequencies) in the optical field being integrated. So Photivra does not report a convergence or error estimate from geometry alone. Reference rendering can compare increasing spatial sample counts on the quantity actually being integrated.
2380
2527
 
2381
- A per-site node allocation safety limit prevents malformed/untrusted counts from creating an oversized array in one call.
2528
+ A per-site safety limit on the number of nodes stops malformed or untrusted counts from creating an oversized array in one call.
2382
2529
 
2383
2530
  ## Sensor spatial-sample reduction
2384
2531
 
2385
- Use `reduceSensorSpatialSamplingQuadrature()` after a renderer or optical model has evaluated one scalar value at every spatial quadrature node's `preAntiAliasingSourcePointMm`.
2532
+ Use `reduceSensorSpatialSamplingQuadrature()` after a renderer or optical model has evaluated one scalar value at the `preAntiAliasingSourcePointMm` of every spatial quadrature node.
2386
2533
 
2387
- Node values are identified by the quadrature identity tuple—AA component index plus aperture X/Y sample indices—rather than by array position.
2534
+ Node values are matched by the node's identity — AA component index plus aperture X/Y sample indices — not by array position.
2388
2535
 
2389
2536
  Two input domains are supported:
2390
2537
 
2391
2538
  - `relative-linear`: a nonnegative dimensionless irradiance-like proxy for deterministic regression/testing;
2392
2539
  - `radiometric-irradiance`: physical sensor-plane irradiance in W/m².
2393
2540
 
2394
- The physical domain follows standard radiometric meaning: irradiance is incident radiant flux per unit area, so integrating W/m² over the geometric aperture area yields incident radiant flux in watts. This still remains upstream of CFA spectral response, microlens response, QE, exposure-time integration, and sensor conversion.
2541
+ The physical domain uses the standard radiometric meaning. Irradiance is incident radiant flux per unit area, so integrating W/m² over the geometric aperture area gives the incident radiant flux in watts. This is still before CFA spectral response, microlens response, QE, exposure-time integration and sensor conversion.
2395
2542
 
2396
- The reducer returns both a normalized spatial average and an area-weighted integral. For relative values the area integral is relative-value × µm²; for physical irradiance it is converted to watts.
2543
+ The reducer returns both a normalized spatial average and an area-weighted integral. For relative values the area integral is relative value × µm². For physical irradiance it is converted to watts.
2397
2544
 
2398
- The CFA `channelId` is descriptive metadata only. No wavelength-dependent filter/sensor response is applied, so the output is **not** a physically color-filtered mosaic value and must not be called RAW.
2545
+ The CFA `channelId` is descriptive metadata only. No wavelength-dependent filter or sensor response is applied. The output is therefore **not** a physically colour-filtered mosaic value, and must not be called RAW.
2399
2546
 
2400
- Every quadrature node requires an explicit nonnegative finite input, including nodes whose pre-AA source coordinate lies outside the active imaging area. The reducer never clamps, zero-fills, extrapolates, drops, or renormalizes missing edge support.
2547
+ Every quadrature node needs an explicit, non-negative, finite input, including nodes whose pre-AA source coordinate lies outside the active imaging area. The reducer never clamps, zero-fills, extrapolates, drops or renormalizes missing edge support.
2401
2548
 
2402
- Signed color-transform intermediates, RGB display values, gamma-encoded values, radiance, spectral irradiance, temporal exposure, photons/electrons, noise, saturation, black level, ADC quantization, RAW codes, and demosaic/reconstruction are outside this contract.
2549
+ Outside this contract: signed colour-transform intermediates, RGB display values, gamma-encoded values, radiance, spectral irradiance, temporal exposure, photons/electrons, noise, saturation, black level, ADC quantization, RAW codes and demosaicing/reconstruction.
2403
2550
 
2404
2551
  ## Sensor spectral response
2405
2552
 
2406
- Use `parseSensorSpectralResponseProfile()` to declare wavelength-dependent response for exact semantic channel IDs from a linked `SensorColorSamplingProfile`, and `resolveSensorSpectralResponseAtWavelength()` to evaluate one channel at one wavelength.
2553
+ The spectral response says how strongly each sensor channel responds at each wavelength.
2407
2554
 
2408
- The first contract supports three evidence models:
2555
+ - `parseSensorSpectralResponseProfile()` declares the wavelength-dependent response for exact channel IDs from a linked `SensorColorSamplingProfile`.
2556
+ - `resolveSensorSpectralResponseAtWavelength()` evaluates one channel at one wavelength.
2409
2557
 
2410
- - **direct effective external QE** — dimensionless generated-charge/incident-photon response for the declared response scope;
2411
- - **direct effective spectral responsivity** — A/W as a distinct physical representation;
2412
- - **separable channel-filter transmittance × detector external QE** — only when both components are independently established and explicitly declared separable.
2558
+ The first contract supports three kinds of evidence:
2413
2559
 
2414
- Do not force a real measured effective response into filter and detector components when the source does not support that decomposition. Conversely, do not apply upstream optical/filter terms again to a response whose scope already includes them.
2560
+ - **direct effective external QE** — a dimensionless response (generated charge per incident photon) for the declared response scope;
2561
+ - **direct effective spectral responsivity** — in A/W (amperes per watt), a different physical representation;
2562
+ - **separable channel-filter transmittance × detector external QE** — only when both parts are independently established and explicitly declared separable.
2563
+
2564
+ Do not split a real measured effective response into filter and detector parts when the source does not support that split. And do not apply upstream optical or filter terms again to a response whose scope already includes them.
2415
2565
 
2416
2566
  Curve rules are intentionally strict:
2417
2567
 
@@ -2425,13 +2575,13 @@ Curve rules are intentionally strict:
2425
2575
  - requests outside the declared wavelength range fail closed;
2426
2576
  - Photivra never assumes response becomes zero outside the measured range.
2427
2577
 
2428
- Embedded multi-point numeric curves require at least one `reusable-data` or `photivra-owned` evidence record. Publicly viewable/manufacturer curves that are only factual-reference material may guide research, but cannot be copied into the public engine without reuse rights.
2578
+ Embedded numeric curves with several points need at least one `reusable-data` or `photivra-owned` evidence record. Publicly viewable or manufacturer curves that are only factual reference material may guide research, but cannot be copied into the public engine without reuse rights.
2429
2579
 
2430
- Channel IDs remain exact. If two mosaic positions require different green-channel response curves, the color topology must use distinct semantic IDs such as `green-a` and `green-b`; one shared `green` ID means one shared semantic channel for this contract.
2580
+ Channel IDs stay exact. If two mosaic positions need different green response curves, the colour topology must use distinct IDs such as `green-a` and `green-b`. One shared `green` ID means one shared channel in this contract.
2431
2581
 
2432
- The resolver validates the entire spectral profile against the linked topology. Unknown channels fail closed. Layered-color topology remains fail-closed because its per-layer spatial relationship is still unresolved.
2582
+ The resolver validates the whole spectral profile against the linked topology. Unknown channels fail closed. Layered-colour topology still fails closed, because its per-layer spatial relationship is unresolved.
2433
2583
 
2434
- Optional reference temperature, incidence angle, and polarization metadata may record the condition under which a curve applies. Schema 0.1.0 does not model response variation with those conditions.
2584
+ Optional reference temperature, incidence angle and polarization metadata may record the conditions under which a curve applies. Schema 0.1.0 does not model how the response changes with those conditions.
2435
2585
 
2436
2586
  This foundation performs **spectral lookup only**. It does not:
2437
2587
 
@@ -2446,7 +2596,7 @@ Those remain explicit downstream steps.
2446
2596
 
2447
2597
  ## Sensor spectral quadrature
2448
2598
 
2449
- Use `calculateSensorSpectralQuadrature()` to build a deterministic wavelength-sampling plan for one exact sensor-response channel before any source spectrum or sensor response is integrated.
2599
+ Use `calculateSensorSpectralQuadrature()` to build a deterministic plan of wavelength samples for one exact sensor-response channel. The plan comes before any source spectrum or sensor response is integrated.
2450
2600
 
2451
2601
  Composition fragment: requires previously validated bindings `colorSamplingProfile`, `spectralResponseProfile`. This is an integration sketch, not a standalone runnable program.
2452
2602
 
@@ -2470,24 +2620,27 @@ console.log(plan.value.nodes[0]?.wavelengthNanometers);
2470
2620
  console.log(plan.value.nodes[0]?.wavelengthMeasureNanometers);
2471
2621
  ```
2472
2622
 
2473
- The requested range edges, every applicable sensor-response curve knot, and optional caller-supplied breakpoints form the segment boundaries. Each segment is then subdivided until every midpoint subinterval is no wider than `maximumSubintervalWidthNanometers`.
2623
+ How the plan is built:
2624
+
2625
+ 1. The segment boundaries are the edges of the requested range, every applicable knot of the sensor-response curve, and any breakpoints you supply.
2626
+ 2. Each segment is split until every midpoint subinterval is no wider than `maximumSubintervalWidthNanometers`.
2474
2627
 
2475
- `wavelengthMeasureNanometers` is **dλ in nanometres**. `normalizedWavelengthWeight` is only that wavelength measure divided by the total requested span. Neither value includes QE, A/W responsivity, channel-filter transmission, lens transmission, source power, photon flux, or any other physical response factor.
2628
+ `wavelengthMeasureNanometers` is **dλ in nanometres**. `normalizedWavelengthWeight` is just that wavelength measure divided by the total requested span. Neither value includes QE, A/W responsivity, channel-filter transmission, lens transmission, source power, photon flux or any other physical response factor.
2476
2629
 
2477
- The requested wavelength range must lie wholly inside the selected response channel's usable declared range. Photivra does not clip, extrapolate, or implicitly zero-fill response outside that range. This check proves only **sensor-response support**; it does not prove that scene spectral data, optical transmission, or another wavelength-dependent input covers the same interval.
2630
+ The requested wavelength range must lie entirely inside the selected channel's usable declared range. Photivra does not clip, extrapolate or silently zero-fill the response outside that range. This check proves only that **the sensor response covers the range**. It does not prove that scene spectral data, optical transmission or any other wavelength-dependent input covers it too.
2478
2631
 
2479
- `additionalBreakpointsNanometers` is intended for known interpolation knots or discontinuity boundaries from other **continuous spectral-density** factors. It must be strictly increasing, contain no duplicates, and remain strictly inside the requested range. This API does not ingest those external spectra and does not claim their provenance or validity.
2632
+ `additionalBreakpointsNanometers` is for known interpolation knots or discontinuities from other **continuous spectral-density** factors. The values must be strictly increasing, with no duplicates, and strictly inside the requested range. This API does not read those external spectra, and makes no claim about their provenance or validity.
2480
2633
 
2481
- The plan preserves the selected response kind, response scope, scientific status, uncertainty declaration, and response-data evidence, but it deliberately **does not apply response values at the nodes**. That boundary matters because:
2634
+ The plan keeps the selected response kind, response scope, scientific status, uncertainty declaration and response-data evidence. But it deliberately **does not apply response values at the nodes**. That matters because:
2482
2635
 
2483
- - external QE is a dimensionless photon-to-generated-charge efficiency;
2636
+ - external QE is a dimensionless efficiency (generated charge per photon);
2484
2637
  - spectral responsivity in A/W maps incident radiant power to electrical current;
2485
- - those representations require different compatible downstream source/signal semantics and must not be collapsed into one integration formula;
2486
- - response scope must be matched to the source plane and collection-area semantics so upstream transmission is neither omitted nor double-counted.
2638
+ - those two representations need different, compatible source and signal meanings downstream, and must not be collapsed into one integration formula;
2639
+ - the response scope must be matched to the source plane and collection area, so upstream transmission is neither left out nor counted twice.
2487
2640
 
2488
- The first quadrature contract targets continuous spectral densities. Discrete/delta-like emission lines need a separate explicit representation rather than being hidden inside a midpoint grid.
2641
+ The first quadrature contract is for continuous spectral densities. Discrete, delta-like emission lines need a separate explicit representation; they must not be hidden inside a midpoint grid.
2489
2642
 
2490
- A bounded wavelength step and alignment to known response knots improve deterministic sampling, but neither proves convergence for an arbitrary integrand. The planner therefore reports no source-independent numerical error estimate.
2643
+ A bounded wavelength step, aligned to the known response knots, makes sampling deterministic, but does not prove convergence for an arbitrary integrand. The planner therefore reports no numerical error estimate that ignores the source.
2491
2644
 
2492
2645
  This foundation still does **not**:
2493
2646
 
@@ -2507,7 +2660,7 @@ Use `reduceSensorSpatioSpectralIrradiance()` after a renderer or optical model h
2507
2660
  - one spatial quadrature node's `preAntiAliasingSourcePointMm`; and
2508
2661
  - one spectral quadrature wavelength.
2509
2662
 
2510
- The required source value is (E_λ(x,y)) in **W/m²/nm**.
2663
+ The required source value is E_λ(x,y), spectral irradiance, in **W/m²/nm**.
2511
2664
 
2512
2665
  Composition fragment: requires previously validated bindings `sampleValues`, `spatialPlan`, `spectralPlan`. This is an integration sketch, not a standalone runnable program.
2513
2666
 
@@ -2535,11 +2688,11 @@ console.log(
2535
2688
  );
2536
2689
  ```
2537
2690
 
2538
- The reducer validates both plans again at runtime and requires exact `colorSamplingProfileId` and `channelId` agreement. A shared label such as `"red"` is not enough to establish that two independently created plans belong to the same color topology.
2691
+ The reducer validates both plans again at runtime, and requires `colorSamplingProfileId` and `channelId` to match exactly. A shared label such as `"red"` is not enough to show that two independently created plans belong to the same colour topology.
2539
2692
 
2540
- Each supplied sample identity contains the full spatial-node identity, `spectralSampleIndex`, and exact wavelength. Array order is irrelevant. Every spatial × spectral pair must be present exactly once.
2693
+ Each supplied sample identity contains the full spatial-node identity, `spectralSampleIndex` and the exact wavelength. Array order does not matter. Every spatial × spectral pair must be present exactly once.
2541
2694
 
2542
- The Cartesian product has its own **100,000-value safety limit**. The individual spatial and spectral plans may each be below their own limits while their product is still too large to materialize safely.
2695
+ The Cartesian product has its own **safety limit of 100,000 values**. The spatial and spectral plans may each be within their own limits while their product is still too large to build safely.
2543
2696
 
2544
2697
  For each wavelength node, Photivra computes:
2545
2698
 
@@ -2551,45 +2704,45 @@ It then multiplies those densities by that node's `wavelengthMeasureNanometers`
2551
2704
  - wavelength-integrated spatial-average irradiance in W/m²;
2552
2705
  - wavelength-integrated geometric-aperture incident flux in W.
2553
2706
 
2554
- Because the source density is **per nanometre**, `dλ` remains in nanometres in this multiplication. Do not multiply by (10^{-9}) unless the spectral-density denominator has also been converted from per-nanometre to per-metre.
2707
+ Because the source density is **per nanometre**, `dλ` stays in nanometres in this multiplication. Do not multiply by 10⁻⁹ unless the spectral density has also been converted from per-nanometre to per-metre.
2555
2708
 
2556
- The geometric-aperture flux remains a **geometric incident-flux** result. The current aperture model does not establish effective collection area, microlens collection efficiency, or another radiometric-area correction.
2709
+ The geometric-aperture flux stays a **geometric incident-flux** result. The current aperture model does not establish effective collection area, microlens collection efficiency or any other radiometric area correction.
2557
2710
 
2558
- This reducer intentionally stops **before sensor response**. The wavelength plan is response-derived, but no QE, A/W responsivity, or channel-filter transmission is applied. Response scope is carried forward but is not yet matched to a declared source plane. That avoids two invalid shortcuts:
2711
+ This reducer deliberately stops **before the sensor response**. The wavelength plan is derived from the response, but no QE, A/W responsivity or channel-filter transmission is applied. The response scope is carried along but not yet matched to a declared source plane. That avoids two invalid shortcuts:
2559
2712
 
2560
2713
  - treating QE as though it were an energy weighting on W/m²/nm;
2561
2714
  - treating A/W responsivity as though it were a photon-to-electron efficiency.
2562
2715
 
2563
2716
  Temporal exposure integration, photons/electrons, electrical current, noise, ADC/RAW conversion, and reconstruction also remain downstream.
2564
2717
 
2565
- The current AA point-splitting kernel is a normalized, wavelength-invariant spatial redistribution approximation. Spectral variation in the supplied (E_λ(x,y)) can still represent wavelength-dependent upstream optics/scene structure, but this reducer does not invent wavelength dependence in the AA kernel itself.
2718
+ The current AA point-splitting kernel is a normalized spatial redistribution approximation that does not vary with wavelength. Spectral variation in the supplied E_λ(x,y) can still represent wavelength-dependent optics or scene structure upstream, but this reducer does not invent wavelength dependence in the AA kernel itself.
2566
2719
 
2567
- As with the spatial and spectral planners individually, no source-independent convergence/error estimate is reported. Reference work should test convergence in the actual downstream integrand by increasing spatial and/or wavelength sampling.
2720
+ As with the spatial and spectral planners, no convergence or error estimate is reported independently of the source. Reference work should test convergence on the actual integrand by increasing spatial and/or wavelength sampling.
2568
2721
 
2569
2722
  ## Sensor response application compatibility
2570
2723
 
2571
- Use `assessSensorResponseApplicationCompatibility()` before the typed sensor-response conversion paths. The assessment is deliberately a **structural gate**, not a response application function.
2724
+ Use `assessSensorResponseApplicationCompatibility()` before the typed sensor-response conversions. The assessment is deliberately a **structural gate** (a check that it is valid to apply the response). It does not apply the response.
2572
2725
 
2573
- It requires an evidence-backed `SensorResponseApplicationProfile` that binds one exact spectral-response profile/channel to the sampling-aperture and optical-stack identities used by the spatial plan. It also declares:
2726
+ It needs an evidence-backed `SensorResponseApplicationProfile`. That profile binds one exact spectral-response profile and channel to the sampling-aperture and optical-stack identities used by the spatial plan. It also declares:
2574
2727
 
2575
2728
  - the response's incident-area normalization;
2576
2729
  - whether a single wavelength-only response can be treated as uniform/separable over the geometric sensitive aperture;
2577
2730
  - how response reference conditions are handled.
2578
2731
 
2579
- The caller separately declares whether the supplied radiometric field is at the `sensor-package-incident` or `site-incident` response reference plane. That declaration carries provenance.
2732
+ The caller separately declares whether the supplied radiometric field is at the `sensor-package-incident` or the `site-incident` response reference plane. That declaration carries provenance.
2580
2733
 
2581
- A response scope and source plane must match exactly:
2734
+ The response scope and the source plane must match exactly:
2582
2735
 
2583
2736
  - `sensor-package-incident-effective-channel-response` requires a sensor-package-incident source;
2584
2737
  - site-incident effective response and separable channel-filter × detector-EQE require a site-incident source.
2585
2738
 
2586
- The current spatio-spectral reducer integrates **geometric-sensitive-aperture** flux only. Therefore the first compatibility gate accepts response application only when the response's incident-area basis is that same geometric sensitive aperture and its declared area matches. A response normalized to the full site cell or an effective collection area is reported as blocked rather than silently reused.
2739
+ The current spatio-spectral reducer integrates flux over the **geometric sensitive aperture** only. So the first compatibility gate accepts a response only when its incident-area basis is that same geometric sensitive aperture, and its declared area matches. A response normalized to the full site cell, or to an effective collection area, is reported as blocked instead of being silently reused.
2587
2740
 
2588
- This distinction is important for common image-sensor QE conventions: a total-pixel QE can be defined against photons incident on the whole pixel area and may therefore already include fill-factor and microlens effects. Such a response is not interchangeable with a curve normalized to photons incident only on the geometric sensitive aperture.
2741
+ This matters for common image-sensor QE conventions. A total-pixel QE can be defined against photons landing on the whole pixel area, so it may already include fill-factor and microlens effects. Such a response cannot be swapped for a curve normalized to photons landing only on the geometric sensitive aperture.
2589
2742
 
2590
- Spatial response is a separate gate as well. Reducing the field spatially before applying one scalar response (R(λ)) assumes response is effectively uniform/separable over the integrated aperture. If that is not established, the gate blocks post-spatial application because the physically correct model may require (R(λ,x,y)) at the original spatial nodes.
2743
+ Spatial response is a separate gate. Reducing the field spatially before applying one scalar response R(λ) assumes the response is effectively uniform (separable) across the integrated aperture. If that is not established, the gate blocks applying the response after spatial reduction, because the physically correct model may need R(λ,x,y) at the original spatial nodes.
2591
2744
 
2592
- Reference conditions are also fail-closed. With `exact-match-required`, the response must declare reference conditions and the caller must provide matching operating temperature, incidence angle, and/or polarization for every condition present in that reference. `assume-compatible` is allowed only as an explicit evidence-backed approximation with a limitation.
2745
+ Reference conditions also fail closed. With `exact-match-required`, the response must declare reference conditions, and the caller must supply a matching operating temperature, incidence angle and/or polarization for every condition in that reference. `assume-compatible` is allowed only as an explicit, evidence-backed approximation with a stated limitation.
2593
2746
 
2594
2747
  An unresolved wavelength basis blocks the gate.
2595
2748
 
@@ -2598,13 +2751,13 @@ A successful assessment reports the authorized signal path:
2598
2751
  - EQE and filter×EQE → `photon-rate-to-electrons`;
2599
2752
  - A/W responsivity → `radiant-power-to-current`.
2600
2753
 
2601
- Even a successful assessment keeps `signalConversionAuthorized: false`. Response linearity/dynamic-range validity is not yet assessed, and no response, photon/electron, current, temporal, noise, ADC, RAW, or reconstruction calculation is performed.
2754
+ Even a successful assessment keeps `signalConversionAuthorized: false`. Whether the response is linear over the signal level (dynamic range) is checked by the next gate. No response, photon/electron, current, temporal, noise, ADC, RAW or reconstruction calculation is done here.
2602
2755
 
2603
2756
  ## Sensor response operating range
2604
2757
 
2605
- Use `assessSensorResponseOperatingRange()` after structural response compatibility has passed. This second gate asks a narrower question: is the **instantaneous optical input** inside a response-law range for which the detector's linearity has been explicitly characterized?
2758
+ Use `assessSensorResponseOperatingRange()` after structural response compatibility has passed. This second gate asks a narrower question: is the **instantaneous optical input** inside a range where the detector's linearity has been explicitly measured?
2606
2759
 
2607
- The operating-range profile is evidence-backed and preserves the quantity that was actually calibrated:
2760
+ The operating-range profile is evidence-backed, and keeps the quantity that was actually calibrated:
2608
2761
 
2609
2762
  - wavelength-integrated geometric-aperture radiant power in W; or
2610
2763
  - wavelength-integrated spatial-average irradiance in W/m².
@@ -2620,13 +2773,13 @@ The profile also declares:
2620
2773
  - scientific status and uncertainty;
2621
2774
  - reference temperature/incidence-angle/polarization conditions and their matching policy.
2622
2775
 
2623
- The assessment first requires the prior structural compatibility result to be non-blocked and to refer to the same response application, spectral response, color topology and channel. It then checks wavelength basis/range, optical-input level and linearity-calibration conditions.
2776
+ The assessment first requires the earlier structural compatibility result to be unblocked, and to refer to the same response application, spectral response, colour topology and channel. It then checks the wavelength basis and range, the optical input level, and the conditions of the linearity calibration.
2624
2777
 
2625
- The operating-range profile also distinguishes **broadband-only** validation from **per-spectral-bin** validation. Broadband-only evidence can establish that the total optical input is inside a characterized range, but it cannot authorize wavelength-dependent EQE or A/W conversion.
2778
+ The profile also tells **broadband-only** validation apart from **per-spectral-bin** validation. Broadband-only evidence can show that the total optical input is inside a measured range, but it cannot authorize wavelength-dependent EQE or A/W conversion.
2626
2779
 
2627
- For `per-spectral-bin` applicability, the profile declares a maximum supported bin width. Every wavelength node's own integrated power or irradiance contribution must remain inside the calibrated range, and no quadrature bin may exceed that width. This prevents one over-range wavelength band from hiding inside an acceptable broadband total and prevents a broad numerical bin from masquerading as a narrowband calibration point.
2780
+ For `per-spectral-bin` applicability, the profile declares a maximum supported bin width. Each wavelength node's own integrated power or irradiance contribution must stay inside the calibrated range, and no quadrature bin may be wider than that width. This stops one over-range wavelength band from hiding inside an acceptable broadband total. It also stops a wide numerical bin from passing as a narrowband calibration point.
2628
2781
 
2629
- Only a non-blocked per-bin assessment can set `responseRateConversionAuthorized: true`. That authorization is deliberately limited to an **instantaneous rate-domain conversion**.
2782
+ Only an unblocked per-bin assessment can set `responseRateConversionAuthorized: true`. That authorization is deliberately limited to an **instantaneous conversion of rates**.
2630
2783
 
2631
2784
  It does **not** establish:
2632
2785
 
@@ -2637,47 +2790,51 @@ It does **not** establish:
2637
2790
  - ADC linearity or clipping;
2638
2791
  - RAW-code validity.
2639
2792
 
2640
- Those depend on temporal integration and downstream electronics and remain separate gates. In particular, an optical detector can be linear over a given incident-power range while a camera exposure still saturates after sufficient integration time.
2793
+ Those depend on integration over time and on downstream electronics, and have their own gates. In particular, a detector can be linear over a given range of incident power while a camera exposure still saturates after a long enough integration time.
2641
2794
 
2642
- The first operating-range model also does not infer a valid wavelength interval from one or a few isolated linearity measurements. If a source only establishes linearity at specific wavelengths, a continuous wavelength applicability range must not be fabricated.
2795
+ The first operating-range model also does not infer a valid wavelength interval from one or a few isolated linearity measurements. If a source shows linearity only at specific wavelengths, do not make up a continuous wavelength range from them.
2643
2796
 
2644
2797
  ## Photon energy wavelength basis
2645
2798
 
2646
- Use `calculatePhotonEnergyFromWavelength()` when a later photon-domain calculation needs (hν).
2799
+ Use `calculatePhotonEnergyFromWavelength()` when a later photon-domain calculation needs the photon energy hν.
2647
2800
 
2648
- For a vacuum-basis wavelength, Photivra uses the exact SI defining constants:
2801
+ For a wavelength measured in vacuum, Photivra uses the exact SI defining constants:
2649
2802
 
2650
- - (h = 6.62607015 × 10^{-34}) J·s;
2651
- - (c = 299792458) m/s.
2803
+ - h = 6.62607015 × 10⁻³⁴ J·s (Planck constant);
2804
+ - c = 299792458 m/s (speed of light).
2652
2805
 
2653
- The calculation is (ν = c / λ_mathrm{vac}) and (E_mathrm{photon} = hν).
2806
+ The calculation is ν = c / λ_vac and E_photon = hν.
2654
2807
 
2655
- For an **air-basis** wavelength, the wavelength cannot be substituted directly into (hc/λ). The caller must provide a sourced phase refractive index at that **exact wavelength** using the explicit definition:
2808
+ A wavelength measured in **air** cannot be put directly into hc/λ, because light is slightly slower in air. The caller must supply a sourced phase refractive index at that **exact wavelength**, using the explicit definition:
2656
2809
 
2657
2810
  `n = λ_vacuum / λ_air`.
2658
2811
 
2659
2812
  Photivra then computes `λ_vacuum = n × λ_air` before deriving frequency and photon energy.
2660
2813
 
2661
- The air refractive-index sample carries scientific status, uncertainty, evidence, and optional reference atmosphere. Calibrated samples require quantified relative uncertainty plus temperature and pressure reference conditions.
2814
+ The air refractive-index sample carries a scientific status, uncertainty, evidence and an optional reference atmosphere. Calibrated samples need a quantified relative uncertainty, plus reference temperature and pressure.
2662
2815
 
2663
- Air refractive index also depends on atmospheric conditions. Therefore an air-basis photon-energy calculation requires one of two policies:
2816
+ The refractive index of air also depends on atmospheric conditions. So a photon-energy calculation from an air wavelength needs one of two policies:
2664
2817
 
2665
2818
  - `exact-match-required`: the caller supplies operating atmosphere conditions that match the refractive-index reference temperature/pressure and any declared humidity/CO₂ fields;
2666
2819
  - `assume-compatible`: an explicit evidence-backed approximation with a limitation.
2667
2820
 
2668
- Photivra never silently sets (n=1).
2821
+ Photivra never silently sets n = 1.
2669
2822
 
2670
- The current foundation preserves refractive-index uncertainty but does **not** propagate that uncertainty into the derived photon energy. It also does not apply QE, calculate photon rate, generate electrons, integrate exposure time, or model saturation/noise.
2823
+ The foundation keeps the refractive-index uncertainty, but does **not** carry it through into the derived photon energy. It also does not apply QE, calculate a photon rate, generate electrons, integrate over exposure time, or model saturation or noise.
2671
2824
 
2672
2825
  ## EQE electron-rate conversion
2673
2826
 
2674
- Use `calculateSensorEqeElectronRate()` only after both the structural response-application assessment and the instantaneous response operating-range assessment have passed.
2827
+ This converts light into an expected rate of generated electrons. Use `calculateSensorEqeElectronRate()` only after both gates above have passed: the structural response-application assessment, and the instantaneous operating-range assessment.
2675
2828
 
2676
- The converter accepts the original color-sampling and spectral-response profiles in addition to those assessments. Spectral planning now carries an exact canonical response-channel data binding through the pre-response reduction. The converter reparses the supplied response once and requires that binding to match exactly, so matching IDs and even matching evidence references are not enough when numeric calibration samples differ.
2829
+ Besides those assessments, the converter takes the original colour-sampling and spectral-response profiles.
2677
2830
 
2678
- The canonical binding is a deterministic exact-data identity for stale/calibration substitution detection; it is not presented as a cryptographic checksum or security boundary.
2831
+ - Spectral planning carries an exact, canonical binding to the response-channel data through the reduction.
2832
+ - The converter re-parses the supplied response once and requires that binding to match exactly.
2833
+ - So matching IDs, or even matching evidence references, are not enough if the numeric calibration samples differ.
2679
2834
 
2680
- The calculation stays in the wavelength quadrature. For every spectral node it computes:
2835
+ The canonical binding is a deterministic identity of the exact data, used to detect stale or swapped calibration data. It is not a cryptographic checksum or a security boundary.
2836
+
2837
+ The calculation stays inside the wavelength quadrature. For every spectral node it computes:
2681
2838
 
2682
2839
  1. radiant-power contribution = geometric-aperture spectral flux density × `dλ`;
2683
2840
  2. photon energy (hν), using the photon-energy wavelength-basis foundation;
@@ -2685,15 +2842,15 @@ The calculation stays in the wavelength quadrature. For every spectral node it c
2685
2842
  4. effective external QE at that exact wavelength through the existing spectral-response resolver;
2686
2843
  5. expected generated-electron rate = photon rate × effective QE.
2687
2844
 
2688
- Direct effective EQE stays direct. When the response profile explicitly declares channel-filter transmittance × detector EQE as separable, the existing resolver is the only component that multiplies those terms; the converter does not invent or repeat filter composition.
2845
+ Direct effective EQE stays direct. When the response profile explicitly declares channel-filter transmittance × detector EQE as separable, the existing resolver is the only place those terms are multiplied. The converter does not invent or repeat filter composition.
2689
2846
 
2690
- The response is applied **per wavelength node**. Photivra never multiplies total broadband radiant power by a single average QE.
2847
+ The response is applied **at each wavelength node**. Photivra never multiplies total broadband radiant power by one average QE.
2691
2848
 
2692
- Post-spatial response also requires the operating-range profile to establish **linear superposition over the geometric sensitive aperture**. If that declaration is omitted, it remains `not-established` and rate conversion is blocked. This prevents spatial averaging from silently hiding a sub-aperture nonlinear response.
2849
+ Applying the response after spatial reduction also needs the operating-range profile to establish **linear superposition over the geometric sensitive aperture**. If that declaration is missing, it stays `not-established` and rate conversion is blocked. This stops spatial averaging from silently hiding a nonlinear response inside the aperture.
2693
2850
 
2694
- The EQE converter additionally requires the operating-range assessment's per-bin identities and calibrated-domain values to match the exact wavelength nodes in the reduction. A stale assessment cannot be reused after spectral power, node width, wavelength, or identity changes.
2851
+ The EQE converter also requires the operating-range assessment's per-bin identities and calibrated-domain values to match the exact wavelength nodes in the reduction. A stale assessment cannot be reused after the spectral power, node width, wavelength or identity changes.
2695
2852
 
2696
- For air-basis spectral nodes, the caller supplies exactly one sourced phase-refractive-index record for every spectral sample identity plus the atmosphere compatibility policy used by the photon-energy foundation. Vacuum-basis conversion accepts no air-index context.
2853
+ For air-wavelength spectral nodes, the caller supplies exactly one sourced phase-refractive-index record per spectral sample identity, plus the atmosphere compatibility policy used for photon energy. Vacuum-wavelength conversion accepts no air-index context.
2697
2854
 
2698
2855
  The output reports:
2699
2856
 
@@ -2702,9 +2859,9 @@ The output reports:
2702
2859
  - per-wavelength power, photon energy, QE and rate contributions;
2703
2860
  - whether channel-filter transmission was part of an explicitly separable response.
2704
2861
 
2705
- Kahan compensated summation is used across wavelength nodes.
2862
+ Kahan compensated summation (which limits rounding error) is used across wavelength nodes.
2706
2863
 
2707
- This is still **rate-domain only**. The converter does not:
2864
+ This is still **rates only**. The converter does not:
2708
2865
 
2709
2866
  - multiply by exposure time;
2710
2867
  - output photon or electron counts;
@@ -2713,15 +2870,23 @@ This is still **rate-domain only**. The converter does not:
2713
2870
  - calculate current from A/W responsivity;
2714
2871
  - perform ADC/RAW conversion or reconstruction.
2715
2872
 
2716
- A/W spectral responsivity is rejected by this API and remains a separate radiant-power→current path.
2873
+ This API rejects A/W spectral responsivity; that is a separate radiant-power → current path (below).
2717
2874
 
2718
- Response, refractive-index and quadrature uncertainties remain visible upstream but are not yet propagated into a combined electron-rate uncertainty.
2875
+ Response, refractive-index and quadrature uncertainties stay visible upstream, but are not yet combined into an electron-rate uncertainty.
2719
2876
 
2720
2877
  ## A/W responsivity photocurrent conversion
2721
2878
 
2722
- Use `calculateSensorResponsivityPhotocurrent()` for the sibling current-domain path when the spectral response is explicitly calibrated in **A/W**.
2879
+ Use `calculateSensorResponsivityPhotocurrent()` for the sibling path, which works in electrical current, when the spectral response is explicitly calibrated in **A/W**.
2880
+
2881
+ The same structural and operating-range gates used by EQE still apply:
2723
2882
 
2724
- The same structural and operating-range gates used by EQE still apply: exact response/profile linkage, matching response reference plane and area basis, spatial response uniformity/separability, linear superposition over the geometric aperture, and explicit per-spectral-bin operating-range applicability. The exact canonical response-channel binding must also match the curve data used upstream; same IDs/evidence with altered A/W samples are rejected.
2883
+ - exact linkage between response and profiles;
2884
+ - matching response reference plane and area basis;
2885
+ - spatial response uniformity/separability;
2886
+ - linear superposition over the geometric aperture;
2887
+ - explicit per-spectral-bin operating-range applicability.
2888
+
2889
+ The exact canonical response-channel binding must also match the curve data used upstream. The same IDs and evidence with altered A/W samples are rejected.
2725
2890
 
2726
2891
  For every spectral node Photivra computes:
2727
2892
 
@@ -2731,19 +2896,19 @@ For every spectral node Photivra computes:
2731
2896
 
2732
2897
  The wavelength contributions are summed with Kahan compensated summation.
2733
2898
 
2734
- A/W also has an additional **electrical calibration applicability** contract. `SensorResponsivityElectricalApplicabilityProfile` binds the response pipeline to the detector electrical conditions under which the A/W calibration is intended to apply:
2899
+ A/W also has an extra **electrical calibration applicability** contract. `SensorResponsivityElectricalApplicabilityProfile` binds the response pipeline to the electrical conditions of the detector under which the A/W calibration is meant to apply:
2735
2900
 
2736
2901
  - zero-bias photovoltaic operation or reverse bias with an explicit magnitude;
2737
2902
  - virtual-ground current readout or a finite declared input impedance;
2738
2903
  - exact-match or explicit evidence-backed compatibility-approximation policy.
2739
2904
 
2740
- These fields are applicability metadata, not a circuit simulator. In particular, Photivra does not use a declared input impedance to claim that a transimpedance circuit is adequate; it only verifies that the operating condition matches the calibration contract.
2905
+ These fields say when the calibration applies; they are not a circuit simulator. In particular, Photivra does not use a declared input impedance to claim that a transimpedance circuit is adequate. It only checks that the operating condition matches the calibration contract.
2741
2906
 
2742
- The electrical categories are intentionally disjoint: `reverse-biased` requires a strictly positive reverse-bias magnitude, while true zero-bias operation uses `zero-bias-photovoltaic`; a finite input impedance must also be strictly greater than zero.
2907
+ The electrical categories do not overlap. `reverse-biased` needs a strictly positive reverse-bias magnitude; true zero-bias operation uses `zero-bias-photovoltaic`. A finite input impedance must also be strictly greater than zero.
2743
2908
 
2744
- The spectral A/W curve is treated as a **quasi-static steady-state** power→current relation only. Detector impulse response, modulation bandwidth, settling time and frequency-dependent responsivity are not modeled by this first path. A future time-varying current integrator must add a separate temporal-response contract before it can consume rapidly varying optical power.
2909
+ The spectral A/W curve is treated only as a **quasi-static, steady-state** power → current relation. This first path does not model detector impulse response, modulation bandwidth, settling time or frequency-dependent responsivity. A future integrator for time-varying current must add a separate temporal-response contract before it can handle rapidly changing optical power.
2745
2910
 
2746
- The result is a **nonnegative detector-terminal photocurrent magnitude in amperes**. Photivra intentionally does not assign circuit direction/polarity from the optical response curve.
2911
+ The result is a **non-negative photocurrent magnitude at the detector terminal, in amperes**. Photivra deliberately does not assign a circuit direction (polarity) from the optical response curve.
2747
2912
 
2748
2913
  The A/W path does **not**:
2749
2914
 
@@ -2761,15 +2926,15 @@ Those remain later explicit stages.
2761
2926
 
2762
2927
  ## Local sensor-rate/exposure binding
2763
2928
 
2764
- Use `bindSensorRateToLocalExposure()` before any response-rate result is integrated over time.
2929
+ Each sensor site can have its own exposure window (for example with a rolling shutter). Use `bindSensorRateToLocalExposure()` to find that window before any response-rate result is integrated over time.
2765
2930
 
2766
- Engine-produced EQE electron-rate and A/W photocurrent results preserve the exact abstract color-sampling site that generated the response. The binding resolves that site against the exact color topology and the existing native-effective-raster/color-site binding before selecting a local shutter window.
2931
+ EQE electron-rate and A/W photocurrent results produced by the engine keep the exact abstract colour-sampling site that generated them. The binding resolves that site against the exact colour topology and the existing native-effective-raster / colour-site binding, then selects the local shutter window.
2767
2932
 
2768
- The first temporal registration is deliberately narrow: `sitesPerNativeSampleX` and `sitesPerNativeSampleY` must both equal 1. Under that evidenced shared-top-left relationship, color-site `(x, y)` maps to native effective sample center `(x + 0.5, y + 0.5)`.
2933
+ The first timing registration is deliberately narrow: `sitesPerNativeSampleX` and `sitesPerNativeSampleY` must both equal 1. Under that evidenced shared-top-left relationship, colour site `(x, y)` maps to the native effective sample center `(x + 0.5, y + 0.5)`.
2769
2934
 
2770
- A multi-site block binding is **not** enough to infer sub-sample timing coordinates. When one native effective sample spans multiple color sites, Photivra fails closed rather than inventing where each site lies inside that effective sample for a spatially varying shutter scan.
2935
+ A binding of multi-site blocks is **not** enough to infer timing coordinates inside one sample. When one native effective sample spans several colour sites, Photivra fails closed, rather than inventing where each site lies inside that sample for a shutter scan that varies across the sensor.
2771
2936
 
2772
- The binding then calls the authoritative capture exposure-window calculation at that exact native point. It verifies that:
2937
+ The binding then calls the authoritative capture exposure-window calculation at that exact native point. It checks that:
2773
2938
 
2774
2939
  - the rate carries a valid site;
2775
2940
  - the bound color site exists and resolves to the same channel as the rate;
@@ -2786,11 +2951,11 @@ This is a **timing binding only**. It explicitly reports:
2786
2951
  - no time-stationarity guarantee;
2787
2952
  - `constantRateTemporalIntegrationAuthorized: false`.
2788
2953
 
2789
- A response rate may therefore **not** yet be multiplied by this duration. Motion, flicker, flash, time-varying illumination/vignetting, shutter modulation, or detector transients require a time-dependent signal model. A separate stationarity contract is required for the constant-rate approximation.
2954
+ So a response rate may **not** yet be multiplied by this duration. Motion, flicker, flash, time-varying illumination or vignetting, shutter modulation and detector transients all need a time-dependent signal model. The constant-rate approximation needs a separate stationarity contract (next section).
2790
2955
 
2791
2956
  ## Constant-rate local-exposure integration
2792
2957
 
2793
- Use `integrateStationarySensorRateOverLocalExposure()` only when an evidence-backed `SensorRateTemporalStationarityProfile` explicitly states that the **reported rate itself** is constant through the exact local exposure window.
2958
+ Use `integrateStationarySensorRateOverLocalExposure()` only when an evidence-backed `SensorRateTemporalStationarityProfile` explicitly states that the **reported rate itself** stays constant through the exact local exposure window.
2794
2959
 
2795
2960
  The stationarity declaration is bound to:
2796
2961
 
@@ -2817,37 +2982,40 @@ For the A/W path:
2817
2982
  - circuit polarity is still not inferred;
2818
2983
  - carrier/electron count is deliberately **not** inferred from A/W current.
2819
2984
 
2820
- The A/W constant-current approximation also inherits the upstream quasi-static detector-response boundary. The stationarity evidence must justify treating the reported steady-state photocurrent as valid throughout the window; Photivra still does not model detector impulse response or bandwidth.
2985
+ The A/W constant-current approximation also inherits the quasi-static detector-response limit from upstream. The stationarity evidence must justify treating the reported steady-state photocurrent as valid throughout the window. Photivra still does not model detector impulse response or bandwidth.
2821
2986
 
2822
- The result is explicitly **photo-signal-only** accumulation. It does not include dark current, hot-pixel/defect current, leakage, charge injection, or other accumulated charge sources. Accordingly:
2987
+ The result accumulates **photo signal only**. It does not include dark current, hot-pixel/defect current, leakage, charge injection or other sources of accumulated charge. So:
2823
2988
 
2824
2989
  - physical full-well assessment is not authorized;
2825
2990
  - camera/digital saturation assessment is not authorized;
2826
2991
  - no clamp is applied;
2827
2992
  - shot/read noise, conversion gain, ADC/RAW and reconstruction remain downstream.
2828
2993
 
2829
- Physical full-well capacity and camera saturation capacity remain separate implemented assessment contracts; they are not interchangeable thresholds.
2994
+ Physical full-well capacity and camera saturation capacity are separate, implemented assessment contracts. They are not interchangeable thresholds.
2830
2995
 
2831
2996
  ## Dark-current charge
2832
2997
 
2833
- Use `calculateSensorDarkCurrentCharge()` with a stationary EQE expected-count exposure result or the validated compact [temporal photo signal](TEMPORAL_PHOTO_RAW.md). Temporal dark charge retains the exact temporal event identity without a stationarity ID. The current path models **pre-compensation thermally generated electrons** in e⁻/s and integrates that rate over the exact local exposure duration already established by the temporal pipeline.
2998
+ Dark current is signal the sensor generates by heat alone, even without light. Use `calculateSensorDarkCurrentCharge()` with either a stationary EQE expected-count exposure result, or the validated compact [temporal photo signal](https://github.com/Photivra/photivra/blob/main/docs/TEMPORAL_PHOTO_RAW.md).
2999
+
3000
+ - Temporal dark charge keeps the exact temporal event identity, without a stationarity ID.
3001
+ - The model is **thermally generated electrons before any compensation**, in e⁻/s. It integrates that rate over the exact local exposure duration already set by the temporal pipeline.
2834
3002
 
2835
- Photivra deliberately does not impose one universal dark-current temperature law. A dark-current profile may provide either:
3003
+ Photivra deliberately does not impose one universal law for how dark current changes with temperature. A dark-current profile may provide either:
2836
3004
 
2837
3005
  - one exact reference temperature and one dark-current rate; or
2838
3006
  - a strictly increasing measured temperature table with piecewise-linear interpolation and fail-closed behavior outside the measured range.
2839
3007
 
2840
- A population-mean dark-current profile is allowed only as an explicit approximation and must not claim pixel/site dark-current nonuniformity is modeled. Exact-site profiles remain bound to the exact color-sampling site.
3008
+ A population-mean dark-current profile (one average for all sites) is allowed only as an explicit approximation. It must not claim that site-to-site dark-current variation is modelled. Exact-site profiles stay bound to the exact colour-sampling site.
2841
3009
 
2842
- Dark-current compensation, black-level offset, hot-pixel/defect excess, leakage, charge injection and other charge sources are separate. The result is an **expected dark-electron count** only; no Poisson sampling or dark-current shot-noise realization is performed.
3010
+ Dark-current compensation, black-level offset, hot-pixel/defect excess, leakage, charge injection and other charge sources are separate. The result is only an **expected dark-electron count**. No Poisson sampling, and no dark-current shot noise, is applied.
2843
3011
 
2844
- This stage still does not authorize physical full-well assessment because the accumulated sensor charge is not yet complete. It also does not apply to the A/W current-domain path: integrated detector-terminal charge must not be reinterpreted as stored sensor electrons without a separate carrier/storage mapping.
3012
+ This stage still does not allow a physical full-well assessment, because the accumulated sensor charge is not complete yet. It also does not apply to the A/W current path: integrated charge at the detector terminal must not be reinterpreted as stored sensor electrons without a separate carrier/storage mapping.
2845
3013
 
2846
3014
  ## Accumulated-charge completeness
2847
3015
 
2848
- Use `composeSensorAccumulatedCharge()` after an EQE photo-signal exposure and its matching dark-current result exist for the **same exact local exposure event**.
3016
+ This step adds up everything stored in a pixel during one exposure. Use `composeSensorAccumulatedCharge()` once you have an EQE photo-signal exposure and its matching dark-current result for the **same exact local exposure event**.
2849
3017
 
2850
- The composer requires photo and dark terms to share:
3018
+ The photo and dark terms must share:
2851
3019
 
2852
3020
  - color topology/channel/site;
2853
3021
  - local-exposure binding ID;
@@ -2855,11 +3023,11 @@ The composer requires photo and dark terms to share:
2855
3023
  - exact start/end offsets in the `first-opening-boundary-phase` time basis;
2856
3024
  - the same local exposure duration.
2857
3025
 
2858
- Equal duration alone is insufficient.
3026
+ An equal duration alone is not enough.
2859
3027
 
2860
- Additional stored-electron components may represent explicitly modeled defect/hot-pixel excess, leakage, charge injection, clock-induced charge, or another identified source. Every component must declare the accounting meaning `incremental-stored-electrons-beyond-photo-and-modeled-dark-current`. This prevents an additional defect/leakage model from silently re-counting electrons already represented by the dark-current term.
3028
+ Extra stored-electron components may represent explicitly modelled defect/hot-pixel excess, leakage, charge injection, clock-induced charge or another identified source. Every component must declare the accounting meaning `incremental-stored-electrons-beyond-photo-and-modeled-dark-current`. This stops an extra defect or leakage model from silently counting electrons that the dark-current term already includes.
2861
3029
 
2862
- A separate evidence-backed `SensorAccumulatedChargeCompletenessProfile` lists the exact additional-component IDs and states that all material stored-electron contributors relevant to physical charge-storage capacity are accounted for. An empty additional-component list is therefore **not itself evidence of completeness**.
3030
+ A separate, evidence-backed `SensorAccumulatedChargeCompletenessProfile` lists the exact extra-component IDs, and states that every material contributor of stored electrons, relevant to physical charge-storage capacity, is accounted for. An empty list of extra components is therefore **not, by itself, evidence of completeness**.
2863
3031
 
2864
3032
  When all bindings and the completeness declaration pass, the result reports:
2865
3033
 
@@ -2868,15 +3036,15 @@ When all bindings and the completeness declaration pass, the result reports:
2868
3036
  - expected additional stored electrons;
2869
3037
  - total expected stored electrons.
2870
3038
 
2871
- All remain expectation values. No photo/dark shot-noise realization or integer carrier sampling is performed.
3039
+ All of these are expected values. No photo or dark shot noise is sampled, and no whole-number carrier counts are drawn.
2872
3040
 
2873
- Only this complete stored-electron result sets `physicalFullWellAssessmentAuthorized: true`. Camera/digital saturation remains separately unauthorized because camera saturation capacity is not the same physical quantity as full-well charge storage.
3041
+ Only this complete stored-electron result sets `physicalFullWellAssessmentAuthorized: true`. Camera/digital saturation stays unauthorized here, because camera saturation capacity is not the same physical quantity as full-well charge storage.
2874
3042
 
2875
- A/W photocharge remains outside this composition because detector-terminal coulombs are not automatically the number of electrons stored in the pixel charge well.
3043
+ A/W photocharge stays outside this composition, because coulombs at the detector terminal are not automatically the number of electrons stored in the pixel's charge well.
2876
3044
 
2877
3045
  ## Physical charge-capacity
2878
3046
 
2879
- Use `assessSensorPhysicalChargeCapacity()` only after accumulated-charge completeness has explicitly authorized physical capacity assessment.
3047
+ Full-well capacity is the most charge a pixel can physically store. Use `assessSensorPhysicalChargeCapacity()` only after accumulated-charge completeness has explicitly authorized a physical capacity assessment.
2880
3048
 
2881
3049
  A `SensorPhysicalChargeCapacityProfile` describes a sourced physical charge-storage limit in **electrons**. The profile is bound to:
2882
3050
 
@@ -2885,7 +3053,7 @@ A `SensorPhysicalChargeCapacityProfile` describes a sourced physical charge-stor
2885
3053
  - an evidence-backed operating-state identifier;
2886
3054
  - exact reference temperature, or an explicit approximation stating temperature dependence is not modeled.
2887
3055
 
2888
- The operating-state identifier is deliberately opaque. Photivra does not infer the correct physical storage capacity from ISO, gain labels, capture-mode names, pixel pitch, or another nearby specification.
3056
+ The operating-state identifier is deliberately opaque. Photivra does not guess the physical storage capacity from ISO, gain labels, capture-mode names, pixel pitch or any other nearby specification.
2889
3057
 
2890
3058
  The assessment compares **total expected stored electrons** with the physical capacity and reports:
2891
3059
 
@@ -2894,15 +3062,15 @@ The assessment compares **total expected stored electrons** with the physical ca
2894
3062
  - `below-capacity`, `at-capacity`, or `above-capacity`;
2895
3063
  - whether the unsaturated expected charge exceeds the declared capacity.
2896
3064
 
2897
- This is an expectation comparison, not a stochastic saturation realization. Expected charge below capacity does not prove that every noisy realization remains below capacity. Expected charge above capacity means the unsaturated linear expectation exceeds the physical storage limit.
3065
+ This compares expected values; it does not simulate random saturation. Expected charge below capacity does not prove that every noisy realization stays below capacity. Expected charge above capacity means the linear expectation, before any saturation, exceeds the physical storage limit.
2898
3066
 
2899
- Photivra does **not** clamp the charge at capacity. It does not calculate post-saturation stored charge, excess/overflow charge, recombination, anti-blooming drain behavior, or neighbor blooming. Those require a separate nonlinear charge-transfer model.
3067
+ Photivra does **not** clamp the charge at capacity. It does not calculate stored charge after saturation, overflow charge, recombination, anti-blooming drain behaviour or blooming into neighbours. Those need a separate nonlinear charge-transfer model.
2900
3068
 
2901
- Physical charge capacity is also **not camera saturation capacity**. Camera/analog/digital clipping can occur at a lower signal level, so camera saturation remains a separate downstream contract.
3069
+ Physical charge capacity is also **not camera saturation capacity**. Clipping in the camera's analog or digital chain can happen at a lower signal level, so camera saturation is a separate contract (next section).
2902
3070
 
2903
3071
  ## Camera saturation capacity
2904
3072
 
2905
- Use `assessSensorCameraSaturationCapacity()` for a source-specific **camera response-chain saturation capacity** expressed as dark-corrected photo-generated electron-equivalent signal.
3073
+ Use `assessSensorCameraSaturationCapacity()` for a **saturation capacity of the camera's response chain**, taken from a specific source and expressed as dark-corrected photo signal in electron equivalents.
2906
3074
 
2907
3075
  This contract intentionally uses a different signal domain from physical charge storage:
2908
3076
 
@@ -2910,11 +3078,11 @@ This contract intentionally uses a different signal domain from physical charge
2910
3078
  - total photo + dark + other stored charge is retained only as a diagnostic;
2911
3079
  - total stored charge is **not** added to the camera-signal capacity comparison.
2912
3080
 
2913
- That separation matches camera-characterization practice in which saturation capacity is derived from the camera's saturated photo response and can be lower than physical full-well because a downstream response stage may clip first.
3081
+ That separation follows camera-characterization practice. There, saturation capacity is derived from the camera's saturated photo response, and can be lower than physical full well because a later stage may clip first.
2914
3082
 
2915
- A camera saturation profile is bound to the exact color/channel, site applicability, operating-state identifier, temperature applicability, measurement method, uncertainty, and evidence. A channel-population mean used for one site remains an approximation.
3083
+ A camera saturation profile is bound to the exact colour/channel, site applicability, operating-state identifier, temperature applicability, measurement method, uncertainty and evidence. A mean over a channel's population used for one site stays an approximation.
2916
3084
 
2917
- The assessment reports photo-signal/capacity ratio, headroom, and below/at/above-capacity status. It does not:
3085
+ The assessment reports the photo-signal/capacity ratio, the headroom, and a below/at/above-capacity status. It does not:
2918
3086
 
2919
3087
  - identify which analog or digital stage limits the response;
2920
3088
  - infer an ADC maximum code or bit-depth clipping threshold;
@@ -2924,23 +3092,23 @@ The assessment reports photo-signal/capacity ratio, headroom, and below/at/above
2924
3092
  - calculate a post-saturation transfer curve;
2925
3093
  - establish stochastic saturation probability.
2926
3094
 
2927
- Stage-specific analog clipping, ADC/digital code limits, and RAW-code behavior require separate evidence-backed response-chain contracts.
3095
+ Analog clipping at a specific stage, ADC/digital code limits and RAW-code behaviour need separate, evidence-backed response-chain contracts.
2928
3096
 
2929
3097
  ## Stochastic charge, conversion, ADC, and RAW code
2930
3098
 
2931
- Use `simulateSensorChargeRealization()` only after the existing accumulated-charge completeness contract has produced complete expected stored charge for one exact sensor site/exposure.
3099
+ This is where random noise enters, and where charge becomes a RAW number. Use `simulateSensorChargeRealization()` only after the accumulated-charge completeness contract has produced the complete expected stored charge for one exact sensor site and exposure.
2932
3100
 
2933
- The first stochastic layer keeps expectation values separate from realizations:
3101
+ The first random (stochastic) layer keeps expected values separate from realizations (random draws):
2934
3102
 
2935
3103
  - photo-generated electrons use an explicit Poisson model;
2936
3104
  - dark-generated electrons use an explicit Poisson model;
2937
3105
  - each additional stored-charge component must declare its own Poisson or deterministic electron-equivalent policy;
2938
- - the realization uses an explicit deterministic 32-bit seed;
3106
+ - the realization uses an explicit deterministic 32-bit seed, scrambled by the MurmurHash3 `fmix32` finalizer so nearby seeds give unrelated streams;
2939
3107
  - electronic read noise is still downstream.
2940
3108
 
2941
3109
  The stochastic realization does not change the upstream expected charge or its provenance.
2942
3110
 
2943
- Use `parseSensorReadoutConversionProfile()` and `resolveSensorReadoutRegime()` for the downstream electronic chain. A regime declares:
3111
+ Use `parseSensorReadoutConversionProfile()` and `resolveSensorReadoutRegime()` for the electronic chain that follows. A regime declares:
2944
3112
 
2945
3113
  - system conversion gain in electrons per code;
2946
3114
  - explicit input-referred electronic read-noise components in RMS electrons;
@@ -2954,9 +3122,11 @@ Regime selection is explicitly:
2954
3122
 
2955
3123
  `explicit-upstream-camera-state-not-inferred-from-iso`
2956
3124
 
2957
- ISO therefore does not silently choose conversion gain, read noise, or ADC behavior. #7 maps an explicit camera ISO/capture-mode state to a documented #14 regime through the separate generic ISO signal-chain contract; the sensor readout contract itself still does not infer that mapping from the ISO number.
3125
+ So ISO never silently chooses conversion gain, read noise or ADC behaviour. The separate [generic ISO signal-chain contract](#generic-high-iso-signal-chain-profiles) (#7) maps an explicit camera ISO/capture-mode state to a documented readout regime (#14). The sensor readout contract itself still does not infer that mapping from the ISO number.
3126
+
3127
+ `calculateExpectedSensorReadout()` stays an expected-value path. It reports zero-mean read noise and does not quantize. If the upstream expected stored charge is already above physical storage capacity, it fails closed, because the physical-capacity assessment does not define a linear charge value after saturation.
2958
3128
 
2959
- `calculateExpectedSensorReadout()` remains an expectation path: it reports zero-mean read noise and does not quantize. If the upstream expected stored charge is already above physical storage capacity, it fails closed because the physical-capacity assessment does not define a linear post-saturation charge value.
3129
+ Both readout paths require the physical-capacity assessment to have been computed for the same charge. Besides a matching identity, its total expected stored electrons must equal the charge's total exactly (the assessment copies that total verbatim). A stale assessment therefore fails closed. On both the expected and the stochastic path, digital saturation is reported when the code reaches (`>=`) the digital saturation code, so a sample quantized exactly to the white level is flagged.
2960
3130
 
2961
3131
  `simulateSensorRawCode()` consumes a stochastic charge realization plus the matching physical-capacity assessment. Its stage order is explicit:
2962
3132
 
@@ -2972,15 +3142,58 @@ realized stored charge
2972
3142
  → RAW code
2973
3143
  ```
2974
3144
 
2975
- The physical storage clamp is a bounded scalar approximation only. Overflow is reported diagnostically, while `bloomingModeled` remains false because neighbor topology, charge transport, and anti-blooming behavior are not modeled.
3145
+ The physical storage clamp is only a bounded scalar approximation. Overflow is reported as a diagnostic. `bloomingModeled` stays false, because neighbour topology, charge transport and anti-blooming behaviour are not modelled.
3146
+
3147
+ Physical storage capacity, pre-ADC saturation and digital/ADC saturation are independent thresholds, and may disagree.
3148
+
3149
+ Quantization rounds to the nearest code, with exact ties going towards +∞. A regime may also declare:
2976
3150
 
2977
- Physical storage capacity, pre-ADC saturation, and digital/ADC saturation are independent thresholds and may disagree.
3151
+ - `adc.digitalGain` (≥ 1), applied around black after quantization and then re-quantized;
3152
+ - `adc.integralNonLinearity`, a piecewise-linear INL (integral non-linearity) in codes.
3153
+
3154
+ ### ISO-dependent gain regimes
3155
+
3156
+ `createIsoGainReadoutRegimes()` derives one explicit regime per ISO setting from declared sensor parameters. ISO then
3157
+ changes RAW brightness the way analog/digital gain does (exposure index of CIPA DC-004; EMVA 1288 linear model):
3158
+
3159
+ ```ts
3160
+ import { createIsoGainReadoutRegimes, parseSensorReadoutConversionProfile } from "@photivra/engine";
3161
+
3162
+ const evidence = [{ sourceOrigin: "photivra", sourceReference: "my-sensor-design", reuseStatus: "photivra-owned" }] as const;
3163
+ const { regimes, standardRegimeBands } = createIsoGainReadoutRegimes({
3164
+ regimeIdPrefix: "", scientificStatus: "approximation", baseExposureIndex: 100,
3165
+ exposureIndices: [100, 200, 400, 800, 1600, 3200],
3166
+ baseSaturationElectronEquivalent: { value: 30000, evidence }, // electrons at ADC white, ISO 100
3167
+ maximumAnalogGain: { value: 16, evidence }, // ISO 3200 adds 2x digital gain
3168
+ adc: { bitDepth: 14, blackLevelCode: 1024, digitalSaturationCode: 16383 },
3169
+ conversionGainModes: [
3170
+ { mode: "low-conversion-gain", minimumExposureIndex: 100, readNoiseRmsElectrons: { value: 3, evidence } },
3171
+ { mode: "high-conversion-gain", minimumExposureIndex: 800, readNoiseRmsElectrons: { value: 1.2, evidence } }
3172
+ ],
3173
+ evidence, limitations: ["Illustrative design values, not a measured camera."]
3174
+ });
3175
+ const readoutProfile = parseSensorReadoutConversionProfile({ schemaVersion: "0.1.0", profileId: "my-readout",
3176
+ colorSamplingProfileId: "my-cfa", channelId: "green",
3177
+ regimeSelectionOwnedBy: "explicit-upstream-camera-state-not-inferred-from-iso", regimes, evidence });
3178
+ // standardRegimeBands binds each ISO to its regime in a GenericIsoSignalChainProfile.
3179
+ ```
3180
+
3181
+ ### Fixed-pattern noise
3182
+
3183
+ Fixed-pattern noise is the part of sensor noise that is the same in every frame from one sensor.
3184
+ `parseSensorFixedPatternNoiseProfile()` declares an opt-in pattern for one sensor instance: PRNU and DSNU (pixel-to-pixel
3185
+ variation in light response and in dark signal), hot pixels, and row and column offsets.
3186
+
3187
+ - The pattern is seeded by `sensorInstanceSeedUint32`, independent of the capture noise seed.
3188
+ - Pass the same profile as `fixedPatternNoise` to `simulateSensorChargeRealization()` and `simulateSensorRawCode()`, or on every producer site.
3189
+ - `calculateSensorFixedPatternSite()` returns the deterministic values for one colour-sampling site.
3190
+ - The generic tier presets carry a synthetic profile in `body.fixedPatternNoise`.
2978
3191
 
2979
3192
  ## RAW capture samples and reconstruction
2980
3193
 
2981
- Use `createSensorRawCaptureSample()` to bind one generated RAW code to the existing capture-mode/color-sampling structural contract.
3194
+ Use `createSensorRawCaptureSample()` to bind one generated RAW code to the existing capture-mode / colour-sampling structure.
2982
3195
 
2983
- Schema 0.1.0 intentionally permits only an ungrouped native-effective capture-mode sample that maps to exactly one color-sampling site. Grouped/binning/remosaic modes fail closed because structural contributor counts do not define signal-combination weights or analog/digital combination math.
3196
+ Schema 0.1.0 deliberately allows only an ungrouped native-effective capture-mode sample that maps to exactly one colour-sampling site. Grouped, binning and remosaic modes fail closed, because knowing how many sites contribute does not define the weights or the analog/digital maths for combining them.
2984
3197
 
2985
3198
  The RAW sample preserves:
2986
3199
 
@@ -2992,11 +3205,11 @@ The RAW sample preserves:
2992
3205
  - charge/read-noise seeds;
2993
3206
  - physical/digital saturation diagnostics.
2994
3207
 
2995
- Physical orientation and output rotation remain downstream, so CFA phase is never reset by portrait orientation or an active crop.
3208
+ Physical orientation and output rotation come later, so CFA phase is never reset by portrait orientation or an active crop.
2996
3209
 
2997
- Use `parseSensorRawReconstructionProfile()` and `resolveSensorRawReconstruction()` for the first reconstruction contract. The profile is tied to one exact capture-mode ID and color-sampling profile ID and declares explicit native-neighborhood linear weights per output channel.
3210
+ Use `parseSensorRawReconstructionProfile()` and `resolveSensorRawReconstruction()` for the first reconstruction contract (turning mosaic samples into full-colour pixels). The profile is tied to one exact capture-mode ID and colour-sampling profile ID. It declares explicit linear weights over the native neighbourhood, per output channel.
2998
3211
 
2999
- Every input RAW sample is re-resolved against the declared color-sampling topology. A stored channel label that disagrees with the CFA at that native site fails closed.
3212
+ Every input RAW sample is re-checked against the declared colour-sampling topology. A stored channel label that disagrees with the CFA at that native site fails closed.
3000
3213
 
3001
3214
  The first reconstruction contract:
3002
3215
 
@@ -3006,11 +3219,65 @@ The first reconstruction contract:
3006
3219
  - does not infer branded Bayer/X-Trans/remosaic behavior from a topology label;
3007
3220
  - reports `aliasingModeled: false` and `moireModeled: false`.
3008
3221
 
3009
- Aliasing/moiré claims require an adequate pre-sampling optical/scene spatial-frequency model; reconstruction artifacts alone are not treated as proof that that physical aliasing model exists.
3222
+ Claims about aliasing or moiré need an adequate model of the spatial frequencies in the scene and optics before sampling. Reconstruction artefacts alone are not treated as proof that such a physical aliasing model exists.
3223
+
3224
+ ## RAW development, white level, highlights and tone curves
3225
+
3226
+ RAW samples also carry:
3227
+
3228
+ - `effectiveSaturationCode` — the code at which the site is fully saturated (see `calculateEffectiveSaturationCode`);
3229
+ - `systemConversionGainElectronsPerCode`;
3230
+ - `readNoiseRmsElectrons`.
3231
+
3232
+ Packed native RAW carries `effectiveSaturationCodes` once per CFA phase.
3233
+
3234
+ Development ("developing" a RAW into a viewable image):
3235
+
3236
+ - normalizes to that white level;
3237
+ - clips saturated or flagged sites before white balance;
3238
+ - applies white balance once, for any invertible camera matrix;
3239
+ - keeps clipped highlights neutral.
3240
+
3241
+ See [photographic export](https://github.com/Photivra/photivra/blob/main/docs/PHOTOGRAPHIC_EXPORT.md) for the equations.
3242
+
3243
+ ```ts
3244
+ import { calculateEffectiveSaturationCode, createNativeRawDevelopmentTask, type NativeRawDevelopmentInput,
3245
+ type SdrRenderingProfile } from "@photivra/engine";
3246
+
3247
+ // A 20,000 e⁻ well at 1 e⁻/DN over a 512 pedestal saturates at code 20512, not at 65535.
3248
+ const white = calculateEffectiveSaturationCode({ physicalChargeCapacityElectrons: 20000,
3249
+ preAdcSaturationElectronEquivalent: 30000, systemConversionGainElectronsPerCode: 1,
3250
+ blackLevelCode: 512, digitalSaturationCode: 65535 });
3251
+ if (white !== 20512) throw new Error("unexpected white level");
3252
+
3253
+ // Luminance tone curves preserve chromaticity; existing curve ids are unchanged.
3254
+ const filmic: SdrRenderingProfile = { schemaVersion: "0.1.0", profileId: "filmic", profileVersion: "1",
3255
+ renderingExposureEv: 0, toneCurve: "naka-rushton-luminance",
3256
+ toneCurveParameters: { exponent: 1.2, semiSaturation: 0.5, whiteLuminance: 4 },
3257
+ gamutHandling: "clip-components", outputDynamicRange: "sdr", transferFunction: "srgb",
3258
+ bitDepth: 8, rounding: "nearest-ties-up", dither: "none" };
3259
+
3260
+ async function develop(input: NativeRawDevelopmentInput) {
3261
+ // The task takes ownership of input.raw (its arrays are detached); output.raw returns it.
3262
+ const task = createNativeRawDevelopmentTask({ ...input, rendering: filmic, highlightMode: "clip" }, {
3263
+ yieldControl: () => new Promise<void>((resolve) => setTimeout(resolve, 0)),
3264
+ onProgress: ({ completedTileCount, totalTileCount }) => void (completedTileCount / totalTileCount)
3265
+ });
3266
+ await task.run();
3267
+ const output = task.takeOutput();
3268
+ return { pixels: output.integerSamples, raw: output.raw, saturatedPixels: output.highlight.saturatedPixelCount };
3269
+ }
3270
+ void develop;
3271
+ ```
3272
+
3273
+ - `highlightMode: "reconstruct"` blends pixels above 0.8 of white towards neutral, instead of clipping
3274
+ each WB-scaled channel. It is an approximation, not a recovery of the clipped signal.
3275
+ - `extended-reinhard-luminance` takes `{ whiteLuminance }`, the luminance mapped to output white.
3276
+ - `createPhotographicExportPair(input, { signal })` accepts an optional `AbortSignal`.
3010
3277
 
3011
3278
  ## Capture-mode profiles
3012
3279
 
3013
- Use `parseCaptureModeProfile()` and `resolveCaptureMode()` to describe how one physical sensor can expose different acquisition/sampling/reconstruction modes without changing sensor identity:
3280
+ One physical sensor can offer several capture modes, such as pixel shift or binning. Use `parseCaptureModeProfile()` and `resolveCaptureMode()` to describe its acquisition, sampling and reconstruction modes, without changing the sensor's identity:
3014
3281
 
3015
3282
  ```ts
3016
3283
  import {
@@ -3106,7 +3373,7 @@ const selected = resolveCaptureMode({
3106
3373
  console.log(selected.processedImageMegapixels);
3107
3374
  ```
3108
3375
 
3109
- The model is deliberately **orthogonal**, not one mutually exclusive marketing-style mode enum. A mode independently describes:
3376
+ The model is deliberately **orthogonal**: it is not one list of mutually exclusive, marketing-style modes. A mode describes each of these independently:
3110
3377
 
3111
3378
  - acquisition as single-frame, fixed multi-frame, or variable multi-frame;
3112
3379
  - per-frame sampling as the native effective raster, exact integer grouping of native effective samples, or another explicitly evidenced effective raster;
@@ -3115,23 +3382,23 @@ The model is deliberately **orthogonal**, not one mutually exclusive marketing-s
3115
3382
  - the processed-image raster before final output crop/resampling;
3116
3383
  - explicit downstream model dependencies.
3117
3384
 
3118
- This lets a computational multi-frame mode also use grouped per-frame sampling instead of forcing it into one exclusive category.
3385
+ So a computational multi-frame mode can also use grouped per-frame sampling, instead of being forced into one exclusive category.
3119
3386
 
3120
- `NativeImageRaster` keeps its canonical meaning: an **effective native image-sampling grid**, not a physical photosite raster. Neither native megapixels nor processed/final-output megapixels may be used to infer physical photodiode count.
3387
+ `NativeImageRaster` keeps its canonical meaning: an **effective native image-sampling grid**, not a raster of physical photosites. Neither native megapixels nor processed/final-output megapixels may be used to infer the physical photodiode count.
3121
3388
 
3122
- For grouped sampling, Photivra derives the per-frame sampling raster only when the native dimensions divide exactly by the evidenced grouping factors. The optional `combinationDomain` distinguishes `charge-domain`, `pre-conversion-analog`, and `post-conversion-digital`. When that fact is not known it must be omitted; the word “binning” or a 2×2 resolution ratio does not establish where combination occurred.
3389
+ For grouped sampling, Photivra derives the per-frame sampling raster only when the native dimensions divide exactly by the evidenced grouping factors. The optional `combinationDomain` distinguishes `charge-domain`, `pre-conversion-analog` and `post-conversion-digital`. If that fact is not known, leave it out. The word "binning", or a 2×2 resolution ratio, does not show where the combination happened.
3123
3390
 
3124
- Inter-frame sensor offsets are expressed in units of the **native effective sampling pitch**. They are not evidence of physical photodiode pitch. The offset sequence length must match a fixed multi-frame count when supplied.
3391
+ Inter-frame sensor offsets are in units of the **native effective sampling pitch**. They are not evidence of the physical photodiode pitch. When supplied, the offset sequence length must match the fixed multi-frame count.
3125
3392
 
3126
- `processedImageRaster` belongs to the capture-mode/reconstruction pipeline. It does not change physical imaging area, crop factor, field of view, or the native effective raster. It may be larger than the native raster for a multi-frame reconstruction without implying a higher-resolution physical sensor.
3393
+ `processedImageRaster` belongs to the capture-mode/reconstruction pipeline. It does not change the physical imaging area, crop factor, field of view or native effective raster. It may be larger than the native raster for a multi-frame reconstruction, without implying a higher-resolution physical sensor.
3127
3394
 
3128
- Final output crop/resampling remains owned by the existing staged capture/output geometry contract. A processed raster therefore does not itself define final display/export dimensions or viewing FOV.
3395
+ The final output crop/resampling is still owned by the existing staged capture/output geometry contract. A processed raster therefore does not, by itself, define final display/export dimensions or the viewing FOV.
3129
3396
 
3130
- The capture-mode schema records dependencies such as `color-sampling-model`, `mode-specific-readout-timing`, `radiometric-calibration`, and `inter-frame-registration` without pretending those downstream models are already implemented.
3397
+ The capture-mode schema records dependencies such as `color-sampling-model`, `mode-specific-readout-timing`, `radiometric-calibration` and `inter-frame-registration`, without pretending those models are already implemented.
3131
3398
 
3132
3399
  ## Sensor readout timing
3133
3400
 
3134
- Use `calculateSensorReadoutTiming()` for a capture-specific native sensor scan schedule without conflating sensor readout with shutter-curtain timing:
3401
+ Use `calculateSensorReadoutTiming()` for the native sensor scan schedule of one capture. This is when data is read off the sensor, which is not the same as when the shutter curtains open and close:
3135
3402
 
3136
3403
  ```ts
3137
3404
  import {
@@ -3194,19 +3461,25 @@ const orientedDirection = transformNativeRasterVectorToOriented({
3194
3461
  });
3195
3462
  ```
3196
3463
 
3197
- The two timing facts are intentionally separate. `captureReadoutDurationSeconds` records the selected capture's declared data-readout duration; `spatialSamplingSkewSeconds` drives only the spatial phase span of the current rolling approximation. Photivra does not assume those values are equal.
3464
+ The two timing facts are deliberately separate, and Photivra does not assume they are equal:
3465
+
3466
+ - `captureReadoutDurationSeconds` is the declared time to read the data of the selected capture.
3467
+ - `spatialSamplingSkewSeconds` only sets how much the readout time shifts from one side of the frame to the other, in the current rolling approximation.
3198
3468
 
3199
- Global readout declares only the capture data-readout duration. Its spatial phase offset is zero everywhere even when that duration is non-zero.
3469
+ Global readout declares only the capture's data-readout duration. Its spatial phase offset is zero everywhere, even when that duration is not zero.
3200
3470
 
3201
- The rolling schedule is a `uniform-linear-single-axis` approximation in invariant native sensor coordinates. `NativeImageRaster` remains an effective image-sampling grid, so this API does not claim that each raster row/column corresponds to one physical photodiode row or hardware readout line. Non-uniform, segmented, center-out, interleaved and multi-tap readout patterns are not represented by this first model.
3471
+ The rolling schedule is a `uniform-linear-single-axis` approximation in the native sensor coordinates, which never change. `NativeImageRaster` is still an effective image-sampling grid, so this API does not claim that each raster row or column is one physical photodiode row or hardware readout line. This first model does not represent non-uniform, segmented, center-out, interleaved or multi-tap readout patterns.
3202
3472
 
3203
- Physical camera orientation is deliberately excluded from the timing input. Use the existing native↔oriented vector transform when a downstream renderer needs the scan direction in oriented capture coordinates. Output crop/resolution is also excluded and therefore cannot silently change native readout timing.
3473
+ - Physical camera orientation is deliberately left out of the timing input. If a renderer needs the scan direction in oriented capture coordinates, use the existing native ↔ oriented vector transform.
3474
+ - Output crop and resolution are also left out, so they cannot silently change native readout timing.
3475
+ - Timing for an active crop or capture mode must be supplied explicitly. The engine never scales full-frame timing from crop dimensions.
3476
+ - The shutter mechanism is recorded separately and does not change the schedule.
3204
3477
 
3205
- Active-crop/capture-mode timing must be supplied explicitly; the engine never scales full-frame timing from crop dimensions. Shutter mechanism is recorded independently and does not modify the schedule. The separate exposure-window, inverse-rotation/temporal, flash and illumination contracts supply bounded timing and integration. This readout primitive does not execute them or infer curtain acceleration, segmented timing or synchronization.
3478
+ The separate exposure-window, inverse-rotation/temporal, flash and illumination contracts provide bounded timing and integration. This readout primitive does not run them, and does not infer curtain acceleration, segmented timing or synchronization.
3206
3479
 
3207
3480
  ## Readout/exposure spatial linkage
3208
3481
 
3209
- Use `assessReadoutExposureTimingLinkage()` when a specific capture-mode source supports a relationship between rolling sensor readout spatial order and an **electronic** exposure boundary:
3482
+ Use `assessReadoutExposureTimingLinkage()` when a source for a specific capture mode supports a relationship between the spatial order of rolling sensor readout and an **electronic** exposure boundary:
3210
3483
 
3211
3484
  ```ts
3212
3485
  import { assessReadoutExposureTimingLinkage } from "@photivra/engine";
@@ -3329,26 +3602,26 @@ const relationship = assessReadoutExposureTimingLinkage({
3329
3602
  console.log(relationship.value.links[0]);
3330
3603
  ```
3331
3604
 
3332
- The positive link means only that the selected electronic exposure boundary shares the rolling readout's **normalized native spatial phase/order**:
3605
+ A positive link means only that the selected electronic exposure boundary moves across the sensor in the same **normalized native spatial order (phase)** as the rolling readout:
3333
3606
 
3334
3607
  - `same` means boundary phase equals readout phase;
3335
3608
  - `reversed` means boundary phase equals `1 - readout phase`.
3336
3609
 
3337
3610
  It does **not** mean the two events occur at the same absolute time.
3338
3611
 
3339
- The function therefore reports `absoluteTemporalAlignment: "not-established"` even when directions and first-to-last timing spans happen to match.
3612
+ So the function reports `absoluteTemporalAlignment: "not-established"`, even when the directions and the first-to-last timing spans happen to match.
3340
3613
 
3341
- The readout spatial skew and exposure-boundary traversal duration may differ. Their ratio is reported as `boundaryTraversalToReadoutSpatialSkewRatio`, which is descriptive only.
3614
+ The readout spatial skew and the exposure boundary's traversal time may differ. Their ratio is reported as `boundaryTraversalToReadoutSpatialSkewRatio`, for information only.
3342
3615
 
3343
- The total `captureReadoutDurationSeconds` remains a separate evidence-backed fact and does not participate in linkage validation.
3616
+ The total `captureReadoutDurationSeconds` is a separate evidence-backed fact, and plays no part in checking the linkage.
3344
3617
 
3345
- Only electronic boundaries can be spatial-phase-linked to sensor readout. A mechanical opening/closing boundary is rejected. Global readout has no rolling spatial phase and therefore cannot participate in this first linkage type.
3618
+ Only electronic boundaries can be linked to sensor readout in this way. A mechanical opening or closing boundary is rejected. Global readout has no rolling spatial phase, so it cannot take part in this first linkage type.
3346
3619
 
3347
- Use `linkage: { kind: "unlinked" }` when Photivra should assert no relationship. This means **no relationship is asserted**; it does not establish that the underlying hardware processes are physically independent.
3620
+ Use `linkage: { kind: "unlinked" }` when Photivra should assert no relationship. This means **no relationship is asserted**. It does not show that the underlying hardware processes are physically independent.
3348
3621
 
3349
3622
  ## Capture exposure-window timing
3350
3623
 
3351
- Use `calculateCaptureExposureWindows()` to describe when different native-sensor locations begin and end their local exposure without conflating shutter actuation with sensor readout:
3624
+ With a focal-plane shutter, different parts of the sensor start and stop collecting light at slightly different times. Use `calculateCaptureExposureWindows()` to describe when each native-sensor location begins and ends its local exposure. Shutter movement and sensor readout are kept separate:
3352
3625
 
3353
3626
  ```ts
3354
3627
  import { calculateCaptureExposureWindows } from "@photivra/engine";
@@ -3426,7 +3699,7 @@ console.log(windows.value.samples);
3426
3699
  console.log(windows.value.localExposureDurationRangeSeconds);
3427
3700
  ```
3428
3701
 
3429
- The time basis is `first-opening-boundary-phase`: zero means the first spatial phase of the declared opening boundary. This deliberately avoids pretending that a spatially scanned capture has one universal local exposure-start event.
3702
+ The time basis is `first-opening-boundary-phase`: time zero is when the declared opening boundary first starts to cross the sensor. This avoids pretending that a capture scanned across the sensor has one universal exposure-start moment.
3430
3703
 
3431
3704
  Opening and closing schedules are independent. Each may currently be either:
3432
3705
 
@@ -3441,11 +3714,11 @@ local end = nominal exposure duration + closing phase
3441
3714
  local duration = local end - local start
3442
3715
  ```
3443
3716
 
3444
- Matched opening/closing scans with equal traversal produce constant local exposure duration. Different directions or traversal durations can intentionally produce spatially varying local duration.
3717
+ Matched opening and closing scans with equal traversal times give the same local exposure duration everywhere. Different directions or traversal times can deliberately make the local duration vary across the sensor.
3445
3718
 
3446
- Photivra validates the **entire active rectangle**, not only supplied diagnostic points. Because the first schedules are affine over the native rectangle, corner extrema are sufficient to reject any declaration that would produce zero or negative local exposure duration somewhere in the capture.
3719
+ Photivra checks the **whole active rectangle**, not only the diagnostic points you supply. Because the first schedules are affine (linear plus offset) over the native rectangle, checking the corners is enough to reject any declaration that would give zero or negative local exposure duration anywhere in the capture.
3447
3720
 
3448
- `shutterMechanism` determines only the conceptual boundary actuators:
3721
+ `shutterMechanism` only says what conceptually opens and closes the exposure:
3449
3722
 
3450
3723
  - `mechanical` → mechanical opening + mechanical closing;
3451
3724
  - `electronic-first-curtain` → electronic opening + mechanical closing;
@@ -3453,15 +3726,15 @@ Photivra validates the **entire active rectangle**, not only supplied diagnostic
3453
3726
 
3454
3727
  The mechanism does not supply traversal direction or timing.
3455
3728
 
3456
- This exposure-window model remains separate from `calculateSensorReadoutTiming()`. A later integration layer must explicitly bind exposure boundaries, sensor readout, and time-parameterized motion; Photivra does not assume a sensor readout phase is an exposure-start or exposure-end boundary.
3729
+ This exposure-window model stays separate from `calculateSensorReadoutTiming()`. A later integration layer must explicitly bind exposure boundaries, sensor readout and time-parameterized motion. Photivra does not assume that a sensor readout phase is an exposure-start or exposure-end boundary.
3457
3730
 
3458
- Physical orientation and digital output geometry remain downstream. The schedule stays in invariant native sensor coordinates and can use the existing native↔oriented vector transforms when a consumer needs presentation-oriented direction.
3731
+ Physical orientation and digital output geometry come later. The schedule stays in native sensor coordinates. Use the existing native ↔ oriented vector transforms when you need the direction as displayed.
3459
3732
 
3460
- The first model does not cover curtain acceleration, curved/nonlinear curtain travel, segmented/center-out/interleaved electronic schedules, flash/flicker interaction, shutter shock, EFCS-specific pupil/bokeh effects, or rolling-shutter image distortion.
3733
+ The first model does not cover curtain acceleration, curved or nonlinear curtain travel, segmented/center-out/interleaved electronic schedules, flash/flicker interaction, shutter shock, pupil/bokeh effects specific to EFCS (electronic first-curtain shutter), or rolling-shutter image distortion.
3461
3734
 
3462
3735
  ## Capture-mode-bound shutter/readout timing
3463
3736
 
3464
- Use `parseCaptureModeTimingProfile()` and `resolveCaptureModeTiming()` when exposure/readout timing evidence applies to one exact resolved capture mode.
3737
+ Use `parseCaptureModeTimingProfile()` and `resolveCaptureModeTiming()` when your exposure/readout timing evidence applies to one exact resolved capture mode.
3465
3738
 
3466
3739
  The profile binds:
3467
3740
 
@@ -3477,23 +3750,23 @@ Schema `0.1.0` supports only the timing families already modeled by Photivra:
3477
3750
  - simultaneous/global timing; and
3478
3751
  - uniform-linear native-sensor scans.
3479
3752
 
3480
- Segmented, center-out, interleaved or other non-uniform schedules fail closed rather than being silently approximated.
3753
+ Segmented, center-out, interleaved or other non-uniform schedules fail closed, rather than being silently approximated.
3481
3754
 
3482
- The resolved result carries both authoritative local exposure windows and sensor data-readout timing, but still reports:
3755
+ The resolved result carries both the authoritative local exposure windows and the sensor data-readout timing, but still reports:
3483
3756
 
3484
3757
  `readoutExposureSynchronization: "not-assumed"`
3485
3758
 
3486
- A shared mode/profile identity does not make readout phase an exposure-start or exposure-end clock.
3759
+ Sharing a mode/profile identity does not make the readout phase an exposure-start or exposure-end clock.
3487
3760
 
3488
- Changing only a processed/output raster does not alter this timing. Timing changes only when the exact capture mode/profile or explicit timing facts change.
3761
+ Changing only a processed/output raster does not change this timing. Timing changes only when the exact capture mode/profile, or the explicit timing facts, change.
3489
3762
 
3490
3763
  ## Depth-aware translation/parallax temporal geometry
3491
3764
 
3492
- Use `calculateCaptureTranslationParallaxTemporalQuadrature()` for camera translation/parallax over the exact local exposure windows resolved above.
3765
+ When the camera moves sideways (translates) during an exposure, near objects shift more in the image than far ones — this is parallax. Use `calculateCaptureTranslationParallaxTemporalQuadrature()` for camera translation and parallax over the exact local exposure windows resolved above.
3493
3766
 
3494
- Unlike pure camera rotation, translation is **depth dependent**. Every scene sample therefore supplies a metric camera-space position with positive `z`.
3767
+ Unlike pure camera rotation, translation **depends on depth**. Every scene sample therefore gives a metric camera-space position with positive `z`.
3495
3768
 
3496
- The model evaluates each scene point independently at deterministic midpoint nodes:
3769
+ The model evaluates each scene point on its own, at deterministic midpoint times:
3497
3770
 
3498
3771
  ```text
3499
3772
  relative scene position(t)
@@ -3501,29 +3774,27 @@ relative scene position(t)
3501
3774
  + (subject velocity - camera translation velocity) * t
3502
3775
  ```
3503
3776
 
3504
- This keeps camera motion and subject motion separate while allowing them to coexist.
3777
+ This keeps camera motion and subject motion separate, while letting both happen at once.
3505
3778
 
3506
- Important boundaries:
3779
+ Important limits:
3507
3780
 
3508
- - metric scene depth is required;
3509
- - one global homography/warp is not authorized for arbitrary 3D translation;
3510
- - orientation changes the reported native/oriented sample vector, not the underlying physical projection;
3511
- - camera rotation is still a separate model;
3512
- - visibility, occlusion/disocclusion, deformation and acceleration remain downstream/future responsibilities;
3513
- - sensor data-readout timing is not used as exposure timing.
3781
+ - Metric scene depth is required.
3782
+ - One global homography (a single 2D warp) is not allowed for arbitrary 3D translation.
3783
+ - Orientation changes the reported native/oriented sample vector, not the underlying physical projection.
3784
+ - Camera rotation is still a separate model.
3785
+ - Visibility, occlusion and disocclusion, deformation and acceleration are left to later stages or future work.
3786
+ - Sensor data-readout timing is not used as exposure timing.
3514
3787
 
3515
- Two scene points at different depths under the same lateral camera translation therefore produce different image-plane displacement.
3788
+ So two scene points at different depths, under the same sideways camera translation, move by different amounts in the image.
3516
3789
 
3517
3790
  ## Temporal scene-radiance sampling
3518
3791
 
3519
- Use `createSceneRadianceTemporalSamplingPlan()` to generate deterministic provider/renderer evaluation times over one committed local exposure window.
3792
+ Use `createSceneRadianceTemporalSamplingPlan()` to generate deterministic times at which a provider or renderer should evaluate the scene, over one committed local exposure window.
3520
3793
 
3521
- The sampling plan uses the same authoritative:
3794
+ The sampling plan uses the same authoritative time basis as the [exposure-window contract](#capture-exposure-window-timing) (#12):
3522
3795
 
3523
3796
  `first-opening-boundary-phase`
3524
3797
 
3525
- time basis as the #12 exposure-window contract.
3526
-
3527
3798
  Each node carries:
3528
3799
 
3529
3800
  - deterministic node/result identity;
@@ -3532,20 +3803,20 @@ Each node carries:
3532
3803
  - normalized averaging weight;
3533
3804
  - seconds-valued integration measure.
3534
3805
 
3535
- A scene-radiance provider evaluates the scene/source state at those node times. Then `reduceSceneRadianceTemporalSamples()` validates that every returned result matches the exact node, provider, scene, wavelength and time identity before integrating.
3806
+ A scene-radiance provider evaluates the scene and source state at those times. Then `reduceSceneRadianceTemporalSamples()` checks that every returned result matches the exact node, provider, scene, wavelength and time identity, before integrating.
3536
3807
 
3537
3808
  The reducer reports both:
3538
3809
 
3539
3810
  - average outgoing spectral radiance; and
3540
3811
  - time-integrated spectral radiance.
3541
3812
 
3542
- Per-node evidence and uncertainty are retained. Aggregate numeric uncertainty is explicitly `not-propagated` because the engine does not invent temporal independence/correlation assumptions.
3813
+ Per-node evidence and uncertainty are kept. The combined numeric uncertainty is explicitly `not-propagated`, because the engine does not invent assumptions about whether samples at different times are independent or correlated.
3543
3814
 
3544
- This is the bridge required for physically timed flicker/flash/source variation. It does not use sensor readout phase as an exposure-time surrogate and does not itself implement visibility, material transport or a renderer.
3815
+ This is the bridge needed for physically timed flicker, flash and other changes in light sources. It does not use sensor readout phase as a stand-in for exposure time, and it does not itself implement visibility, material transport or a renderer.
3545
3816
 
3546
3817
  ## Manual flash and ordinary sync
3547
3818
 
3548
- The #106 foundation models a manual flash as **time-varying scene illumination**, not a post-render brightness control.
3819
+ The manual-flash foundation (#106) models a flash as **scene illumination that changes over time**, not as a brightness control applied after rendering.
3549
3820
 
3550
3821
  The flow is:
3551
3822
 
@@ -3558,58 +3829,58 @@ manual flash source geometry + spectrum + relative output
3558
3829
  -> optics and sensor integration
3559
3830
  ```
3560
3831
 
3561
- Use `parseManualFlashProfile()` for one generic manual flash source and `parseFlashSyncCapabilityProfile()` for the exact capture-mode/timing profile that permits ordinary synchronization.
3832
+ (In the flow above, #12 is the [exposure-window timing](#capture-exposure-window-timing) and #85 is the [scene-radiance provider](#scene-radiance-provider-and-material-response) with its [temporal illumination](#temporal-illumination).)
3833
+
3834
+ Use `parseManualFlashProfile()` for one generic manual flash source, and `parseFlashSyncCapabilityProfile()` for the exact capture-mode/timing profile that allows ordinary synchronization.
3562
3835
 
3563
3836
  A manual flash profile declares:
3564
3837
 
3565
- - stable source identity;
3566
- - #85 point/spot/area/etc. geometry;
3567
- - source spectrum;
3568
- - relative-linear output scale;
3838
+ - a stable source identity;
3839
+ - source geometry (point, spot, area and so on), as in [scene illumination source profiles](#scene-illumination-source-profiles);
3840
+ - the source spectrum;
3841
+ - a relative, linear output scale;
3569
3842
  - explicit piecewise-linear pulse support and shape;
3570
- - provenance/limitations.
3843
+ - provenance and limitations.
3571
3844
 
3572
- Schema 0.1.0 uses relative source magnitude only. It does not claim a calibrated guide number, joule value, branded flash range, or manufacturer-specific pulse curve.
3845
+ Schema 0.1.0 uses relative source magnitude only. It does not claim a calibrated guide number, joule value, branded flash range or manufacturer-specific pulse curve.
3573
3846
 
3574
3847
  ### Front- and rear-curtain timing
3575
3848
 
3576
- Call `resolveManualFlashSync()` with the resolved #12 capture-mode timing.
3849
+ Call `resolveManualFlashSync()` with the resolved capture-mode timing.
3577
3850
 
3578
- For ordinary one-pulse flash, Photivra derives the interval during which **the entire active frame is simultaneously exposed**:
3851
+ For an ordinary single-pulse flash, Photivra works out the interval during which **the whole active frame is exposed at the same time**:
3579
3852
 
3580
3853
  ```text
3581
3854
  whole-frame-open start = latest opening-boundary phase
3582
3855
  whole-frame-open end = earliest closing-boundary phase
3583
3856
  ```
3584
3857
 
3585
- For a uniform focal-plane opening scan, the latest opening is the opening traversal duration. The earliest closing remains the nominal closing reference. If the interval is zero/negative, ordinary flash cannot uniformly light the full active frame.
3858
+ For a uniform focal-plane opening scan, the latest opening is the opening traversal time. The earliest closing stays at the nominal closing reference. If the interval is zero or negative, an ordinary flash cannot light the whole active frame evenly.
3586
3859
 
3587
- Front sync places the pulse at the beginning of this valid interval. Rear sync places it at the end:
3860
+ Front-curtain sync puts the pulse at the start of this interval. Rear-curtain sync puts it at the end:
3588
3861
 
3589
3862
  ```text
3590
3863
  front pulse start = whole-frame-open start
3591
3864
  rear pulse start = whole-frame-open end - pulse support duration
3592
3865
  ```
3593
3866
 
3594
- The full pulse support must fit inside the interval. A shutter/capture timing with no adequate all-frame-open interval fails closed.
3867
+ The whole pulse must fit inside the interval. A shutter/capture timing without a long enough whole-frame-open interval fails closed.
3595
3868
 
3596
3869
  Sensor **data readout timing is never used as flash exposure timing**.
3597
3870
 
3598
3871
  ### HSS boundary
3599
3872
 
3600
- `high-speed-sync` is a recognized request but is intentionally unsupported in schema 0.1.0.
3601
-
3602
- Photivra does not make HSS work by allowing an ordinary short pulse above the normal sync regime. Real HSS requires an extended/repeated illumination waveform coordinated with the moving exposure slit and has different energy/range behavior.
3873
+ `resolveManualFlashSync()` still rejects `high-speed-sync`. Photivra does not fake HSS (high-speed sync) by allowing an ordinary short pulse at shutter speeds faster than normal sync. Real HSS needs a long or repeated light waveform, coordinated with the moving exposure slit, and it behaves differently in energy and range. That is handled by the separate `resolveHighSpeedFlashSync()` burst model, for schema 0.2.0 sync profiles that declare HSS (see [Rolling shutter, video, flash TTL/HSS and highlight-priority metering](#rolling-shutter-video-flash-ttlhss-and-highlight-priority-metering-200)).
3603
3874
 
3604
3875
  ### Illumination overlay
3605
3876
 
3606
- `createManualFlashIlluminationOverlay()` appends the flash as a real #85 illumination source and constructs the matching temporal waveform/source binding.
3877
+ `createManualFlashIlluminationOverlay()` adds the flash as a real scene illumination source, and builds the matching temporal waveform and source binding.
3607
3878
 
3608
- If an ambient `SceneIlluminationTemporalProfile` already exists, its waveforms and source bindings are preserved and rebound to the new combined illumination-profile identity. Flash therefore does not erase ambient flicker or other source timing.
3879
+ If an ambient `SceneIlluminationTemporalProfile` already exists, its waveforms and source bindings are kept and rebound to the new combined illumination-profile identity. So the flash does not erase ambient flicker or other source timing.
3609
3880
 
3610
- When flash is disabled, the base illumination and temporal profiles are returned unchanged in meaning and no flash source is added.
3881
+ When the flash is disabled, the base illumination and temporal profiles are returned with unchanged meaning, and no flash source is added.
3611
3882
 
3612
- The flash source retains its position/direction/coverage geometry. Distance falloff, surface orientation, shadows, visibility and indirect transport remain owned by the scene-radiance provider/renderer; Photivra does not fake those effects with a frame-wide scalar.
3883
+ The flash source keeps its position, direction and coverage geometry. Distance falloff, surface orientation, shadows, visibility and indirect light remain the job of the scene-radiance provider or renderer. Photivra does not fake those effects with one number for the whole frame.
3613
3884
 
3614
3885
  ### Manual flash controls remain separate
3615
3886
 
@@ -3624,19 +3895,19 @@ The manual-flash layer does not change:
3624
3895
  - drive mode;
3625
3896
  - stable-support state.
3626
3897
 
3627
- The source output control is independent of camera ISO/aperture/shutter. Those camera settings affect the resulting captured flash contribution only through the normal image-formation chain.
3898
+ The flash output control is independent of the camera's ISO, aperture and shutter. Those camera settings affect the captured flash contribution only through the normal image-formation chain.
3628
3899
 
3629
- Flash spectrum is physical scene illumination. A downstream Flash WB preset, if selected, must not alter the source spectrum.
3900
+ The flash spectrum is physical scene illumination. A Flash WB preset chosen later, if any, must not change the source spectrum.
3630
3901
 
3631
3902
  ### #105 release binding
3632
3903
 
3633
- An optional committed `ResolvedReleaseFrame` may be supplied. Its shutter duration must match the #12 timing contract. The resolved flash event then exposes pulse start/end in sequence-relative scene time without changing the release frame.
3904
+ You may supply an optional committed `ResolvedReleaseFrame` from [release sequencing](#logical-release-sequences). Its shutter duration must match the exposure-window timing. The resolved flash event then reports pulse start and end in scene time relative to the sequence, without changing the release frame.
3634
3905
 
3635
- TTL/preflash metering, flash exposure compensation policy, FE/FV lock, recycle/burst/thermal limits, red-eye preflashes, modeling lamps, AF-assist and wireless protocol simulation remain outside this first manual-flash contract.
3906
+ Generic pre-flash TTL (through-the-lens) power, with flash exposure compensation, is solved separately by `resolveTtlFlashExposure()`, and applied to the manual profile with `createTtlScaledFlashProfile()`. Outside the manual-flash contract: FE/FV lock, recycle/burst/thermal limits, red-eye pre-flashes, modelling lamps, AF-assist and simulation of wireless protocols.
3636
3907
 
3637
3908
  ## Capture orientation, active area, and output geometry
3638
3909
 
3639
- Use `resolveCaptureGeometry()` to keep physical sensor identity, active capture, physical camera orientation, and final digital output geometry separate:
3910
+ Use `resolveCaptureGeometry()` to keep four things separate: the physical sensor, the active capture area, how the camera is physically held (orientation), and the final digital output geometry:
3640
3911
 
3641
3912
  ```ts
3642
3913
  import { resolveCaptureGeometry } from "@photivra/engine";
@@ -3684,17 +3955,20 @@ import {
3684
3955
  } from "@photivra/engine";
3685
3956
  ```
3686
3957
 
3687
- Point transforms use continuous raster-edge coordinates; pixel centers may be represented with +0.5 offsets. Vector transforms rotate direction only. Rectangle transforms preserve integer half-open semantics. Clockwise and counter-clockwise portrait transforms are distinct and round-trip to native coordinates.
3958
+ - Point transforms use continuous coordinates on pixel edges; pixel centers are at +0.5 offsets.
3959
+ - Vector transforms rotate direction only.
3960
+ - Rectangle transforms keep integer, half-open semantics.
3961
+ - Clockwise and counter-clockwise portrait transforms are distinct, and both round-trip back to native coordinates.
3688
3962
 
3689
- `activeCaptureRect` is expressed in native coordinates. The generic model derives physical active-area bounds by assuming native image samples uniformly span the declared physical imaging area. The result exposes physical bounds and center offset relative to the optical axis so off-center crops remain optically asymmetric.
3963
+ `activeCaptureRect` is given in native coordinates. The generic model works out the physical bounds of the active area by assuming native image samples are spread evenly across the declared physical imaging area. The result gives the physical bounds and the center offset from the optical axis, so off-center crops stay optically asymmetric.
3690
3964
 
3691
- `outputCropRect` is expressed in oriented active-capture coordinates. `outputRaster` may resample that crop but must preserve its aspect ratio within integer-rounding tolerance; implicit geometric stretching is rejected.
3965
+ `outputCropRect` is given in oriented active-capture coordinates. `outputRaster` may resample that crop, but must keep its aspect ratio within integer-rounding tolerance. Hidden geometric stretching is rejected.
3692
3966
 
3693
- Display/file transforms such as EXIF mirroring remain separate from physical `CaptureOrientation`.
3967
+ Display and file transforms, such as EXIF mirroring, stay separate from the physical `CaptureOrientation`.
3694
3968
 
3695
- Use `calculateActiveCaptureFieldOfView()` for active physical capture FOV. It preserves signed horizontal/vertical bounds for off-center crops and reports both opposite-corner diagonal spans. `diagonalDegrees` is the larger of those spans; centered crops produce equal diagonal spans.
3969
+ Use `calculateActiveCaptureFieldOfView()` for the FOV of the active physical capture. It keeps signed horizontal/vertical bounds for off-center crops, and reports both corner-to-corner diagonal spans. `diagonalDegrees` is the larger of the two; centered crops give equal spans.
3696
3970
 
3697
- Final digital/output crop is deliberately excluded from active-capture FOV.
3971
+ The final digital/output crop is deliberately not part of active-capture FOV.
3698
3972
 
3699
3973
  ## Centered crop and subject framing crop
3700
3974
 
@@ -3715,7 +3989,7 @@ console.log(crop.value.megapixels);
3715
3989
  console.log(crop.value.retainedAreaFraction);
3716
3990
  ```
3717
3991
 
3718
- Use `calculateSubjectFramingCrop()` to calculate the additional crop needed for a projected subject to occupy a target fraction of frame height:
3992
+ Use `calculateSubjectFramingCrop()` to work out the extra crop needed for a projected subject to fill a target fraction of the frame height:
3719
3993
 
3720
3994
  ```ts
3721
3995
  import { calculateSubjectFramingCrop } from "@photivra/engine";
@@ -3732,13 +4006,19 @@ console.log(framing.value.subjectHeightFraction);
3732
4006
  console.log(framing.value.subjectClipped);
3733
4007
  ```
3734
4008
 
3735
- Subject framing assumes the crop can be positioned around the subject; it does not check the subject's actual position against image edges.
4009
+ Subject framing assumes the crop can be placed around the subject. It does not check the subject's actual position against the image edges.
4010
+
4011
+ Integer rounding:
3736
4012
 
3737
- See [Physics Foundation](PHYSICS_FOUNDATION.md#sensor-geometry-sampling-capture-and-crop).
4013
+ - `calculateCenteredCrop()` floors the shorter axis (`floor(dimension / cropFactor)`). It derives the longer axis as the integer nearest to the source aspect ratio, capped at `floor(dimension / cropFactor)`. For example, 6000×4000 at 1.7× gives 3528×2352 (exactly 3:2).
4014
+ - `calculateSubjectFramingCrop()` uses the largest whole-pixel height within the requested crop height (`subjectHeightPixels / targetSubjectHeightFraction`), never fewer than `ceil(subjectHeightPixels)`, and the nearest-integer width for the source ratio.
4015
+ - So `subjectHeightFraction` is at least the target (within one pixel of crop height), and `subjectClipped` is never `true` when the subject fits the source frame.
4016
+
4017
+ See [Physics Foundation](https://github.com/Photivra/photivra/blob/main/docs/PHYSICS_FOUNDATION.md#sensor-geometry-sampling-capture-and-crop).
3738
4018
 
3739
4019
  ## Projected subject motion
3740
4020
 
3741
- Use `calculateProjectedMotionBlur()` for representative-point image-plane displacement during an exposure:
4021
+ Use `calculateProjectedMotionBlur()` for how far one representative point moves on the image plane during an exposure:
3742
4022
 
3743
4023
  ```ts
3744
4024
  import { calculateProjectedMotionBlur } from "@photivra/engine";
@@ -3758,15 +4038,15 @@ console.log(motion.value.distanceMm);
3758
4038
  console.log(motion.value.distancePixels);
3759
4039
  ```
3760
4040
 
3761
- The model assumes constant world-space linear velocity. It tracks a representative point rather than extended-object scale blur, rotation, deformation, or acceleration.
4041
+ The model assumes constant linear velocity in world space. It follows one representative point. It does not model blur from changing object size, rotation, deformation or acceleration.
3762
4042
 
3763
- See [Motion and Signal Foundation](MOTION_AND_SIGNAL.md#projected-subject-motion).
4043
+ See [Motion and Signal Foundation](https://github.com/Photivra/photivra/blob/main/docs/MOTION_AND_SIGNAL.md#projected-subject-motion).
3764
4044
 
3765
4045
  ## Extended-object time-varying projection
3766
4046
 
3767
- `calculateProjectedMotionBlur()` remains the compact representative-point displacement API. Use `calculateExtendedObjectProjectionTrajectory()` when object extent matters.
4047
+ `calculateProjectedMotionBlur()` stays the compact API for one representative point. Use `calculateExtendedObjectProjectionTrajectory()` when the object's size and shape matter.
3768
4048
 
3769
- The extended-object foundation accepts:
4049
+ The extended-object foundation takes:
3770
4050
 
3771
4051
  - multiple explicit metric camera-space points belonging to one object/region;
3772
4052
  - one shared constant rigid object-translation velocity;
@@ -3785,13 +4065,13 @@ image x(t) = projection distance * x(t) / z(t)
3785
4065
  image y(t) = projection distance * y(t) / z(t)
3786
4066
  ```
3787
4067
 
3788
- This is important for axial motion. An object centered on the optical axis can have almost zero center-point displacement while its off-axis corners move radially because magnification changes.
4068
+ This matters for motion towards or away from the camera. An object centered on the optical axis can have almost no movement at its center, while its off-axis corners move outwards or inwards because the magnification changes.
3789
4069
 
3790
4070
  ### Front-to-back motion and scale
3791
4071
 
3792
- Do not interpret every depth-direction motion result as one global scale.
4072
+ Do not read every result for motion in depth as one global scale.
3793
4073
 
3794
- If the caller has a known fronto-parallel planar patch, it may explicitly provide:
4074
+ If you have a known flat patch facing the camera squarely (fronto-parallel), you may provide it explicitly:
3795
4075
 
3796
4076
  ```ts
3797
4077
  frontoparallelPlane: {
@@ -3812,7 +4092,7 @@ The scale diagnostic is valid for that declared patch only. It does **not** auth
3812
4092
 
3813
4093
  ### Mixed lateral + depth motion
3814
4094
 
3815
- Lateral and depth translation are evaluated together. Because projection depends on each point's depth, different object points can have materially different image-plane trajectories.
4095
+ Sideways and depth translation are evaluated together. Because projection depends on each point's depth, different points on the object can follow quite different paths on the image plane.
3816
4096
 
3817
4097
  The result therefore exposes per-point, per-time:
3818
4098
 
@@ -3822,15 +4102,15 @@ The result therefore exposes per-point, per-time:
3822
4102
  - depth;
3823
4103
  - optional validated planar scale.
3824
4104
 
3825
- Camera and object translation remain separate input identities even though the relative projection uses `object - camera` translation.
4105
+ Camera and object translation stay separate inputs, even though the relative projection uses `object - camera` translation.
3826
4106
 
3827
4107
  ### #12 local-exposure integration
3828
4108
 
3829
- Use `calculateCaptureExtendedObjectTemporalProjection()` when the geometry must follow capture timing.
4109
+ Use `calculateCaptureExtendedObjectTemporalProjection()` when the geometry must follow capture timing (the [exposure windows](#capture-exposure-window-timing), #12).
3830
4110
 
3831
- Every object point supplies the native destination point whose committed #12 exposure window applies. The engine creates deterministic midpoint nodes inside each point's local exposure interval and evaluates the same extended-object projection there.
4111
+ Each object point names the native destination point whose committed exposure window applies. The engine creates deterministic midpoint times inside each point's local exposure interval, and evaluates the same extended-object projection there.
3832
4112
 
3833
- With rolling/local exposure timing, two image locations can therefore integrate different portions of the same physical object trajectory.
4113
+ With rolling or other local exposure timing, two image locations can therefore record different parts of the same object's path.
3834
4114
 
3835
4115
  Sensor **data readout timing is not substituted for exposure timing**.
3836
4116
 
@@ -3849,15 +4129,15 @@ It does not calculate:
3849
4129
  - camera rotation;
3850
4130
  - a universal projective warp.
3851
4131
 
3852
- If relative depth changes, the object-to-focus relationship also changes through time. A downstream high-fidelity renderer may therefore need to evaluate defocus/PSF at the same physical-time nodes rather than convolving one motion result with one static defocus kernel.
4132
+ If the relative depth changes, the object's distance from the focus plane also changes over time. A high-fidelity renderer may therefore need to evaluate defocus/PSF at the same physical times, instead of convolving one motion result with one fixed defocus kernel.
3853
4133
 
3854
- A renderer using #85 should evaluate scene radiance/visibility at the temporal geometry states rather than inferring a complete blur from endpoints alone.
4134
+ A renderer using a [scene-radiance provider](#scene-radiance-provider-and-material-response) should evaluate scene radiance and visibility at each of these geometry states, rather than guessing a complete blur from the endpoints alone.
3855
4135
 
3856
4136
  ## Relative pre-exposure metering
3857
4137
 
3858
- Use `parseExposureMeteringProfile()` and `meterRelativeExposure()` for the first renderer-neutral metering path.
4138
+ Metering measures how bright the scene is, so exposure can be set. Use `parseExposureMeteringProfile()` and `meterRelativeExposure()` for the first metering path. It does not depend on any renderer.
3859
4139
 
3860
- Schema `0.1.0` deliberately accepts only **relative pre-exposure linear signal**. It does not claim calibrated luminance, scene spectral radiance, sensor-plane irradiance, or a commercial-camera meter calibration.
4140
+ Schema `0.1.0` deliberately accepts only a **relative, linear signal measured before exposure**. It does not claim calibrated luminance, scene spectral radiance, sensor-plane irradiance or the meter calibration of any commercial camera.
3861
4141
 
3862
4142
  ```ts
3863
4143
  import {
@@ -3969,26 +4249,27 @@ if (metered.value.status === "resolved") {
3969
4249
  }
3970
4250
  ```
3971
4251
 
3972
- For a controlled uniform fixture, halving every relative-linear sample from `1.0` to `0.5` produces a **+1 stop** shift to the same declared target. That is the intended educational invariant used by Manual + Auto ISO tests.
4252
+ For a controlled uniform test scene, halving every relative-linear sample from `1.0` to `0.5` gives a **+1 stop** shift to reach the same declared target. The Manual + Auto ISO tests rely on this deliberate, educational rule.
3973
4253
 
3974
4254
  ### Metering policies
3975
4255
 
3976
- The first generic policies are explicit rather than branded:
4256
+ The first generic policies are spelled out explicitly, not named after brands:
3977
4257
 
3978
- - `multi-zone-uniform` — area-weighted average over all supplied zones;
3979
- - `center-weighted-radial` — multiplies area weight by a radial physical-frame weighting function with explicit edge weight and exponent;
3980
- - `spot` — selects samples inside an explicit spot center/radius in the oriented active capture frame;
3981
- - `highlight-weighted` — gives brighter relative-linear samples more weight through an explicit exponent and minimum weight fraction.
4258
+ - `multi-zone-uniform` — area-weighted average over all supplied zones.
4259
+ - `center-weighted-radial` — multiplies each area weight by a radial weighting over the physical frame, with an explicit edge weight and exponent.
4260
+ - `spot` — uses only samples inside an explicit spot (center and radius) in the oriented active capture frame.
4261
+ - `highlight-weighted` — gives brighter samples more weight, through an explicit exponent and a minimum weight fraction, then places that weighted mean at the target. It emphasizes highlights; it does not protect them. In a mostly mid-grey frame with a small bright area, it darkens the mid-tones (about 0.5–0.8 EV compared with uniform metering, for a frame that is 95 % at 0.18 and 5 % at 0.9), without placing the highlights relative to saturation.
4262
+ - `highlight-priority` — places the area-weighted `highlightPercentile` quantile of the samples `headroomStops` below a declared `saturationRelativeSignal`. The profile target must equal `saturationRelativeSignal · 2^−headroomStops` (`calculateHighlightPriorityPlacementSignal()`). It is a quantile over space, so the time-averaged scene-radiance meter and TTL flash reject it.
3982
4263
 
3983
- The sample producer supplies `areaWeight`, so denser sampling in one part of the frame does not automatically mean more metering influence.
4264
+ The sample producer supplies `areaWeight`, so sampling one part of the frame more densely does not automatically give it more influence.
3984
4265
 
3985
- Center/spot distance is evaluated using the **physical dimensions of the oriented active capture frame**, not CSS/display pixels. A final output crop is not used by schema 0.1.0.
4266
+ Center and spot distances use the **physical dimensions of the oriented active capture frame**, not CSS or display pixels. Schema 0.1.0 does not use a final output crop.
3986
4267
 
3987
4268
  ### Target and downstream control
3988
4269
 
3989
- `targetRelativeSignal` is a profile-defined reference. It is not a universal claim about 18% gray or a specific manufacturer's meter calibration.
4270
+ `targetRelativeSignal` is a reference defined by the profile. It is not a universal claim about 18% grey, or about any manufacturer's meter calibration.
3990
4271
 
3991
- The meter reports the base scene measurement independently from exposure compensation. It does not choose aperture, shutter duration, or ISO.
4272
+ The meter reports the base scene measurement separately from exposure compensation. It does not choose aperture, shutter duration or ISO.
3992
4273
 
3993
4274
  The intended flow is:
3994
4275
 
@@ -4001,19 +4282,21 @@ pre-exposure relative scene signal
4001
4282
  -> aperture / shutter / ISO
4002
4283
  ```
4003
4284
 
4004
- A zero-signal sample set returns `status: "no-signal"` rather than infinity/NaN.
4285
+ (#99 is [exposure control](#manual-and-auto-iso-exposure-resolution): the resolvers that choose aperture, shutter and ISO.)
4286
+
4287
+ A sample set with zero signal returns `status: "no-signal"`, not infinity or NaN.
4005
4288
 
4006
- `measurementId` and `sceneStateId` remain in the result so a future application can freeze a meter result for AE lock or reject stale results after scene-light changes.
4289
+ `measurementId` and `sceneStateId` stay in the result, so an application can freeze a meter result for AE lock, or reject stale results after the scene lighting changes.
4007
4290
 
4008
- The input must explicitly state that exposure settings, white balance, tone mapping, display gamma, and sharpening have not already been applied. This prevents a circular final-preview auto-exposure loop.
4291
+ The input must state explicitly that exposure settings, white balance, tone mapping, display gamma and sharpening have not already been applied. This prevents a feedback loop where auto-exposure meters its own final preview.
4009
4292
 
4010
- This path is ambient/relative metering only. Flash/TTL metering and calibrated camera-specific photometric/radiometric metering remain separate. Exposure compensation and automatic exposure consume the frozen meter target through the dedicated downstream contracts rather than modifying the meter measurement.
4293
+ This path covers ambient, relative metering only. Flash/TTL metering, and calibrated camera-specific photometric or radiometric metering, are separate. Exposure compensation and automatic exposure use the frozen meter target through their own contracts; they never change the meter measurement.
4011
4294
 
4012
4295
  ### Equipment metering capability binding
4013
4296
 
4014
- Use `parseGenericBodyMeteringCapabilityProfile()` plus `assessExposureMeteringProfileCompatibility()` to declare which engine-owned metering profiles/modes a generic camera body may select.
4297
+ Use `parseGenericBodyMeteringCapabilityProfile()` with `assessExposureMeteringProfileCompatibility()` to declare which engine-owned metering profiles and modes a generic camera body may select.
4015
4298
 
4016
- The equipment profile does **not** duplicate the meter target/calibration values. Those remain authoritative in the selected `ExposureMeteringProfile`.
4299
+ The equipment profile does **not** repeat the meter target or calibration values. Those stay authoritative in the selected `ExposureMeteringProfile`.
4017
4300
 
4018
4301
  Composition fragment: requires previously validated bindings `evidence`, `profile`. This is an integration sketch, not a standalone runnable program.
4019
4302
 
@@ -4052,13 +4335,14 @@ const compatibility =
4052
4335
  });
4053
4336
  ```
4054
4337
 
4055
- The compatibility result preserves `targetPolicyOwnership: "metering-profile"`, so equipment metadata never becomes a second source of calibration truth.
4338
+ The compatibility result keeps `targetPolicyOwnership: "metering-profile"`, so equipment metadata never becomes a second source of calibration truth.
4056
4339
 
4057
- Spot/focus-point linkage is capability metadata only. A future AF/spot-link control must explicitly consume it; the meter does not silently move a spot because a focus point exists.
4340
+ Linking the metering spot to the focus point is capability metadata only. A future AF/spot-link control must use it explicitly. The meter does not silently move a spot just because a focus point exists.
4058
4341
 
4059
4342
  ### Meter target and automatic exposure integration
4060
4343
 
4061
- Use `createExposureMeterTargetFromMeteringResult()` to freeze a spatial or temporal meter result into the target consumed by #99 exposure-mode resolution. Use `setExposureCompensationOnMeterTarget()` to apply compensation downstream without mutating the source measurement.
4344
+ - `createExposureMeterTargetFromMeteringResult()` freezes a spatial or temporal meter result into the target used by exposure-mode resolution.
4345
+ - `setExposureCompensationOnMeterTarget()` applies compensation afterwards, without changing the source measurement.
4062
4346
 
4063
4347
  For the controlled relative path:
4064
4348
 
@@ -4069,15 +4353,15 @@ uniform scene signal 0.5 -> meter target scale 2 -> ISO 200
4069
4353
 
4070
4354
  when aperture and shutter are fixed in Manual + Auto ISO and the ISO capability range permits it.
4071
4355
 
4072
- The frozen target retains the original meter measurement/scene/profile identity, providing the AE-lock seam. Reusing that target after scene changes intentionally keeps the locked meter target; creating a new meter result follows the new scene state.
4356
+ The frozen target keeps the original meter measurement, scene and profile identity; this is how AE lock works. Reusing that target after the scene changes deliberately keeps the locked target. Creating a new meter result follows the new scene state.
4073
4357
 
4074
- Automatic resolvers consume the typed target directly and do not reapply exposure compensation.
4358
+ Automatic resolvers read the typed target directly, and do not apply exposure compensation again.
4075
4359
 
4076
4360
  ## Scene-radiance-derived and temporal metering
4077
4361
 
4078
- Use `createSceneRadianceDerivedExposureMeteringSampleSet()` when a renderer/reference evaluator supplies relative pre-exposure meter zones derived from the #85 scene-radiance context.
4362
+ Use `createSceneRadianceDerivedExposureMeteringSampleSet()` when a renderer or reference evaluator supplies relative pre-exposure meter zones derived from a [scene-radiance provider](#scene-radiance-provider-and-material-response) (#85).
4079
4363
 
4080
- The bridge validates:
4364
+ The bridge checks:
4081
4365
 
4082
4366
  - scene/provider/illumination/material profile identity;
4083
4367
  - provider material fidelity against the supplied material profile;
@@ -4085,7 +4369,7 @@ The bridge validates:
4085
4369
  - optional temporal-illumination profile identity;
4086
4370
  - explicit capture time when the provider is time-varying.
4087
4371
 
4088
- The scalar reduction remains renderer/provider supplied. Photivra does **not** turn one or more spectral-radiance samples into luminance or a camera meter signal automatically.
4372
+ The reduction to one number per zone is still supplied by the renderer or provider. Photivra does **not** automatically turn spectral-radiance samples into luminance or a camera meter signal.
4089
4373
 
4090
4374
  Composition fragment: requires previously validated bindings `captureGeometry`, `illuminationProfile`, `materialResponseProfile`, `profile`, `providerProfile`. This is an integration sketch, not a standalone runnable program.
4091
4375
 
@@ -4146,15 +4430,15 @@ const meter = meterRelativeExposure({
4146
4430
  });
4147
4431
  ```
4148
4432
 
4149
- The source context reports `spectralReductionCalibrated: false`, and calibrated luminance/scene-radiance claims remain unauthorized.
4433
+ The source context reports `spectralReductionCalibrated: false`. Claims of calibrated luminance or scene radiance are not allowed.
4150
4434
 
4151
4435
  ### Time-varying illumination
4152
4436
 
4153
- If the scene-radiance provider declares `illuminationTemporalProfileId`, the bridge requires the matching temporal profile plus a finite `captureTimeSecondsFromReference` on the shared `first-opening-boundary-phase` reference.
4437
+ If the scene-radiance provider declares `illuminationTemporalProfileId`, the bridge needs the matching temporal profile, plus a finite `captureTimeSecondsFromReference` on the shared `first-opening-boundary-phase` reference.
4154
4438
 
4155
- One such sample set is an explicit instantaneous meter snapshot at that time.
4439
+ One such sample set is an explicit meter snapshot at that instant.
4156
4440
 
4157
- To average multiple snapshots, use `meterSceneRadianceTemporalExposure()` with an explicit `weighted-time-average` policy:
4441
+ To average several snapshots, use `meterSceneRadianceTemporalExposure()` with an explicit `weighted-time-average` policy:
4158
4442
 
4159
4443
  Composition fragment: requires previously validated bindings `profile`, `sampleAtTime0`, `sampleAtTime1`. This is an integration sketch, not a standalone runnable program.
4160
4444
 
@@ -4186,17 +4470,17 @@ const temporalMeter =
4186
4470
  });
4187
4471
  ```
4188
4472
 
4189
- The temporal weights must be positive and sum to one. Capture times must be strictly increasing. Every sample must share the same committed `sceneStateId`, provider/material/derivation context, temporal profile and active-capture geometry.
4473
+ The time weights must be positive and sum to one. Capture times must be strictly increasing. Every sample must share the same committed `sceneStateId`, provider/material/derivation context, temporal profile and active-capture geometry.
4190
4474
 
4191
- The meter averages **linear relative signals first**, then computes the stop offset to the target. It never averages EV/stop offsets.
4475
+ The meter averages the **linear relative signals first**, then computes the stop offset to the target. It never averages EV or stop offsets.
4192
4476
 
4193
- This policy is explicit ambient temporal metering. It does not infer time weights from source flicker frequency, source waveform shape, shutter/readout duration, renderer frame cadence, or final display brightness. Flash/TTL metering remains separate.
4477
+ This policy is explicit ambient metering over time. It does not guess time weights from the flicker frequency, the source waveform shape, the shutter or readout duration, the renderer's frame rate, or the final display brightness. Flash/TTL metering is separate.
4194
4478
 
4195
4479
  ## Logical release sequences
4196
4480
 
4197
- Use `parseGenericReleaseCapabilityProfile()` and `resolveReleaseSequence()` for deterministic logical camera releases such as single-frame capture, bursts, self-timer sequences, exposure bracketing, and focus bracketing.
4481
+ A "release" is one press of the shutter button and the frames it produces. Use `parseGenericReleaseCapabilityProfile()` and `resolveReleaseSequence()` for deterministic logical camera releases: single frames, bursts, self-timer sequences, exposure bracketing and focus bracketing.
4198
4482
 
4199
- This is deliberately separate from sensor capture modes: a burst of several photographs is several logical captures, not one multi-frame sensor reconstruction.
4483
+ This is deliberately separate from sensor capture modes. A burst of several photographs is several logical captures, not one multi-frame sensor reconstruction.
4200
4484
 
4201
4485
  Composition fragment: requires previously validated bindings `evidence`, `exposureCapabilities`. This is an integration sketch, not a standalone runnable program.
4202
4486
 
@@ -4297,38 +4581,38 @@ Each frame carries:
4297
4581
  - deterministic per-frame seed;
4298
4582
  - explicit automation/lock policy.
4299
4583
 
4300
- Adjacent frames derive different stochastic seeds from the same stable sequence seed, while replaying identical sequence inputs reproduces the same sequence.
4584
+ Neighbouring frames derive different random seeds from the same stable sequence seed. Replaying identical sequence inputs reproduces the same sequence.
4301
4585
 
4302
4586
  ### Cadence feasibility
4303
4587
 
4304
- Requested FPS is not treated as more authoritative than physics/capabilities.
4588
+ The requested frames per second (FPS) never override physics or the camera's capabilities.
4305
4589
 
4306
- For non-overlapping ordinary still exposures, the actual start-to-start interval is constrained by:
4590
+ For ordinary still exposures, which do not overlap, the actual start-to-start interval is limited by:
4307
4591
 
4308
4592
  - requested cadence;
4309
4593
  - body maximum cadence;
4310
4594
  - previous frame exposure duration;
4311
4595
  - minimum inter-frame gap.
4312
4596
 
4313
- The resolver reports the active timing constraint rather than silently scheduling overlapping exposures.
4597
+ The resolver reports which timing limit applied, instead of silently scheduling overlapping exposures.
4314
4598
 
4315
- Buffer/media/thermal slowdown, pre-release capture, and special overlapping acquisition modes are not modeled in schema 0.1.0.
4599
+ Schema 0.1.0 does not model slowdown from buffers, media or heat, pre-release capture, or special modes with overlapping acquisition.
4316
4600
 
4317
4601
  ### Bracketing
4318
4602
 
4319
- Exposure bracketing changes an explicit physical exposure axis per frame. It does not apply a post-render brightness offset.
4603
+ Exposure bracketing changes an explicit physical exposure setting for each frame. It does not add a brightness offset after rendering.
4320
4604
 
4321
- Focus bracketing consumes explicit `FocusPlane` states. It does not claim focus-stacked output or assign universal physical meaning to an abstract focus-step index.
4605
+ Focus bracketing uses explicit `FocusPlane` states. It does not claim to produce a focus-stacked image, and does not give an abstract focus-step index a universal physical meaning.
4322
4606
 
4323
- White-balance bracketing is not forced into physical multi-exposure semantics; WB rendering/development variation remains downstream color-pipeline work.
4607
+ White-balance bracketing is not forced to mean several physical exposures. Varying WB in rendering or development is work for the colour pipeline later on.
4324
4608
 
4325
4609
  ### Cancellation
4326
4610
 
4327
- Use `createCancelledReleaseSequence()` to record an explicit completed/omitted frame boundary without mutating the scheduled source sequence.
4611
+ Use `createCancelledReleaseSequence()` to record explicitly which frames were completed and which were left out, without changing the scheduled source sequence.
4328
4612
 
4329
4613
  ## Generic equipment exposure capabilities
4330
4614
 
4331
- Use the #109 equipment-capability API to describe **what a generic Photivra body and lens allow** before #99 chooses any setting.
4615
+ Use the equipment-capability API (#109) to describe **what a generic Photivra body and lens allow**, before [exposure control](#manual-and-auto-iso-exposure-resolution) (#99) chooses any setting.
4332
4616
 
4333
4617
  ```ts
4334
4618
  import {
@@ -4527,20 +4811,20 @@ A setting grid is either:
4527
4811
  - `continuous-within-range` — a downstream resolver may choose any finite value inside the range; or
4528
4812
  - `discrete-values` — only the declared values are valid.
4529
4813
 
4530
- For a variable-aperture lens, the discrete aperture list is filtered after the wide-open f-number is resolved at the selected focal length.
4814
+ For a lens whose widest aperture changes with zoom, the discrete aperture list is filtered after the wide-open f-number is resolved at the selected focal length.
4531
4815
 
4532
4816
  ### Capability versus selected state
4533
4817
 
4534
- These profiles do **not** represent the current camera settings. They are immutable capability inputs.
4818
+ These profiles do **not** describe the current camera settings. They are immutable inputs that say what is possible.
4535
4819
 
4536
- The resolver also does not choose aperture, shutter or ISO. #99 owns:
4820
+ The resolver also does not choose aperture, shutter or ISO. Exposure control owns:
4537
4821
 
4538
- - manual versus automatic axis policy;
4539
- - validation/quantization of requested settings;
4822
+ - which settings are manual and which are automatic;
4823
+ - validating and quantizing requested settings;
4540
4824
  - clamping to capability limits;
4541
4825
  - residual exposure diagnostics.
4542
4826
 
4543
- The legacy `CameraConfiguration` remains semantically unchanged while #109 builds the newer versioned capability/state architecture.
4827
+ The legacy `CameraConfiguration` keeps its meaning unchanged, alongside this newer, versioned capability/state design.
4544
4828
 
4545
4829
  Auto ISO capability preserves three states:
4546
4830
 
@@ -4550,13 +4834,13 @@ Auto ISO capability preserves three states:
4550
4834
 
4551
4835
  An unknown capability must not be treated as supported.
4552
4836
 
4553
- ISO capability is control metadata only. It does not imply sensor noise, analog gain topology, photon count, conversion gain or high-ISO quality.
4837
+ ISO capability is control metadata only. It says nothing about sensor noise, analog gain topology, photon count, conversion gain or high-ISO quality.
4554
4838
 
4555
- Likewise, shutter-duration capability does not imply shutter mechanism, rolling/global readout, exposure-boundary timing or flash sync.
4839
+ Likewise, shutter-duration capability says nothing about the shutter mechanism, rolling/global readout, exposure-boundary timing or flash sync.
4556
4840
 
4557
4841
  ## Meter target and exposure compensation
4558
4842
 
4559
- Use `createExposureMeterTargetFromMeteringResult()` to freeze a spatial or temporal metering result into the stable target consumed by #99.
4843
+ Use `createExposureMeterTargetFromMeteringResult()` to freeze a spatial or temporal metering result into the stable target that exposure control reads.
4560
4844
 
4561
4845
  Composition fragment: requires previously validated bindings `meterResult`. This is an integration sketch, not a standalone runnable program.
4562
4846
 
@@ -4587,11 +4871,11 @@ The frozen target copies the authoritative meter identity by value:
4587
4871
  - scene-state ID;
4588
4872
  - metering-profile ID.
4589
4873
 
4590
- This is the AE-lock seam. Holding the target object intentionally preserves the earlier meter snapshot even if the live scene later changes.
4874
+ This is how AE lock works. Holding on to the target object deliberately keeps the earlier meter snapshot, even if the live scene later changes.
4591
4875
 
4592
4876
  ### Compensation semantics
4593
4877
 
4594
- Exposure compensation is an **absolute downstream target offset**.
4878
+ Exposure compensation is an **absolute offset applied to the target**, after metering.
4595
4879
 
4596
4880
  For a resolved target:
4597
4881
 
@@ -4607,19 +4891,19 @@ compensated exposure scale =
4607
4891
 
4608
4892
  Therefore +1 EV compensation requests twice the exposure of the uncompensated target; -1 EV requests half.
4609
4893
 
4610
- `setExposureCompensationOnMeterTarget()` always re-derives from the stored uncompensated base values. Passing an already compensated target with a new compensation value does not compound the old compensation. This avoids control drift during repeated slider/dial updates.
4894
+ `setExposureCompensationOnMeterTarget()` always works from the stored, uncompensated base values. If you pass a target that is already compensated, with a new compensation value, the old compensation is not added on top. This stops drift when a slider or dial is moved repeatedly.
4611
4895
 
4612
- A changed compensation state requires a new `targetId`. The source meter snapshot identity remains unchanged.
4896
+ A changed compensation state needs a new `targetId`. The identity of the source meter snapshot stays the same.
4613
4897
 
4614
- The meter measurement itself is never mutated, and the target still reports `automaticExposureResolved: false`. #99 owns the later aperture/shutter/ISO decision.
4898
+ The meter measurement itself is never changed, and the target still reports `automaticExposureResolved: false`. Exposure control makes the later aperture/shutter/ISO decision.
4615
4899
 
4616
- A no-signal meter target remains `no-signal` after compensation. The engine does not convert darkness into infinity or fabricate a reachable automatic exposure.
4900
+ A `no-signal` meter target stays `no-signal` after compensation. The engine does not turn darkness into infinity, or make up an automatic exposure that could be reached.
4617
4901
 
4618
4902
  ## ISO/exposure-index capabilities and generic high-ISO signal chain
4619
4903
 
4620
- Photivra keeps **reported ISO/exposure-index state** separate from captured photons and from the #14 electronic readout model.
4904
+ Photivra keeps the **reported ISO / exposure-index setting** separate from the captured photons, and from the [electronic readout model](#stochastic-charge-conversion-adc-and-raw-code) (#14).
4621
4905
 
4622
- Use `parseIsoCapabilityProfile()` for detailed camera/body capability metadata beyond the older generic numeric envelope.
4906
+ Use `parseIsoCapabilityProfile()` for detailed ISO capability metadata for a camera body, beyond the older generic numeric range.
4623
4907
 
4624
4908
  The profile can declare:
4625
4909
 
@@ -4630,7 +4914,7 @@ The profile can declare:
4630
4914
  - a global Auto ISO range;
4631
4915
  - evidenced per-capture-mode standard ranges, expanded-setting availability, and Auto ISO ranges.
4632
4916
 
4633
- The contract describes **reported exposure-index capability**, not physical sensor gain. Resolving a setting therefore explicitly reports that it did not modify or infer:
4917
+ The contract describes **which exposure-index settings a camera reports**, not physical sensor gain. Resolving a setting therefore states explicitly that it did not change or infer:
4634
4918
 
4635
4919
  - captured-photon expectation;
4636
4920
  - photon shot-noise statistics;
@@ -4640,36 +4924,36 @@ The contract describes **reported exposure-index capability**, not physical sens
4640
4924
  - saturation;
4641
4925
  - exact commercial-camera behavior.
4642
4926
 
4643
- Expanded settings are intentionally separate from the ordinary standard numeric setting grid. A reported `H1` equivalent does not mean Photivra has inferred how a real camera implements that state internally.
4927
+ Expanded settings are deliberately separate from the ordinary standard numeric setting grid. A reported `H1` equivalent does not mean Photivra knows how a real camera implements that setting internally.
4644
4928
 
4645
4929
  ### Binding detailed ISO capability into automatic exposure
4646
4930
 
4647
- Use `bindIsoCapabilityToExposureCapabilities()` to combine the detailed #7 profile with the existing resolved body/lens exposure envelope consumed by #99.
4931
+ Use `bindIsoCapabilityToExposureCapabilities()` to combine this detailed ISO profile (#7) with the resolved body/lens exposure envelope that exposure control reads.
4648
4932
 
4649
- The global standard ISO range/grid must agree exactly between both contracts; conflicting duplicate capability facts fail closed.
4933
+ The global standard ISO range and grid must agree exactly between the two contracts. Conflicting duplicate facts fail closed.
4650
4934
 
4651
4935
  An evidenced capture-mode policy may narrow the standard ISO range for that mode. Auto ISO can then be narrower still.
4652
4936
 
4653
- For example, a mode may allow manual standard ISO 100–12800 while restricting Auto ISO to 200–3200. #99 keeps manual ISO validation on the mode's standard range/grid but uses the separate Auto ISO bounds for all automatic-ISO paths.
4937
+ For example, a mode may allow manual standard ISO 100–12800 while limiting Auto ISO to 200–3200. Exposure control checks manual ISO against the mode's standard range and grid, but uses the separate Auto ISO bounds for every automatic-ISO path.
4654
4938
 
4655
- Expanded settings remain outside #99's ordinary numeric Auto-ISO resolver and are selected explicitly through the detailed ISO capability contract.
4939
+ Expanded settings stay outside the ordinary numeric Auto ISO resolver. They are selected explicitly through the detailed ISO capability contract.
4656
4940
 
4657
4941
  ### Generic high-ISO signal-chain profiles
4658
4942
 
4659
- `parseGenericIsoSignalChainProfile()` and `resolveGenericIsoSignalChain()` provide the generic Stage-B model.
4943
+ `parseGenericIsoSignalChainProfile()` and `resolveGenericIsoSignalChain()` provide the generic "Stage-B" model, which links an ISO setting to the sensor's electronics.
4660
4944
 
4661
- The profile does **not** define an ISO→noise equation. Instead, for one exact capture mode it maps:
4945
+ The profile does **not** define an ISO → noise equation. Instead, for one exact capture mode, it maps:
4662
4946
 
4663
4947
  - standard ISO/EI bands; and
4664
4948
  - explicitly named expanded ISO states
4665
4949
 
4666
- to existing #14 `SensorReadoutConversionRegime` IDs.
4950
+ to existing `SensorReadoutConversionRegime` IDs (the [readout regimes](#stochastic-charge-conversion-adc-and-raw-code), #14).
4667
4951
 
4668
- Those #14 regimes own the actual declared conversion gain, electronic read-noise components, pre-ADC saturation, black level, ADC and RAW-code behavior.
4952
+ Those regimes own the actual declared conversion gain, electronic read-noise components, pre-ADC saturation, black level, ADC and RAW-code behaviour.
4669
4953
 
4670
- Changing ISO or a generic profile therefore cannot alter upstream photoelectron expectation or photon shot-noise statistics. Any change in downstream read noise or conversion/saturation behavior comes from the explicitly selected #14 regime.
4954
+ So changing ISO, or a generic profile, cannot change the expected photoelectrons or photon shot-noise statistics upstream. Any change in read noise or conversion/saturation behaviour comes from the explicitly selected regime.
4671
4955
 
4672
- Signal-chain bands may be discontinuous where only discrete ISO values are supported. Overlapping bands fail closed rather than letting ordering choose a regime.
4956
+ Signal-chain bands may have gaps where only discrete ISO values are supported. Overlapping bands fail closed, rather than letting their order pick a regime.
4673
4957
 
4674
4958
  The first generic profile deliberately does not add:
4675
4959
 
@@ -4679,38 +4963,38 @@ The first generic profile deliberately does not add:
4679
4963
  - processed-image/JPEG behavior;
4680
4964
  - manufacturer-specific response curves.
4681
4965
 
4682
- Those effects require their own explicit models/evidence and must not be hidden inside an ordinal quality label.
4966
+ Those effects need their own explicit models and evidence, and must not be hidden inside a quality label.
4683
4967
 
4684
4968
  ### Good / Better / Best convenience mapping
4685
4969
 
4686
4970
  `parseGenericIsoSignalChainPresetCatalog()` can map the user-facing convenience labels `good`, `better`, and `best` to explicit signal-chain profile IDs.
4687
4971
 
4688
- The labels are not the scientific input. The selected profile parameters are.
4972
+ The labels are not the scientific input; the selected profile's parameters are.
4689
4973
 
4690
4974
  The catalog explicitly carries:
4691
4975
 
4692
4976
  - `performanceOrderingClaimed: false`;
4693
4977
  - `realCameraRankingClaimed: false`.
4694
4978
 
4695
- This prevents the convenience labels from becoming a claim that one real camera class is universally superior or that a tier name itself changes sensor physics.
4979
+ This stops the convenience labels from turning into a claim that one class of real camera is always better, or that a tier name by itself changes sensor physics.
4696
4980
 
4697
4981
  ## Manual and Auto ISO exposure resolution
4698
4982
 
4699
- Use `resolveManualExposureMode()` for the first #99 exposure-control slice.
4983
+ Exposure control (#99) chooses the camera settings from a meter target. Use `resolveManualExposureMode()` for Manual mode, the first part of it.
4700
4984
 
4701
- This resolver consumes:
4985
+ This resolver takes:
4702
4986
 
4703
- - a frozen #100 `ExposureMeterTarget`;
4704
- - the resolved #109 generic body+lens exposure capabilities;
4987
+ - a frozen `ExposureMeterTarget` (from [metering](#meter-target-and-exposure-compensation), #100);
4988
+ - the resolved generic body + lens exposure capabilities (from [equipment capabilities](#generic-equipment-exposure-capabilities), #109);
4705
4989
  - an explicit relative reference exposure anchor;
4706
- - caller-owned aperture and shutter settings;
4990
+ - aperture and shutter settings chosen by the caller;
4707
4991
  - either manual ISO or Auto ISO.
4708
4992
 
4709
- The resolver does **not** meter the scene, apply exposure compensation, change aperture/shutter, or infer sensor noise/gain behavior.
4993
+ The resolver does **not** meter the scene, apply exposure compensation, change aperture or shutter, or infer sensor noise or gain behaviour.
4710
4994
 
4711
4995
  ### Reference exposure anchor
4712
4996
 
4713
- The relative meter target tells the resolver how much exposure change is required, but it does not by itself define an absolute ISO.
4997
+ The relative meter target tells the resolver how much the exposure must change. On its own, it does not define an absolute ISO.
4714
4998
 
4715
4999
  Therefore this API requires:
4716
5000
 
@@ -4722,9 +5006,9 @@ const referenceExposure = {
4722
5006
  };
4723
5007
  ```
4724
5008
 
4725
- The anchor means: when the target scale is `1`, this aperture/shutter/ISO combination represents the declared relative exposure baseline.
5009
+ The anchor means: when the target scale is `1`, this aperture/shutter/ISO combination is the declared relative exposure baseline.
4726
5010
 
4727
- It is explicit input, not a universal ISO-100 or gray-card rule. All three anchor values must be valid in the same resolved equipment capability envelope.
5011
+ It is an explicit input, not a universal ISO-100 or grey-card rule. All three anchor values must be valid in the same resolved equipment capability envelope.
4728
5012
 
4729
5013
  ### Manual + Auto ISO
4730
5014
 
@@ -4760,9 +5044,9 @@ With aperture/shutter fixed at the reference values:
4760
5044
  - target scale `2` resolves ideal ISO 200;
4761
5045
  - target scale `4` resolves ideal ISO 400.
4762
5046
 
4763
- This is the controlled invariant used for live scene-light response: a uniform -1 stop scene-light change produces +1 stop of ideal ISO when limits/grid permit it.
5047
+ This is the controlled rule for responding to live changes in scene light: if the scene gets 1 stop darker everywhere, the ideal ISO goes up 1 stop, when the limits and grid allow it.
4764
5048
 
4765
- If manual optical settings differ from the reference, the resolver compensates only through ISO:
5049
+ If the manual optical settings differ from the reference, the resolver compensates only through ISO:
4766
5050
 
4767
5051
  ```text
4768
5052
  manual optical factor =
@@ -4778,7 +5062,7 @@ ideal Auto ISO =
4778
5062
 
4779
5063
  ### Discrete ISO quantization
4780
5064
 
4781
- For a discrete #109 ISO grid, the first policy chooses the nearest valid ISO in log2 exposure space.
5065
+ For a discrete ISO grid from the equipment capabilities, the first policy picks the nearest valid ISO in log2 exposure space (that is, nearest in stops).
4782
5066
 
4783
5067
  On an exact midpoint tie, the lower ISO is selected.
4784
5068
 
@@ -4805,7 +5089,7 @@ The limiting constraint is reported as one of:
4805
5089
 
4806
5090
  If Auto ISO capability is `unsupported` or `unknown`, the resolver returns a blocked result. Unknown is not treated as supported.
4807
5091
 
4808
- A `no-signal` meter target also blocks Auto ISO rather than selecting maximum ISO or infinity.
5092
+ A `no-signal` meter target also blocks Auto ISO, instead of selecting maximum ISO or infinity.
4809
5093
 
4810
5094
  ### Manual ISO
4811
5095
 
@@ -4826,17 +5110,17 @@ const resolved =
4826
5110
  });
4827
5111
  ```
4828
5112
 
4829
- Manual aperture, shutter, and ISO are preserved exactly after capability validation.
5113
+ Manual aperture, shutter and ISO are kept exactly, after they are checked against the capabilities.
4830
5114
 
4831
- A changed meter target or exposure compensation changes only the target-residual diagnostic. It does not change any manual setting.
5115
+ A changed meter target or exposure compensation changes only the target-residual diagnostic. It never changes a manual setting.
4832
5116
 
4833
- This is intentionally different from Manual + Auto ISO.
5117
+ This is deliberately different from Manual + Auto ISO.
4834
5118
 
4835
5119
  ### Boundaries
4836
5120
 
4837
- The #99 resolver now covers the requested Manual, priority, Program, Full Auto exposure, and Bulb/Time control families.
5121
+ Exposure control now covers Manual, the priority modes, Program, Full Auto exposure, and Bulb/Time control.
4838
5122
 
4839
- The following remain intentionally separate rather than hidden inside shooting-mode resolution:
5123
+ These stay deliberately separate, rather than being hidden inside shooting-mode resolution:
4840
5124
 
4841
5125
  - safety shift;
4842
5126
  - flash-aware exposure;
@@ -4845,7 +5129,7 @@ The following remain intentionally separate rather than hidden inside shooting-m
4845
5129
 
4846
5130
  ## Aperture Priority with manual ISO
4847
5131
 
4848
- Use `resolveAperturePriorityExposureMode()` when aperture and ISO are caller-selected and shutter is the only automatic exposure axis.
5132
+ Use `resolveAperturePriorityExposureMode()` when you choose aperture and ISO, and shutter is the only automatic setting.
4849
5133
 
4850
5134
  Composition fragment: requires previously validated bindings `capabilities`, `target`. This is an integration sketch, not a standalone runnable program.
4851
5135
 
@@ -4870,7 +5154,7 @@ const resolved =
4870
5154
  });
4871
5155
  ```
4872
5156
 
4873
- This mode uses the same relative target/reference model as Manual + Auto ISO. It does not introduce an Aperture-Priority-specific exposure equation.
5157
+ This mode uses the same relative target/reference model as Manual + Auto ISO. It adds no exposure equation of its own.
4874
5158
 
4875
5159
  The ideal shutter is:
4876
5160
 
@@ -4885,13 +5169,13 @@ ideal shutter =
4885
5169
  / fixed-axis factor
4886
5170
  ```
4887
5171
 
4888
- Aperture and ISO are validated against the same resolved #109 capability envelope and then preserved exactly.
5172
+ Aperture and ISO are checked against the same resolved capability envelope, then kept exactly.
4889
5173
 
4890
5174
  ### Shutter capability and quantization
4891
5175
 
4892
- For a continuous shutter grid, an in-range ideal duration is used directly.
5176
+ For a continuous shutter grid, an ideal duration inside the range is used as is.
4893
5177
 
4894
- For a discrete shutter grid, the first policy chooses the nearest duration in log2 exposure-time space. On an exact tie, the **shorter duration** is selected.
5178
+ For a discrete shutter grid, the first policy picks the nearest duration in log2 exposure-time space (nearest in stops). On an exact tie, the **shorter duration** wins.
4895
5179
 
4896
5180
  The result reports:
4897
5181
 
@@ -4917,21 +5201,21 @@ Residual sign is shared with the Manual resolver:
4917
5201
 
4918
5202
  ### Exposure compensation and no-signal
4919
5203
 
4920
- Exposure compensation is already encoded in the #100 target. Aperture Priority consumes that target and does not apply compensation again.
5204
+ Exposure compensation is already included in the meter target. Aperture Priority uses that target and does not apply compensation again.
4921
5205
 
4922
- A no-signal target blocks automatic shutter resolution. The engine does not select the longest shutter or an infinite duration.
5206
+ A no-signal target blocks automatic shutter resolution. The engine does not pick the longest shutter or an infinite duration.
4923
5207
 
4924
5208
  ### Boundaries
4925
5209
 
4926
- This slice intentionally keeps ISO manual.
5210
+ This resolver deliberately keeps ISO manual.
4927
5211
 
4928
- Aperture Priority + Auto ISO is later work because two automatic axes require an explicit selection policy such as a minimum-shutter rule. The engine must not assume ISO always moves before or after shutter.
5212
+ Aperture Priority + Auto ISO is a separate resolver ([next section](#priority-modes-with-auto-iso)), because two automatic settings need an explicit selection policy, such as a minimum-shutter rule. The engine must not assume that ISO always moves before, or after, the shutter.
4929
5213
 
4930
- Flash-aware shutter restrictions, safety shift, and manufacturer-specific program behavior also remain separate.
5214
+ Flash-aware shutter limits, safety shift and manufacturer-specific program behaviour are also separate.
4931
5215
 
4932
5216
  ## Priority modes with Auto ISO
4933
5217
 
4934
- The larger #99 priority group adds the remaining A/S combinations before Program/Full Auto.
5218
+ These resolvers add the remaining Aperture (A) and Shutter (S) Priority combinations.
4935
5219
 
4936
5220
  ### Aperture Priority + Auto ISO
4937
5221
 
@@ -4970,7 +5254,7 @@ The policy is explicit:
4970
5254
  - `allow-slower-shutter`; or
4971
5255
  - `hold-preferred-shutter` and return residual underexposure.
4972
5256
 
4973
- For discrete shutter grids, the pre-ISO shutter selection is never longer than the requested/preferred duration. This prevents a quantization choice from requiring an ISO below the supported minimum to undo overexposure.
5257
+ For discrete shutter grids, the shutter chosen before ISO is raised is never longer than the requested or preferred duration. Otherwise rounding to the grid could overexpose, and undoing that would need an ISO below the supported minimum.
4974
5258
 
4975
5259
  ### Shutter Priority + manual ISO
4976
5260
 
@@ -4992,9 +5276,9 @@ const resolved =
4992
5276
  });
4993
5277
  ```
4994
5278
 
4995
- Shutter and ISO remain fixed. Aperture alone resolves.
5279
+ Shutter and ISO stay fixed; only the aperture is resolved.
4996
5280
 
4997
- Continuous aperture capability uses the ideal f-number directly. Discrete aperture grids choose the nearest log2 f-number, with the narrower aperture on an exact tie.
5281
+ With continuous aperture capability, the ideal f-number is used as is. Discrete aperture grids pick the nearest f-number in log2 space, with the narrower aperture winning an exact tie.
4998
5282
 
4999
5283
  ### Shutter Priority + Auto ISO
5000
5284
 
@@ -5030,26 +5314,26 @@ This policy:
5030
5314
  3. for a discrete aperture grid, chooses an aperture no wider than the ideal;
5031
5315
  4. uses Auto ISO to fill the remaining exposure.
5032
5316
 
5033
- That direction is deliberate: choosing an aperture wider than the ideal at minimum ISO could overexpose while leaving no lower ISO available to compensate.
5317
+ That direction is deliberate. Choosing an aperture wider than the ideal at minimum ISO could overexpose, with no lower ISO left to compensate.
5034
5318
 
5035
5319
  ### Shared boundaries
5036
5320
 
5037
- These are control policies over the same relative exposure equation. They do not create new optics/sensor physics.
5321
+ These are control policies over the same relative exposure equation. They create no new optics or sensor physics.
5038
5322
 
5039
5323
  All modes:
5040
5324
 
5041
- - consume the already-compensated #100 target;
5042
- - consume the resolved #109 capability envelope;
5325
+ - use the meter target, with compensation already applied;
5326
+ - use the resolved capability envelope;
5043
5327
  - use the explicit reference exposure anchor;
5044
- - preserve residual-stop sign conventions;
5045
- - do not infer ISO noise/gain topology;
5328
+ - keep the same sign convention for residual stops;
5329
+ - do not infer ISO noise or gain topology;
5046
5330
  - do not apply flash policy or safety shift.
5047
5331
 
5048
- Flash-aware behavior, safety shift, and named-camera program behavior remain separate work.
5332
+ Flash-aware behaviour, safety shift and the program behaviour of named cameras are separate work.
5049
5333
 
5050
5334
  ## Program Auto and Full Auto exposure
5051
5335
 
5052
- Program Auto and Full Auto exposure both require a declared, versioned **program line**. The engine does not invent a unique "correct" aperture/shutter pair.
5336
+ Program Auto and Full Auto exposure both need a declared, versioned **program line**: a curve that says which aperture and shutter to use at each brightness level. The engine does not invent one "correct" aperture/shutter pair.
5053
5337
 
5054
5338
  ### Generic program line
5055
5339
 
@@ -5108,9 +5392,9 @@ const programLine =
5108
5392
  });
5109
5393
  ```
5110
5394
 
5111
- Each node's declared stop coordinate must match its aperture/shutter optical exposure relative to the same `referenceExposure` supplied to #99. Nodes outside the resolved #109 aperture/shutter envelope fail closed.
5395
+ Each node's declared stop value must match its aperture/shutter optical exposure, relative to the same `referenceExposure` given to the exposure resolver. Nodes outside the resolved aperture/shutter capability envelope fail closed.
5112
5396
 
5113
- Between nodes, aperture and shutter interpolate in log2 space. This keeps optical exposure stops linear across the segment before equipment quantization.
5397
+ Between nodes, aperture and shutter interpolate in log2 space. That keeps the optical exposure, in stops, linear along each segment, before rounding to the equipment's settings.
5114
5398
 
5115
5399
  ### Program Auto with manual ISO
5116
5400
 
@@ -5128,9 +5412,9 @@ const resolved =
5128
5412
  });
5129
5413
  ```
5130
5414
 
5131
- Aperture and shutter are automatic; ISO is preserved exactly after capability validation.
5415
+ Aperture and shutter are automatic. ISO is kept exactly, after it is checked against the capabilities.
5132
5416
 
5133
- If the target falls beyond the first/last program-line node, the line clamps to the endpoint and the result reports residual under/over-exposure.
5417
+ If the target falls beyond the first or last program-line node, the line stops at that endpoint, and the result reports the remaining under- or over-exposure.
5134
5418
 
5135
5419
  ### Program Auto with Auto ISO
5136
5420
 
@@ -5157,7 +5441,7 @@ The program line is first evaluated at the minimum selectable ISO. Auto ISO then
5157
5441
  - aperture/shutter quantization;
5158
5442
  - capability clamping.
5159
5443
 
5160
- If the program line produces too much exposure even at minimum ISO, ISO clamps at minimum and the residual remains visible.
5444
+ If the program line gives too much exposure even at minimum ISO, ISO stays at the minimum and the residual is reported.
5161
5445
 
5162
5446
  ### Full Auto exposure
5163
5447
 
@@ -5190,33 +5474,33 @@ It explicitly does **not** resolve:
5190
5474
  - scene recognition;
5191
5475
  - stabilization policy.
5192
5476
 
5193
- Those fields remain false in the result so a calling application cannot accidentally treat this engine mode as whole-camera automation.
5477
+ Those fields stay false in the result, so an application cannot accidentally treat this mode as automation of the whole camera.
5194
5478
 
5195
5479
  ### Program/Full Auto consistency
5196
5480
 
5197
5481
  Given the same:
5198
5482
 
5199
- - #100 exposure target;
5200
- - #109 equipment capabilities;
5483
+ - exposure target;
5484
+ - equipment capabilities;
5201
5485
  - reference exposure;
5202
5486
  - program line;
5203
5487
  - Auto ISO policy;
5204
5488
 
5205
- Program Auto + Auto ISO and Full Auto exposure should resolve identical aperture/shutter/ISO values.
5489
+ Program Auto + Auto ISO and Full Auto exposure should give identical aperture, shutter and ISO values.
5206
5490
 
5207
- Only the control-mode identity differs.
5491
+ Only the name of the control mode differs.
5208
5492
 
5209
5493
  ### Boundaries
5210
5494
 
5211
- Program lines are product policy approximations, not physical laws. Generic lines must not be described as reproducing a specific manufacturer unless exact evidence supports that claim.
5495
+ Program lines are approximations of product policy, not physical laws. Do not describe a generic line as reproducing a specific manufacturer unless exact evidence supports that claim.
5212
5496
 
5213
- Flash-aware program changes, safety shift, and broader Full Auto camera decisions remain separate work.
5497
+ Flash-aware program changes, safety shift and wider Full Auto camera decisions are separate work.
5214
5498
 
5215
5499
  ## Bulb, Time, and long-exposure control
5216
5500
 
5217
- Bulb and Time are **duration-control behaviors**, not alternate exposure equations.
5501
+ Bulb and Time are **ways of controlling duration**, not different exposure equations. (Bulb: the shutter stays open while the button is held. Time: one press opens it, a second press closes it.)
5218
5502
 
5219
- Use `resolveExposureDurationControl()` to turn user/control events into the concrete positive seconds required by image formation.
5503
+ Use `resolveExposureDurationControl()` to turn user or control events into the concrete, positive number of seconds that image formation needs.
5220
5504
 
5221
5505
  ### Fixed duration
5222
5506
 
@@ -5275,9 +5559,9 @@ const time =
5275
5559
  });
5276
5560
  ```
5277
5561
 
5278
- Bulb and Time require finite, ordered timestamps from the same explicit monotonic-seconds control reference.
5562
+ Bulb and Time need finite timestamps, in order, from the same explicit monotonic-seconds control clock.
5279
5563
 
5280
- An unfinished Bulb/Time control cannot enter the physical exposure pipeline.
5564
+ An unfinished Bulb or Time control cannot enter the physical exposure pipeline.
5281
5565
 
5282
5566
  ### Bind into authoritative exposure timing
5283
5567
 
@@ -5325,9 +5609,9 @@ It does **not** change or infer:
5325
5609
  - sensor temperature/heating;
5326
5610
  - flash behavior.
5327
5611
 
5328
- There is no universal 30-second cap for Bulb/Time. A completed 300-second Bulb exposure resolves to 300 seconds.
5612
+ There is no universal 30-second cap for Bulb or Time. A completed 300-second Bulb exposure resolves to 300 seconds.
5329
5613
 
5330
- Once resolved, that duration flows through the same existing/future physical path as any fixed duration:
5614
+ Once resolved, that duration follows the same physical path, now and in future, as any fixed duration:
5331
5615
 
5332
5616
  ```text
5333
5617
  resolved elapsed seconds
@@ -5343,9 +5627,9 @@ There is no Bulb-specific physics shortcut.
5343
5627
 
5344
5628
  ### Mode-invariant downstream physics
5345
5629
 
5346
- Shooting-mode names are control policy only.
5630
+ Shooting-mode names are only control policy.
5347
5631
 
5348
- If Manual, Program, and Full Auto exposure resolve the same:
5632
+ If Manual, Program and Full Auto exposure resolve the same:
5349
5633
 
5350
5634
  - aperture;
5351
5635
  - shutter duration;
@@ -5353,7 +5637,7 @@ If Manual, Program, and Full Auto exposure resolve the same:
5353
5637
 
5354
5638
  then `calculateRelativeRenderedExposure()` and all later physical models receive identical physical inputs and must produce identical results.
5355
5639
 
5356
- Likewise, equal fixed/Bulb/Time elapsed durations with the same shutter/timing inputs produce identical authoritative exposure windows.
5640
+ Likewise, equal elapsed durations from fixed, Bulb or Time control, with the same shutter and timing inputs, give identical authoritative exposure windows.
5357
5641
 
5358
5642
  ## Exposure and ISO relations
5359
5643
 
@@ -5386,7 +5670,7 @@ console.log(exposure.value.factor);
5386
5670
  console.log(exposure.value.stops);
5387
5671
  ```
5388
5672
 
5389
- Use `calculateRelativeRenderedExposure()` when a renderer needs a nominal linear brightness multiplier that combines the aperture/shutter optical-exposure change with ISO gain relative to declared reference settings:
5673
+ Use `calculateRelativeRenderedExposure()` when a renderer needs a nominal linear brightness multiplier. It combines the change in optical exposure (aperture and shutter) with ISO gain, relative to declared reference settings:
5390
5674
 
5391
5675
  ```ts
5392
5676
  import { calculateRelativeRenderedExposure } from "@photivra/engine";
@@ -5406,9 +5690,9 @@ console.log(rendered.value.opticalFactor); // 0.5
5406
5690
  console.log(rendered.value.isoGainFactor); // 2
5407
5691
  ```
5408
5692
 
5409
- This is a relative rendering relation, not a radiometric sensor model. ISO is treated as nominal rendering gain; it does not create photons, and the function does not model clipping, tone mapping, lens transmission, or sensor-specific noise behavior.
5693
+ This is a relative rendering relation, not a radiometric sensor model. ISO is treated as nominal rendering gain; it does not create photons. The function does not model clipping, tone mapping, lens transmission or sensor-specific noise.
5410
5694
 
5411
- Use `calculateEquivalentIso()` to calculate the nominal ISO/gain compensation needed to preserve rendered exposure:
5695
+ Use `calculateEquivalentIso()` to calculate the nominal ISO (gain) change needed to keep the rendered exposure the same:
5412
5696
 
5413
5697
  ```ts
5414
5698
  import { calculateEquivalentIso } from "@photivra/engine";
@@ -5426,11 +5710,11 @@ console.log(iso.value);
5426
5710
 
5427
5711
  These relations do not model scene radiance, lens T-stop/transmission, vignetting, or sensor-specific gain/noise behavior.
5428
5712
 
5429
- See [Motion and Signal Foundation](MOTION_AND_SIGNAL.md#exposure-relations).
5713
+ See [Motion and Signal Foundation](https://github.com/Photivra/photivra/blob/main/docs/MOTION_AND_SIGNAL.md#exposure-relations).
5430
5714
 
5431
5715
  ## Capture rotation exposure trajectories
5432
5716
 
5433
- Use `calculateCaptureRotationTrajectories()` when you need to evaluate **pure camera rotation over each native point's local exposure interval** while preserving the separation between shutter/exposure timing and sensor readout:
5717
+ Use `calculateCaptureRotationTrajectories()` to evaluate **pure camera rotation over each native point's own local exposure interval**. Shutter/exposure timing and sensor readout stay separate:
5434
5718
 
5435
5719
  ```ts
5436
5720
  import { calculateCaptureRotationTrajectories } from "@photivra/engine";
@@ -5514,27 +5798,27 @@ const trajectories = calculateCaptureRotationTrajectories({
5514
5798
  console.log(trajectories.value.samples);
5515
5799
  ```
5516
5800
 
5517
- The function maps each native point to the pre-orientation image plane at the capture reference time, then evaluates the existing `calculateCameraRotationImageMapping()` primitive at that point's local exposure start and end.
5801
+ The function maps each native point to the image plane (before orientation) at the capture reference time. It then evaluates the existing `calculateCameraRotationImageMapping()` primitive at that point's local exposure start and end.
5518
5802
 
5519
- The explicit time reference is the **first opening-boundary phase**. For this composition only, that reference is bound to `t = 0` of the low-level rotation model. This does not redefine the standalone rotation API for other callers.
5803
+ The explicit time reference is the **first opening-boundary phase**. For this composition only, that reference is tied to `t = 0` of the low-level rotation model. This does not change the standalone rotation API for other callers.
5520
5804
 
5521
- The returned trajectory is **forward temporal geometry for a stationary world ray**:
5805
+ The returned trajectory is the **forward path over time of one stationary ray from the world**:
5522
5806
 
5523
5807
  - `referenceImagePointMm` is the ray's image-plane position at the capture reference;
5524
5808
  - `atLocalExposureStart` and `atLocalExposureEnd` are its mapped positions at the local window endpoints;
5525
5809
  - `localExposureTrajectoryEndpointDeltaMm` is the chord between those endpoints.
5526
5810
 
5527
- The chord is not an integrated blur kernel. It also is not a renderer-ready rolling-shutter warp.
5811
+ The chord is not an integrated blur kernel. Nor is it a rolling-shutter warp ready for a renderer.
5528
5812
 
5529
- A capture-scan image mapping is location-dependent because capture time varies across the image. For the current pure-rotation model, destination-to-reference inversion is analytic once the destination location selects its local capture time. More general motion models may require additional solving and global-validity analysis. Finite-exposure rendering still requires integration over the full local interval rather than one endpoint or midpoint displacement.
5813
+ When the capture scans across the sensor, the image mapping depends on location, because capture time varies across the image. For the current pure-rotation model, going back from destination to reference is analytic once the destination location has picked its local capture time. More general motion models may need extra solving and checks that the mapping is valid everywhere. Rendering a finite exposure still needs integration over the whole local interval, not one endpoint or midpoint displacement.
5530
5814
 
5531
- `calculateSensorReadoutTiming()` is intentionally not an input. If a specific capture mode has a defensible relationship between electronic exposure timing and sensor readout timing, that relationship should be represented by a future explicit mode/link contract rather than assumed by this API.
5815
+ `calculateSensorReadoutTiming()` is deliberately not an input. If a specific capture mode has a defensible relationship between electronic exposure timing and sensor readout timing, a future explicit mode/link contract should represent it. This API does not assume it.
5532
5816
 
5533
- The current trajectory model remains pure rotation only. It excludes camera translation/parallax, subject motion, rotating/deforming subjects, occlusion changes, panning intent, exposure integration, flash/flicker, shutter shock, and final rolling-shutter inverse warping.
5817
+ The trajectory model is pure rotation only. It leaves out camera translation/parallax, subject motion, rotating or deforming subjects, occlusion changes, panning intent, exposure integration, flash/flicker, shutter shock and the final inverse rolling-shutter warp.
5534
5818
 
5535
5819
  ## Capture rotation temporal quadrature
5536
5820
 
5537
- Use `calculateCaptureRotationTemporalQuadrature()` when a downstream renderer or reference evaluator needs deterministic pure-rotation geometry samples across each destination point's complete local exposure interval:
5821
+ Use `calculateCaptureRotationTemporalQuadrature()` when a renderer or reference evaluator needs deterministic geometry samples for pure rotation, spread over each destination point's whole local exposure interval:
5538
5822
 
5539
5823
  ```ts
5540
5824
  import { calculateCaptureRotationTemporalQuadrature } from "@photivra/engine";
@@ -5580,18 +5864,18 @@ The first model uses deterministic uniform midpoint quadrature. With `N` samples
5580
5864
  phase_i = (i + 0.5) / N
5581
5865
  ```
5582
5866
 
5583
- Each phase is passed through `calculateCaptureRotationInverseMappings()`, so instantaneous destination-to-reference geometry remains defined by one existing public model.
5867
+ Each phase is passed through `calculateCaptureRotationInverseMappings()`, so the instantaneous destination-to-reference geometry is defined by one existing public model.
5584
5868
 
5585
- Every node exposes two different temporal weights:
5869
+ Every node has two different time weights:
5586
5870
 
5587
- - `normalizedTimeWeight = 1 / N` for a downstream time-average under the declared uniform temporal-response approximation;
5588
- - `timeMeasureSeconds = localExposureDuration / N` for a downstream time integral.
5871
+ - `normalizedTimeWeight = 1 / N`, for a time average under the declared uniform temporal-response approximation;
5872
+ - `timeMeasureSeconds = localExposureDuration / N`, for a time integral.
5589
5873
 
5590
- Do not interchange them. The first is dimensionless; the second carries seconds. Neither includes shutter transmission, scene radiance, sensor response, photon conversion, vignetting, or other radiometric throughput.
5874
+ Do not swap them. The first is dimensionless; the second is in seconds. Neither includes shutter transmission, scene radiance, sensor response, photon conversion, vignetting or any other radiometric throughput.
5591
5875
 
5592
- Different sensor locations may have different local exposure start/end times. If opening and closing scans produce different local durations, `timeMeasureSeconds` therefore differs by destination point while the normalized average weights still sum to approximately one for each point.
5876
+ Different sensor locations may have different local exposure start and end times. If the opening and closing scans give different local durations, `timeMeasureSeconds` differs between destination points, while the normalized weights still sum to about one for each point.
5593
5877
 
5594
- This API returns **temporal geometry nodes and measures only**. It does not:
5878
+ This API returns **only time-sampled geometry and its weights**. It does not:
5595
5879
 
5596
5880
  - fetch or sample scene radiance;
5597
5881
  - average the reference coordinates;
@@ -5601,13 +5885,13 @@ This API returns **temporal geometry nodes and measures only**. It does not:
5601
5885
  - apply flash/flicker or shutter-shock behavior;
5602
5886
  - write or resample output pixels.
5603
5887
 
5604
- Averaging geometric coordinates is not equivalent to integrating radiance along moving rays.
5888
+ Averaging geometric coordinates is not the same as integrating radiance along moving rays.
5605
5889
 
5606
- Photivra also does not report a geometry-only integration-error estimate. Temporal image error depends on downstream radiance, visibility, texture/edge frequency, reconstruction, and the motion path. A reference renderer can compare increasing `temporalSampleCount` values in the radiance/output domain when convergence evidence is needed.
5890
+ Photivra also does not report an integration-error estimate from geometry alone. The error in the image depends on radiance, visibility, how fine the texture and edges are, reconstruction and the motion path. When convergence evidence is needed, a reference renderer can compare increasing `temporalSampleCount` values on the radiance or output.
5607
5891
 
5608
5892
  ## Instantaneous capture rotation inverse mapping
5609
5893
 
5610
- Use `calculateCaptureRotationInverseMappings()` to map captured destination locations back to the **first-opening-boundary reference image** under pure camera rotation:
5894
+ Use `calculateCaptureRotationInverseMappings()` to map captured destination locations back to the **reference image at the first opening boundary**, under pure camera rotation:
5611
5895
 
5612
5896
  ```ts
5613
5897
  import {
@@ -5701,9 +5985,9 @@ console.log(mapping.value.samples);
5701
5985
  - `0.5` evaluates the local temporal midpoint;
5702
5986
  - `1` evaluates local exposure end.
5703
5987
 
5704
- Photivra does not choose a default phase because a finite exposure does not have one uniquely correct sharp geometry.
5988
+ Photivra does not choose a default phase, because a finite exposure has no single correct sharp geometry.
5705
5989
 
5706
- Each destination native point first selects its local exposure time. Under the current constant-axis pure-rotation model, the captured ray can then be rotated **analytically** back to the exposure-start reference frame with `calculateInverseCameraRotationImageMapping()`. No iterative/fixed-point solver or convergence tolerance is needed.
5990
+ Each destination native point first picks its local exposure time. Under the current model (pure rotation about a constant axis), the captured ray can then be rotated **analytically** back to the exposure-start reference frame with `calculateInverseCameraRotationImageMapping()`. No iterative solver or convergence tolerance is needed.
5707
5991
 
5708
5992
  The result reports:
5709
5993
 
@@ -5714,19 +5998,19 @@ The result reports:
5714
5998
  - equivalent continuous native effective-sample displacement;
5715
5999
  - that displacement rotated into the requested physical capture orientation.
5716
6000
 
5717
- The mapping deliberately remains in the ideal pre-lens geometry stage. It does not apply radial distortion, lateral CA, PSF, output crop, or output resampling.
6001
+ The mapping deliberately stays in the ideal geometry stage, before the lens. It does not apply radial distortion, lateral CA, PSF, output crop or output resampling.
5718
6002
 
5719
- Reference rays are **not clamped** to the active source region. Camera motion can cause a valid captured destination to require reference-image data outside that region. A renderer must decide how to handle unavailable source coverage rather than silently clamping/stretching an edge.
6003
+ Reference rays are **not clamped** to the active source region. Camera motion can mean that a valid captured destination needs reference-image data from outside that region. A renderer must decide how to handle missing source coverage, rather than silently clamping or stretching an edge.
5720
6004
 
5721
- This is an **instantaneous** mapping only. It is not a complete finite-exposure rolling/capture-scan image. Motion blur requires multiple temporal samples or another defensible integration method over each local exposure window, including an explicit exposure-weighting model if the weighting is not uniform.
6005
+ This is an **instantaneous** mapping only. It is not a complete image of a finite rolling (scanned) exposure. Motion blur needs several time samples, or another defensible way of integrating over each local exposure window, including an explicit exposure-weighting model if the weighting is not uniform.
5722
6006
 
5723
- Sensor data-readout timing is intentionally not part of this calculation. Capture exposure boundaries remain the authoritative timing input until a separate capture-mode/link contract establishes otherwise.
6007
+ Sensor data-readout timing is deliberately not part of this calculation. The capture exposure boundaries stay the authoritative timing input, until a separate capture-mode/link contract says otherwise.
5724
6008
 
5725
- The current model is pure rotation only. Translation/parallax, subject motion, deformation, occlusion changes, global injectivity/fold analysis, flash/flicker, shutter shock, and finite-exposure integration remain outside this slice.
6009
+ The model is pure rotation only. Outside it: translation/parallax, subject motion, deformation, occlusion changes, checks that the mapping never folds over on itself, flash/flicker, shutter shock and finite-exposure integration.
5726
6010
 
5727
6011
  ## Spatial camera-rotation mapping
5728
6012
 
5729
- Use `calculateCameraRotationImageMapping()` when you need the image-plane location of a stationary world ray after **pure camera rotation** at an arbitrary physical time from exposure start.
6013
+ Use `calculateCameraRotationImageMapping()` when you need to know where a stationary ray from the world lands on the image plane after **pure camera rotation**, at any physical time after exposure start.
5730
6014
 
5731
6015
  ```ts
5732
6016
  import { calculateCameraRotationImageMapping } from "@photivra/engine";
@@ -5753,17 +6037,18 @@ console.log(mapped.value.deltaImagePlaneSamples);
5753
6037
 
5754
6038
  Coordinate/sign convention:
5755
6039
 
5756
- - camera axes at exposure start are +X right, +Y up, +Z forward;
5757
- - positive pitch/yaw/roll follow the right-hand rule about +X/+Y/+Z;
6040
+ - camera axes at exposure start are +X right, +Y up, +Z forward (a left-handed basis);
6041
+ - positive pitch/yaw/roll follow the physical right-hand rule about +X/+Y/+Z: positive yaw turns the camera left and moves stationary content toward +X; positive pitch tilts it up and moves content toward −Y; positive roll is clockwise as seen from behind the camera and rotates content counter-clockwise;
5758
6042
  - the returned image-plane basis is +X right, +Y up;
5759
- - a stationary world ray is transformed by the **inverse** camera rotation, so positive physical camera rotation generally moves scene imagery in the opposite screen direction;
6043
+ - a stationary world ray is transformed by the **inverse** camera rotation, so image content moves opposite to the camera's own turn;
6044
+ - `estimateCameraShakeBlur()` uses the same yaw/pitch signs (from 2.0.0); see [Motion and signal](https://github.com/Photivra/photivra/blob/main/docs/MOTION_AND_SIGNAL.md#spatial-camera-rotation);
5760
6045
  - optional `deltaImagePlaneSamples` retains image-plane +Y-up semantics and is **not** a native-raster (+Y-down) vector.
5761
6046
 
5762
- The angular-velocity vector is assumed constant and is integrated as one axis-angle rotation. This avoids arbitrary Euler ordering for simultaneous pitch/yaw/roll.
6047
+ The angular-velocity vector is assumed constant, and is integrated as one axis-angle rotation. This avoids picking an arbitrary Euler-angle order when pitch, yaw and roll happen at the same time.
5763
6048
 
5764
- The function is intentionally rotation-only. Camera translation is excluded because translational optical flow depends on scene depth/parallax.
6049
+ The function is deliberately rotation-only. Camera translation is left out, because image motion from translation depends on scene depth (parallax).
5765
6050
 
5766
- It is also separate from stabilization. `estimateCameraShakeBlur()` remains the older educational stabilization-equivalent approximation and keeps its existing global-vector semantics for compatibility.
6051
+ It is also separate from stabilization. `estimateCameraShakeBlur()` is still the older, educational stabilization-equivalent approximation with one global vector. From 2.0.0, its yaw/pitch signs match this mapping at the optical axis.
5767
6052
 
5768
6053
  ## Camera shake and stabilization-equivalent approximation
5769
6054
 
@@ -5790,13 +6075,20 @@ console.log(shake.value.residualMotionFactor);
5790
6075
  console.log(shake.provenance.kind); // "approximation"
5791
6076
  ```
5792
6077
 
5793
- The `2^-stops` attenuation is an educational approximation, not a real IBIS/OIS or CIPA rating. The current output is one global image-plane vector rather than spatially varying rotational optical flow.
6078
+ `deltaXmm`/`deltaYmm` are the image displacement (+X right, +Y up) of a stationary point on the optical axis.
6079
+
6080
+ - Positive yaw (camera turns left) gives positive `deltaXmm`.
6081
+ - Positive pitch (camera tilts up) gives negative `deltaYmm`.
6082
+ - These signs match `calculateCameraRotationImageMapping()` at the optical axis. For a single axis, the displacements are equal within floating-point rounding.
6083
+ - A per-axis rotation of π/2 rad or more over the exposure is rejected.
6084
+
6085
+ The `2^-stops` attenuation is an educational approximation, not a real IBIS/OIS (in-body / optical image stabilization) or CIPA rating. The output is one global image-plane vector, not image motion that varies across the frame.
5794
6086
 
5795
- See [Camera Shake and Stabilization](STABILIZATION.md).
6087
+ See [Camera Shake and Stabilization](https://github.com/Photivra/photivra/blob/main/docs/STABILIZATION.md).
5796
6088
 
5797
6089
  ## Radiometry readiness
5798
6090
 
5799
- Use `parseRadiometryReadinessProfile()` and `assessRadiometryReadiness()` to validate whether a declared radiometric calibration package contains the prerequisites needed for a future photon estimate.
6091
+ Use `parseRadiometryReadinessProfile()` and `assessRadiometryReadiness()` to check whether a declared radiometric calibration package has the prerequisites a future photon estimate would need.
5800
6092
 
5801
6093
  The gate covers six independently declared requirements:
5802
6094
 
@@ -5850,21 +6142,21 @@ console.log(readiness.composedPhotonOutputEnabled); // always false
5850
6142
 
5851
6143
  Readiness states are:
5852
6144
 
5853
- - `not-ready`: one or more required prerequisite categories are missing;
5854
- - `approximate-only`: every category is present, but at least one component is approximate or has unquantified uncertainty;
6145
+ - `not-ready`: one or more required categories are missing;
6146
+ - `approximate-only`: every category is present, but at least one component is approximate or has no quantified uncertainty;
5855
6147
  - `calibrated-ready`: every category is declared calibrated and carries quantified uncertainty.
5856
6148
 
5857
- A readiness assessment validates the declared structure and provenance metadata. It does not verify that a source or calibration is scientifically correct.
6149
+ A readiness assessment checks the declared structure and provenance metadata. It does not check that a source or calibration is scientifically correct.
5858
6150
 
5859
- Geometric sample pitch is **not** accepted as photon-collection area by itself. The profile must declare either an effective collection area or a geometric cell area plus explicit fill factor.
6151
+ Geometric sample pitch alone is **not** accepted as photon-collection area. The profile must declare either an effective collection area, or a geometric cell area plus an explicit fill factor.
5860
6152
 
5861
- Data-bearing spectral/spatial calibration inputs are represented by an artifact ID plus SHA-256 checksum. Photivra does not infer reuse rights from public availability; reusable calibration data requires explicit licensing or Photivra ownership.
6153
+ Spectral and spatial calibration inputs that carry data are referenced by an artifact ID plus a SHA-256 checksum. Photivra does not assume reuse rights because something is publicly available. Reusable calibration data needs an explicit licence or Photivra ownership.
5862
6154
 
5863
- The readiness API does not calculate photons, does not change the existing `calculatePhotoelectrons()` primitive, and does not add photon/noise output to `simulatePocCamera()`. The bounded [executed environment route](PRODUCTION_ENVIRONMENT_CAPTURE.md) performs explicitly declared EQE photon/electron integration; this readiness gate alone does not activate it or verify calibration.
6155
+ The readiness API does not calculate photons, does not change the existing `calculatePhotoelectrons()` primitive, and does not add photon/noise output to `simulatePocCamera()`. The bounded [executed environment route](https://github.com/Photivra/photivra/blob/main/docs/PRODUCTION_ENVIRONMENT_CAPTURE.md) does explicitly declared EQE photon/electron integration. This readiness gate alone neither turns it on nor checks calibration.
5864
6156
 
5865
6157
  ## Photoelectron and SNR primitives
5866
6158
 
5867
- Use `calculatePhotoelectrons()` only when you already have a defensible mean incident photon count and quantum efficiency:
6159
+ Use `calculatePhotoelectrons()` only when you already have a defensible mean count of incident photons and a quantum efficiency (the fraction of photons that produce an electron):
5868
6160
 
5869
6161
  ```ts
5870
6162
  import { calculatePhotoelectrons } from "@photivra/engine";
@@ -5877,7 +6169,7 @@ const electrons = calculatePhotoelectrons({
5877
6169
  console.log(electrons.value);
5878
6170
  ```
5879
6171
 
5880
- Use `calculateSignalToNoise()` for the basic Poisson-shot-noise plus independent RMS read-noise model:
6172
+ Use `calculateSignalToNoise()` for the basic signal-to-noise ratio (SNR) model: Poisson shot noise plus independent RMS read noise:
5881
6173
 
5882
6174
  ```ts
5883
6175
  import { calculateSignalToNoise } from "@photivra/engine";
@@ -5895,11 +6187,11 @@ console.log(snr.value.snrDb);
5895
6187
 
5896
6188
  These primitives do not derive photons from scene imagery and do not model a complete commercial sensor pipeline.
5897
6189
 
5898
- See [Motion and Signal Foundation](MOTION_AND_SIGNAL.md#signalnoise-primitives).
6190
+ See [Motion and Signal Foundation](https://github.com/Photivra/photivra/blob/main/docs/MOTION_AND_SIGNAL.md#signalnoise-primitives).
5899
6191
 
5900
6192
  ## Scene illumination source profiles
5901
6193
 
5902
- Use `parseSceneIlluminationProfile()` when illumination metadata crosses a renderer/integration boundary:
6194
+ Use `parseSceneIlluminationProfile()` to read light-source metadata that comes from a renderer or another integration:
5903
6195
 
5904
6196
  ```ts
5905
6197
  import { parseSceneIlluminationProfile } from "@photivra/engine";
@@ -5953,26 +6245,35 @@ console.log(illumination.sources[0]?.sourceId);
5953
6245
  console.log(illumination.sceneRadianceCalculated); // false
5954
6246
  ```
5955
6247
 
5956
- The schema intentionally has no generic `intensity` field. Physical magnitude forms are source-specific:
6248
+ The schema deliberately has no generic `intensity` field. How brightness is stated depends on the kind of source:
5957
6249
 
5958
6250
  - point sources may declare radiant intensity in W/sr;
5959
6251
  - area sources may declare surface radiance in W/(m²·sr);
5960
6252
  - directional sources may declare irradiance in W/m² at an explicit reference plane;
5961
- - spot and environment sources are relative-only in schema 0.1.0 until their angular/radiance-field semantics are modeled.
6253
+ - spot and environment sources are relative-only in schema 0.1.0, until their angular and radiance-field meaning is modelled.
5962
6254
 
5963
- All source families may use an explicitly approximate relative linear scale. Turning a source off uses `enabled: false`; a zero physical/relative magnitude is not used as a hidden off state.
6255
+ Every kind of source may use an explicitly approximate, relative linear scale. To turn a source off, use `enabled: false`. A zero magnitude is never used as a hidden "off" state.
5964
6256
 
5965
- Spectrum representations are likewise explicit. Linear-sRGB is preview-only. A blackbody temperature is a declared blackbody approximation, not a universal CCT control. Continuous relative spectra require reusable-data provenance and do not become absolute spectral power/radiance merely because a physical broadband magnitude also exists.
6257
+ Spectra are explicit too:
5966
6258
 
5967
- Discrete sources may instead use `discrete-relative-lines`. Each line carries an explicit wavelength and a normalized **wavelength-integrated** line weight; line weights must sum to one and use a resolved air/vacuum wavelength basis. These values are not per-nanometre densities and are never assigned an arbitrary line width. `resolveSceneIlluminationDiscreteLineMeasure()` can distribute a source's integrated magnitude across those line fractions while preserving its source quantity unit.
6259
+ - Linear sRGB is for previews only.
6260
+ - A blackbody temperature is a declared blackbody approximation, not a universal CCT control.
6261
+ - Continuous relative spectra need reusable-data provenance. They do not become absolute spectral power or radiance just because a physical broadband magnitude also exists.
5968
6262
 
5969
- The parser establishes illumination metadata only. It does not calculate material response, visibility, indirect transport, outgoing scene spectral radiance, sensor-plane irradiance, photons/electrons, fluorescence, volumetric spectral transport, or polarization.
6263
+ Sources with discrete spectral lines may use `discrete-relative-lines` instead.
5970
6264
 
5971
- See [Scene Radiance and Illumination](SCENE_RADIANCE_AND_ILLUMINATION.md).
6265
+ - Each line has an explicit wavelength and a normalized, **wavelength-integrated** line weight.
6266
+ - Line weights must sum to one and use a resolved air/vacuum wavelength basis.
6267
+ - These values are not per-nanometre densities, and a line is never given an arbitrary width.
6268
+ - `resolveSceneIlluminationDiscreteLineMeasure()` can distribute a source's integrated magnitude across those line fractions, keeping the source's unit.
6269
+
6270
+ The parser only sets up light-source metadata. It does not calculate material response, visibility, indirect light, outgoing scene spectral radiance, sensor-plane irradiance, photons/electrons, fluorescence, volumetric spectral transport or polarization.
6271
+
6272
+ See [Scene Radiance and Illumination](https://github.com/Photivra/photivra/blob/main/docs/SCENE_RADIANCE_AND_ILLUMINATION.md).
5972
6273
 
5973
6274
  ## Scene-radiance provider and material response
5974
6275
 
5975
- Use the provider boundary when a renderer or reference evaluator needs to return one outgoing spectral-radiance sample without moving its rendering algorithm into the engine.
6276
+ A scene-radiance provider is the renderer (or reference evaluator) that works out how much light leaves a point in the scene. Use this provider boundary when it needs to hand back one sample of outgoing spectral radiance, without moving its rendering algorithm into the engine.
5976
6277
 
5977
6278
  First define material-response metadata:
5978
6279
 
@@ -6067,17 +6368,22 @@ const provider = parseSceneRadianceProviderProfile({
6067
6368
  });
6068
6369
  ```
6069
6370
 
6070
- An evaluation request identifies one exact surface/environment direction, physical time, and wavelength. The renderer/provider computes the radiance externally and returns a result in `W/m^2/sr/nm`. Parse both with `parseSceneRadianceEvaluationRequest()` and `parseSceneRadianceEvaluationResult()`, then call `validateSceneRadianceEvaluationBindings()` with the provider, illumination profile, material profile, request, and result.
6371
+ How an evaluation works:
6372
+
6373
+ 1. A request names one exact surface or environment direction, a physical time and a wavelength.
6374
+ 2. The renderer/provider computes the radiance outside the engine, and returns a result in `W/m^2/sr/nm`.
6375
+ 3. Parse both with `parseSceneRadianceEvaluationRequest()` and `parseSceneRadianceEvaluationResult()`.
6376
+ 4. Call `validateSceneRadianceEvaluationBindings()` with the provider, illumination profile, material profile, request and result.
6071
6377
 
6072
- The binding validator checks exact scene/profile/material/sample/wavelength identity and provider material-fidelity consistency. It **does not** recompute the renderer's numeric result, apply optics, calculate sensor-plane irradiance, or authorize photon output.
6378
+ The binding validator checks the exact scene, profile, material, sample and wavelength identity, and that the provider's material fidelity is consistent. It **does not** recompute the renderer's number, apply optics, calculate sensor-plane irradiance or allow photon output.
6073
6379
 
6074
- Provider/result schema 0.1.0 is approximation-only. Calibrated material or illumination inputs cannot silently promote provider output to calibrated scene radiance.
6380
+ Provider/result schema 0.1.0 is approximation-only. Calibrated material or illumination inputs cannot silently turn provider output into calibrated scene radiance.
6075
6381
 
6076
- See [Scene Radiance and Illumination](SCENE_RADIANCE_AND_ILLUMINATION.md#scene-radiance-provider-and-material-response-boundary).
6382
+ See [Scene Radiance and Illumination](https://github.com/Photivra/photivra/blob/main/docs/SCENE_RADIANCE_AND_ILLUMINATION.md#scene-radiance-provider-and-material-response-boundary).
6077
6383
 
6078
6384
  ## Shared spectral composition
6079
6385
 
6080
- Use `composeSpectralCoverage()` when multiple **continuous** wavelength-dependent factors need one common coverage/breakpoint plan.
6386
+ Use `composeSpectralCoverage()` when several **continuous** wavelength-dependent factors (scene, lens, sensor) need one shared plan of wavelength coverage and breakpoints.
6081
6387
 
6082
6388
  Composition fragment: requires previously validated bindings `spectralResponseProfile`. This is an integration sketch, not a standalone runnable program.
6083
6389
 
@@ -6145,11 +6451,11 @@ The composition:
6145
6451
  - does not apply scene values, optical transmission, QE, or A/W responsivity;
6146
6452
  - does not perform spectral integration or claim convergence.
6147
6453
 
6148
- Continuous illumination spectra can be converted to the same participant shape with `createSceneIlluminationSpectralCoverageParticipant()`. RGB, blackbody approximations, unresolved spectra, and discrete line spectra are rejected by that adapter.
6454
+ Continuous illumination spectra can be converted to the same participant shape with `createSceneIlluminationSpectralCoverageParticipant()`. That adapter rejects RGB, blackbody approximations, unresolved spectra and discrete line spectra.
6149
6455
 
6150
6456
  ### Discrete line measures
6151
6457
 
6152
- Discrete lines use a different integration path.
6458
+ Discrete spectral lines (as from a laser or some lamps) are integrated a different way.
6153
6459
 
6154
6460
  Composition fragment: requires previously validated bindings `illumination`. This is an integration sketch, not a standalone runnable program.
6155
6461
 
@@ -6188,21 +6494,21 @@ if (
6188
6494
  }
6189
6495
  ```
6190
6496
 
6191
- A discrete line entry already represents an integrated wavelength contribution. Therefore line integration is:
6497
+ A discrete line entry already is a wavelength-integrated contribution. So integrating lines is just:
6192
6498
 
6193
6499
  ```text
6194
6500
  total = sum(line integrated quantities)
6195
6501
  ```
6196
6502
 
6197
- There is **no** `dλ` multiplication. Assigning an arbitrary width to a laser/line emitter merely to reuse continuous quadrature is invalid.
6503
+ There is **no** multiplication by `dλ`. Giving a laser or line emitter an arbitrary width, just to reuse continuous quadrature, is invalid.
6198
6504
 
6199
- The source-level helper may distribute point radiant intensity (W/sr), area radiance (W/m²/sr), directional reference-plane irradiance (W/m²), or an explicitly relative magnitude across normalized line fractions. It does not apply material transport, optics, sensor response, photon conversion, or combined uncertainty, and it does not authorize a calibrated outgoing-radiance claim.
6505
+ The source-level helper may distribute point radiant intensity (W/sr), area radiance (W/m²/sr), directional reference-plane irradiance (W/m²) or an explicitly relative magnitude across normalized line fractions. It does not apply material transport, optics, sensor response, photon conversion or combined uncertainty, and it does not allow a claim of calibrated outgoing radiance.
6200
6506
 
6201
- See [Scene Radiance and Illumination](SCENE_RADIANCE_AND_ILLUMINATION.md#shared-spectral-composition).
6507
+ See [Scene Radiance and Illumination](https://github.com/Photivra/photivra/blob/main/docs/SCENE_RADIANCE_AND_ILLUMINATION.md#shared-spectral-composition).
6202
6508
 
6203
6509
  ## Temporal illumination
6204
6510
 
6205
- Use `parseSceneIlluminationTemporalProfile()` to add explicit flash/flicker-style modulation to sources without changing the base illumination schema.
6511
+ Use `parseSceneIlluminationTemporalProfile()` to add explicit changes over time — such as a flash pulse or flicker — to light sources, without changing the base illumination schema.
6206
6512
 
6207
6513
  Composition fragment: requires previously validated bindings `illuminationProfile`. This is an integration sketch, not a standalone runnable program.
6208
6514
 
@@ -6296,19 +6602,19 @@ console.log(
6296
6602
  );
6297
6603
  ```
6298
6604
 
6299
- Waveform time is explicitly registered to seconds from the first opening-boundary phase. Sensor readout duration is not used as a substitute for exposure timing.
6605
+ Waveform time is explicitly tied to seconds from the first opening-boundary phase. Sensor readout duration is not used in place of exposure timing.
6300
6606
 
6301
- For finite exposure, use `integrateSceneIlluminationTemporalMultiplierOverExposureWindow()` with the authoritative `CaptureExposureWindows` result and one sample index. The integration uses deterministic midpoint quadrature over that local exposure window and returns average and seconds-integrated relative multipliers.
6607
+ For a finite exposure, use `integrateSceneIlluminationTemporalMultiplierOverExposureWindow()` with the authoritative `CaptureExposureWindows` result and one sample index. It uses deterministic midpoint quadrature over that local exposure window, and returns the average and the seconds-integrated relative multipliers.
6302
6608
 
6303
- This layer does not apply the source's physical magnitude, evaluate material/visibility transport, calculate scene radiance, perform sensor response, or choose camera exposure settings.
6609
+ This layer does not apply the source's physical magnitude, evaluate material or visibility transport, calculate scene radiance, apply sensor response or choose camera exposure settings.
6304
6610
 
6305
6611
  A scene-radiance provider may optionally bind the temporal profile with `illuminationTemporalProfileId`. When declared, `validateSceneRadianceEvaluationBindings()` requires the matching temporal profile.
6306
6612
 
6307
- See [Scene Radiance and Illumination](SCENE_RADIANCE_AND_ILLUMINATION.md#temporal-illumination).
6613
+ See [Scene Radiance and Illumination](https://github.com/Photivra/photivra/blob/main/docs/SCENE_RADIANCE_AND_ILLUMINATION.md#temporal-illumination).
6308
6614
 
6309
6615
  ## Camera and scene schema validation
6310
6616
 
6311
- Use the runtime parsers when camera or scene data crosses an untrusted JSON boundary.
6617
+ Use the runtime parsers whenever camera or scene data comes from untrusted JSON.
6312
6618
 
6313
6619
  Composition fragment: requires previously validated bindings `cameraJson`, `sceneJson`. This is an integration sketch, not a standalone runnable program.
6314
6620
 
@@ -6322,7 +6628,7 @@ const camera = parseCameraConfiguration(JSON.parse(cameraJson));
6322
6628
  const scene = parseSceneDefinition(JSON.parse(sceneJson));
6323
6629
  ```
6324
6630
 
6325
- The parsers throw `InvalidConfigurationError` when the supplied structure or supported values are invalid.
6631
+ The parsers throw `InvalidConfigurationError` when the structure, or one of the values, is invalid or unsupported.
6326
6632
 
6327
6633
  Example camera shape:
6328
6634
 
@@ -6353,7 +6659,7 @@ const camera = parseCameraConfiguration({
6353
6659
  });
6354
6660
  ```
6355
6661
 
6356
- See [Validation boundaries in the README](../README.md#validation-boundaries) and [Architecture](ARCHITECTURE.md).
6662
+ See [Validation boundaries in the README](../README.md#validation-boundaries) and [Architecture](https://github.com/Photivra/photivra/blob/main/docs/ARCHITECTURE.md).
6357
6663
 
6358
6664
  ## Provenance, uncertainty, and quality metadata
6359
6665
 
@@ -6373,7 +6679,7 @@ console.log(result.provenance.modelVersion);
6373
6679
  console.log(result.provenance.assumptions);
6374
6680
  ```
6375
6681
 
6376
- For code that defines a new scientific result, the package exports helpers such as `calculatedResult()`, `approximationResult()`, `estimatedResult()`, and `calibratedResult()`.
6682
+ If your code defines a new scientific result, the package exports helpers such as `calculatedResult()`, `approximationResult()`, `estimatedResult()` and `calibratedResult()`.
6377
6683
 
6378
6684
  ```ts
6379
6685
  import { calculatedResult } from "@photivra/engine";
@@ -6397,25 +6703,36 @@ const result = calculatedResult(
6397
6703
  );
6398
6704
  ```
6399
6705
 
6400
- Do not invent uncertainty values. Quality metadata should only be supplied when the uncertainty, calibration, valid range, or quality note has a defensible basis.
6706
+ Do not invent uncertainty values. Supply quality metadata only when the uncertainty, calibration, valid range or quality note has a defensible basis.
6401
6707
 
6402
- See [Scientific and Source Provenance](PROVENANCE.md) and [Public API Style](API_STYLE.md#result-metadata).
6708
+ See [Scientific and Source Provenance](https://github.com/Photivra/photivra/blob/main/docs/PROVENANCE.md) and [Public API Style](https://github.com/Photivra/photivra/blob/main/docs/API_STYLE.md#result-metadata).
6403
6709
 
6404
6710
  ## Composed POC simulation
6405
6711
 
6406
- Use `simulatePocCamera()` when you want one composed response containing the current POC calculations.
6712
+ Use `simulatePocCamera()` when you want one combined response with the current POC (proof-of-concept) calculations.
6713
+
6714
+ The root engine and the composed POC are versioned independently. `ENGINE_API_VERSION` describes the root library; `POC_SIMULATION_API_VERSION` describes this composed request/response contract.
6715
+
6716
+ The composed POC reports X/Y geometric sample pitch, but internally it still uses one representative horizontal pitch for blur and sampling calculations, for backwards compatibility. It therefore rejects sensor geometry whose X and Y pitch differ by more than 1%. The lower-level geometry APIs handle each axis separately, for more general sensor layouts.
6407
6717
 
6408
- The root engine and composed POC are versioned independently. `ENGINE_API_VERSION` describes the root library surface; `POC_SIMULATION_API_VERSION` describes this composed request/response contract.
6718
+ POC API 0.20 composes the capture-geometry foundation all the way to final output and viewing. Existing requests remain valid. New callers may supply an optional `capture` object with the physical orientation, an optional native active-capture rectangle, an optional oriented output crop and an optional final output raster.
6409
6719
 
6410
- The current composed POC reports X/Y geometric sample pitch but still uses one backwards-compatible representative horizontal pitch internally for blur/sampling calculations. It therefore rejects sensor geometry whose X/Y pitch differs by more than 1%. Axis-aware lower-level geometry remains available for more general sensor layouts.
6720
+ - The staged `capture` request deliberately keeps the legacy `crop.factor` separate: it must stay `1` when `capture` is present.
6721
+ - This geometry request is separate from the sensor/capture-mode profile contract.
6722
+ - Staged capture can use the existing `subjectCrop` request, but the result appears under `capture.subjectFraming`, because it is a framing stage after output, not a legacy total-crop factor.
6411
6723
 
6412
- POC API 0.20 composes the capture-geometry foundation through final output/viewing semantics. Existing requests remain valid. New callers may supply an optional `capture` object with physical orientation, an optional native active-capture rectangle, an optional oriented output crop, and an optional final output raster.
6724
+ In capture mode, the equivalent-viewing CoC (circle of confusion) uses the final retained physical image region, including subject framing when present. Changing only the output pixel resolution does not change it. An explicit `circleOfConfusionMm` stays unchanged.
6413
6725
 
6414
- The POC staged `capture` geometry request deliberately keeps legacy `crop.factor` separate: it must remain `1` when `capture` is present. This geometry request is distinct from the sensor/capture-mode profile contract. Staged capture can use the existing `subjectCrop` request, but the result appears under `capture.subjectFraming` because it is a post-output framing stage rather than a legacy total-crop factor.
6726
+ The optional `capture` block in the response gives:
6415
6727
 
6416
- Equivalent-viewing CoC in capture mode uses the final retained physical image region, including subject framing when present; changing only output pixel resolution does not change the criterion. Explicit `circleOfConfusionMm` remains unchanged.
6728
+ - the resolved geometry;
6729
+ - active and output FOV;
6730
+ - the 35 mm-equivalent focal length of the active capture, based on the diagonal;
6731
+ - the output sampling scale;
6732
+ - optional subject framing;
6733
+ - explicit motion diagnostics in native-raster, oriented and output coordinates.
6417
6734
 
6418
- The optional response `capture` block exposes resolved geometry, active/output FOV, active-capture diagonal-based 35 mm-equivalent focal length, output sampling scale, optional subject framing, and explicit native-raster/oriented/output motion diagnostics. Legacy motion Y is image-plane +Y-up; capture raster Y is +Y-down, so the conversion is reported rather than hidden. Physical focal length remains authoritative. Sensor-architecture metadata and radiometry-readiness profiles are still standalone.
6735
+ Legacy motion Y uses the image plane, +Y up; capture raster Y is +Y down. The conversion is therefore reported, not hidden. The physical focal length stays authoritative. Sensor-architecture metadata and radiometry-readiness profiles are still standalone.
6419
6736
 
6420
6737
 
6421
6738
  ```ts
@@ -6537,16 +6854,704 @@ console.log(portraitCrop.capture?.focalLength);
6537
6854
  console.log(portraitCrop.capture?.motion.outputDeltaPixels);
6538
6855
  ```
6539
6856
 
6540
- The focus request must supply exactly one circle-of-confusion criterion: either `circleOfConfusionMm` or `equivalentViewingCircleOfConfusion`. In staged capture mode, the equivalent-viewing approximation uses the final retained physical image region; an explicit `circleOfConfusionMm` remains unchanged by crop/output geometry.
6857
+ The focus request must supply exactly one circle-of-confusion criterion: either `circleOfConfusionMm` or `equivalentViewingCircleOfConfusion`. In staged capture mode, the equivalent-viewing approximation uses the final retained physical image region. An explicit `circleOfConfusionMm` is not changed by crop or output geometry.
6541
6858
 
6542
- Additional named defocus, sampling, and motion samples can be supplied when a renderer or analysis client needs per-object outputs.
6859
+ You can supply extra named defocus, sampling and motion samples when a renderer or analysis tool needs results per object.
6543
6860
 
6544
- See [Local POC Simulation API](POC_API.md#simulate) for the complete composed request/response semantics. That HTTP transport is repository-only contributor tooling and is not shipped as a package subpath; `simulatePocCamera()` itself is part of the browser-safe root package.
6861
+ See [Local POC Simulation API](https://github.com/Photivra/photivra/blob/main/docs/POC_API.md#simulate) for the full request/response meaning. That HTTP transport is repository-only contributor tooling, and is not shipped as a package subpath. `simulatePocCamera()` itself is part of the browser-safe root package.
6862
+
6863
+ ## DNG import and PNG output
6864
+
6865
+ - `parseDngRawImage` reads a DNG or TIFF-EP CFA/LinearRaw file (uncompressed or lossless JPEG, 8–16 bits). It returns the stored raw codes plus the CFA, black/white levels, active area, default crop, colour matrices, as-shot neutral and orientation. Nothing is applied to the codes.
6866
+ - `serializePngImage` writes an 8- or 16-bit RGB PNG with an sRGB, ICC or cICP (Display P3, BT.2020; sRGB, BT.709 or linear transfer) colour description, and optional mDCV/cLLI display metadata. PQ and HLG are not part of 2.0 (see the patent review in `THIRD_PARTY.md`).
6867
+
6868
+ See [DNG import and PNG output](https://github.com/Photivra/photivra/blob/main/docs/IMPORT_AND_PNG.md) for supported subsets, limits and sources.
6869
+
6870
+ ```ts
6871
+ import { serializePngImage, parseDngRawImage } from '@photivra/engine';
6872
+
6873
+ // Encode a 1 x 1 Display P3 pixel (sRGB transfer), then show that a PNG is not a DNG.
6874
+ const png = serializePngImage({ width: 1, height: 1, bitDepth: 8, samples: Uint8Array.of(255, 128, 0),
6875
+ colourEncoding: { kind: 'cicp', primaries: 'display-p3', transfer: 'srgb', fullRange: true } });
6876
+ let rejected = false;
6877
+ try { parseDngRawImage({ bytes: png }); } catch { rejected = true; }
6878
+ if (!rejected) throw new Error('A PNG must not parse as a DNG');
6879
+ ```
6545
6880
 
6546
6881
  ## Native-only Print planning
6547
6882
 
6548
- Use [registered linear statistics](PRINT_REGION_STATISTICS.md) to distinguish same-realization processing changes from per-site repeat-capture variance. The [static neutral Gabor contrast reference](PRINT_CONTRAST_REFERENCE.md) requires explicit stimulus, luminance and observer conditions and does not accept an arbitrary photographic ROI as a qualified stimulus. See the [#195/#196 candidate evidence review](PRINT_195_196_REVIEW.md) before integrating either diagnostic.
6883
+ - [Registered linear statistics](https://github.com/Photivra/photivra/blob/main/docs/PRINT_REGION_STATISTICS.md) tell apart changes caused by processing the same capture from variation between repeated captures at each site.
6884
+ - The [static neutral Gabor contrast reference](https://github.com/Photivra/photivra/blob/main/docs/PRINT_CONTRAST_REFERENCE.md) needs explicit stimulus, luminance and observer conditions. It does not accept an arbitrary photographic ROI (region of interest) as a valid stimulus.
6885
+ - Read the [#195/#196 candidate evidence review](https://github.com/Photivra/photivra/blob/main/docs/PRINT_195_196_REVIEW.md) before integrating either diagnostic.
6886
+
6887
+ See [Print planning](https://github.com/Photivra/photivra/blob/main/docs/PRINT_PLANNING.md) for a complete, strict packed example and the exact-ratio integer policy.
6888
+
6889
+ - `calculatePrintPlan()` says whether the native sampling is enough, and gives a compatible minimum file raster.
6890
+ - `calculatePrintSizeLimit()` distinguishes finite, unbounded and unavailable sampling limits.
6891
+ - Print quality and delivery are not assessed.
6892
+
6893
+ The [regional Print detail diagnostic](https://github.com/Photivra/photivra/blob/main/docs/PRINT_DETAIL_ASSESSMENT.md) measures one declared, coherent sinusoidal fundamental in a selected linear-luminance ROI, and records the exact pipeline stage it measured. Its [validation protocol](https://github.com/Photivra/photivra/blob/main/docs/PRINT_DETAIL_VALIDATION.md) and [capability ledger](https://github.com/Photivra/photivra/blob/main/docs/validation/print-detail-capabilities.json) separate mathematical conformance from the still-blocked qualification of acquired images, renderers and perceived quality.
6894
+
6895
+ ## Rolling shutter, video, flash TTL/HSS and highlight-priority metering (2.0.0)
6896
+
6897
+ These additions are approximations. The equations, derivations and sources are in
6898
+ [Electronic rolling-shutter image formation](https://github.com/Photivra/photivra/blob/main/docs/MOTION_AND_SIGNAL.md#electronic-rolling-shutter-image-formation),
6899
+ [Video frame sequences](https://github.com/Photivra/photivra/blob/main/docs/MOTION_AND_SIGNAL.md#video-frame-sequences) and
6900
+ [Flash: guide number, TTL and high-speed sync](https://github.com/Photivra/photivra/blob/main/docs/SCENE_SENSOR_TEMPORAL_EXPOSURE.md#flash-guide-number-ttl-and-high-speed-sync).
6901
+
6902
+ With an electronic rolling shutter, each sensor line starts its exposure slightly later than the one before.
6903
+ The model turns the rolling readout declaration into an exposure schedule with one offset per line.
6904
+
6905
+ - Pass the schedule wherever an exposure window is accepted: native RAW and environment `exposureWindow`, and
6906
+ production `temporalCapture.exposureWindowInput` with `readoutExposureLinkage`.
6907
+ - Skew, wobble, partial exposure and flicker banding then appear naturally from sampling each site at its own time.
6908
+ - A video clip is a deterministic sequence of ordinary release frames.
6909
+
6910
+ ```ts
6911
+ import {
6912
+ assessShutterAngleRule,
6913
+ calculateCaptureExposureWindows,
6914
+ resolveElectronicRollingShutterExposureSchedule,
6915
+ resolveVideoFrameSequence,
6916
+ createRollingShutterCaptureExposureSchedule
6917
+ } from "@photivra/engine";
6918
+
6919
+ const evidence = [{ sourceOrigin: "photivra" as const, sourceReference: "example:rolling-video", reuseStatus: "photivra-owned" as const }];
6920
+ const readout = {
6921
+ readoutMode: "rolling" as const,
6922
+ captureReadoutDurationSeconds: { value: 0.016, unit: "s" as const, evidence },
6923
+ scanDirectionNative: { value: "top-to-bottom" as const, evidence },
6924
+ spatialSamplingSkewSeconds: { value: 0.016, unit: "s" as const, evidence }
6925
+ };
6926
+ const schedule = resolveElectronicRollingShutterExposureSchedule({
6927
+ readout,
6928
+ nominalExposureDurationSeconds: { value: 1 / 250, unit: "s", evidence },
6929
+ linkageEvidence: evidence
6930
+ });
6931
+ const windows = calculateCaptureExposureWindows({
6932
+ ...createRollingShutterCaptureExposureSchedule(schedule),
6933
+ nativeRaster: { pixelWidth: 6000, pixelHeight: 4000 },
6934
+ samplePointsNative: [{ x: 3000, y: 0.5 }, { x: 3000, y: 3999.5 }]
6935
+ }).value;
6936
+ // Top and bottom lines open about 16 ms apart: a subject moving 2000 px/s is sheared by ~32 px.
6937
+ console.log(windows.samples.map((window) => window.startOffsetSecondsFromOpeningReference));
6938
+
6939
+ const clip = resolveVideoFrameSequence({
6940
+ sequenceId: "clip-1",
6941
+ opticalState: { bodyId: "generic-body", bodyVersion: "1", lensId: "generic-50", lensVersion: "1", focalLengthMm: 50,
6942
+ aperture: 4, focusDistanceM: 5, captureMode: "video", outputWidth: 1920, outputHeight: 1080, frameRateHz: 24,
6943
+ stabilizationMode: "off" },
6944
+ shutterAngleDegrees: 180,
6945
+ frameCount: 3,
6946
+ sequenceSeedUint32: 7,
6947
+ iso: 400,
6948
+ automation: { ae: "locked", af: "manual", awb: "locked" },
6949
+ timingEvidence: evidence,
6950
+ rollingShutter: { readout, linkageEvidence: evidence }
6951
+ });
6952
+ console.log(clip.exposureSeconds, clip.frames.map((frame) => [frame.exposureStartTimeSeconds, frame.stochasticSeedUint32]));
6953
+ console.log(assessShutterAngleRule(24, 172.8).classification);
6954
+ ```
6955
+
6956
+ Highlight-priority metering places a highlight percentile below saturation. TTL (through-the-lens) flash metering
6957
+ works out the main-flash power from a pre-flash, measured with the same linear meter. The result feeds the ordinary
6958
+ front/rear-curtain sync:
6959
+
6960
+ ```ts
6961
+ import {
6962
+ calculateFlashPowerForDistance,
6963
+ calculateHighlightPriorityPlacementSignal,
6964
+ meterRelativeExposure,
6965
+ parseExposureMeteringProfile,
6966
+ resolveCaptureGeometry,
6967
+ resolveTtlFlashExposure,
6968
+ type ExposureMeteringSampleSet
6969
+ } from "@photivra/engine";
6970
+
6971
+ const evidence = [{ sourceOrigin: "photivra" as const, sourceReference: "example:flash-meter", reuseStatus: "photivra-owned" as const }];
6972
+ const zones = (signals: readonly number[]): ExposureMeteringSampleSet => ({
6973
+ measurementId: "meter", sceneStateId: "scene", inputDomain: "relative-pre-exposure-linear-signal",
6974
+ captureRegion: "oriented-active-capture",
6975
+ captureGeometry: resolveCaptureGeometry({ imagingArea: { widthMm: 36, heightMm: 24 },
6976
+ nativeRaster: { pixelWidth: 6000, pixelHeight: 4000 }, orientation: "landscape" }).value,
6977
+ processingState: { exposureSettingsApplied: false, whiteBalanceApplied: false, toneMappingApplied: false,
6978
+ displayGammaApplied: false, sharpeningApplied: false },
6979
+ samples: signals.map((relativeLinearSignal, index) => ({ sampleId: "zone-" + index,
6980
+ positionOrientedCaptureUv: { u: (index % 5 + 0.5) / 5, v: (Math.floor(index / 5) + 0.5) / 4 },
6981
+ relativeLinearSignal, areaWeight: 1 }))
6982
+ });
6983
+ const meter = (policy: Parameters<typeof parseExposureMeteringProfile>[0], target: number) => parseExposureMeteringProfile({
6984
+ schemaVersion: "0.1.0", profileId: "meter", scientificStatus: "approximation",
6985
+ inputDomain: "relative-pre-exposure-linear-signal", captureRegion: "oriented-active-capture", policy,
6986
+ target: { kind: "relative-signal-reference", targetRelativeSignal: target, evidence }, evidence,
6987
+ limitations: ["Generic relative meter."]
6988
+ });
6989
+
6990
+ // 95 % mid-grey, 5 % bright: place the 99th percentile half a stop below saturation.
6991
+ const scene = zones([...Array.from({ length: 19 }, () => 0.18), 0.9]);
6992
+ const placement = calculateHighlightPriorityPlacementSignal(1, 0.5);
6993
+ const highlight = meterRelativeExposure({
6994
+ profile: meter({ kind: "highlight-priority", highlightPercentile: 0.99, saturationRelativeSignal: 1, headroomStops: 0.5 }, placement),
6995
+ sampleSet: scene
6996
+ }).value;
6997
+ console.log(highlight.highlightPriority?.percentileRelativeSignal, highlight.status === "resolved" && highlight.requiredExposureScaleToTarget);
6998
+
6999
+ // Manual guide-number exposure: GN 40 m at ISO 100, subject 5 m at f/4 → quarter power.
7000
+ console.log(calculateFlashPowerForDistance({ fullPowerGuideNumberMetersIso100: 40, subjectDistanceM: 5,
7001
+ apertureFNumber: 4, iso: 100, minimumPowerFraction: 1 / 128 }).powerFraction);
7002
+
7003
+ // TTL: ambient and 1/32-power pre-flash readings of the same zones.
7004
+ const ttl = resolveTtlFlashExposure({
7005
+ meteringProfile: meter({ kind: "multi-zone-uniform" }, 0.18),
7006
+ ambientSampleSet: zones([0.02, 0.02, 0.04, 0.04]),
7007
+ preFlashSampleSet: zones([0.12, 0.12, 0.06, 0.06]),
7008
+ preFlashPowerFraction: 1 / 32,
7009
+ ambientScaleToMainExposure: 0.5,
7010
+ flashExposureCompensationStops: 0,
7011
+ minimumPowerFraction: 1 / 128,
7012
+ guideNumber: { fullPowerGuideNumberMetersIso100: 40, iso: 100, apertureFNumber: 4 }
7013
+ }).value;
7014
+ console.log(ttl.status, ttl.powerFraction, ttl.flashRangeAtApertureM);
7015
+ // Apply with createTtlScaledFlashProfile(flashProfile, ttl) and place it with resolveManualFlashSync() as before.
7016
+ ```
7017
+
7018
+ High-speed sync (HSS) needs a schema 0.2.0 sync-capability profile that declares HSS and its burst energy fraction.
7019
+
7020
+ - `resolveHighSpeedFlashSync()` returns the burst support, the energy fraction per line `η·t_e/D`, and the
7021
+ guide-number factor `√(η·t_e/D)`.
7022
+ - `createHighSpeedSyncIlluminationOverlay()` registers the burst as a temporal illumination source. Every line
7023
+ integrates it over its own window.
7024
+ - To solve TTL power for HSS, pass `syncEnergyFactor = energyFractionOfSinglePulseRange.minimum` to
7025
+ `resolveTtlFlashExposure()`.
7026
+
7027
+ ## Lens projections, aberrations, polarizer and haze (2.0)
7028
+
7029
+ The models below are standalone primitives. See [Physics foundation](https://github.com/Photivra/photivra/blob/main/docs/PHYSICS_FOUNDATION.md#non-rectilinear-lens-projections-20) and [PSF foundation](https://github.com/Photivra/photivra/blob/main/docs/PSF_FOUNDATION.md#zernike-and-seidel-wavefronts-20) for equations, sources and limits.
7030
+
7031
+ ### Fisheye and equirectangular projections
7032
+
7033
+ ```ts
7034
+ import {
7035
+ calculateFieldOfView,
7036
+ calculateLensProjectionDirection,
7037
+ calculateLensProjectionImagePoint
7038
+ } from "@photivra/engine";
7039
+
7040
+ // 8 mm equidistant fisheye across a 24 mm sensor: 2 * (12 / 8) rad = 171.9 degrees.
7041
+ const fisheye = calculateFieldOfView({
7042
+ focalLengthMm: 8,
7043
+ sensorDimensionMm: 24,
7044
+ projectionModel: "fisheye-equidistant"
7045
+ });
7046
+ console.log(fisheye.value.degrees, fisheye.provenance.model);
7047
+
7048
+ // Omitting projectionModel keeps the rectilinear result unchanged.
7049
+ console.log(calculateFieldOfView({ focalLengthMm: 50, sensorDimensionMm: 36 }).value.degrees);
6549
7050
 
6550
- See [Print planning](PRINT_PLANNING.md) for a complete strict packed example and the exact-ratio integer policy. `calculatePrintPlan()` answers native sampling sufficiency and a compatible minimum file raster; `calculatePrintSizeLimit()` distinguishes finite, unbounded and unavailable sampling bounds. Quality and delivery are separately unassessed.
7051
+ const point = calculateLensProjectionImagePoint({
7052
+ projectionModel: "fisheye-stereographic",
7053
+ focalLengthMm: 8,
7054
+ direction: { x: 1, y: 0.2, z: -0.1 } // slightly behind the lens: still mapped
7055
+ });
7056
+ const back = calculateLensProjectionDirection({
7057
+ projectionModel: "fisheye-stereographic",
7058
+ focalLengthMm: 8,
7059
+ imagePointMm: point.value.imagePointMm
7060
+ });
7061
+ console.log(point.value.imagePointMm, back.value.direction);
7062
+ ```
7063
+
7064
+ ### Brown–Conrady distortion
7065
+
7066
+ ```ts
7067
+ import {
7068
+ calculateBrownConradyDistortionMapping,
7069
+ calculateInverseBrownConradyDistortionMapping,
7070
+ type BrownConradyDistortionProfile
7071
+ } from "@photivra/engine";
7072
+
7073
+ const profile: BrownConradyDistortionProfile = {
7074
+ normalizationRadiusMm: 21.6,
7075
+ maximumNormalizedRadius: 1,
7076
+ coefficients: { k1: -0.08, k2: 0.01, k3: 0, p1: 0.002, p2: -0.0015 }
7077
+ };
7078
+ const distorted = calculateBrownConradyDistortionMapping({ imagePointMm: { x: 10, y: -6 }, profile });
7079
+ const ideal = calculateInverseBrownConradyDistortionMapping({
7080
+ distortedImagePointMm: distorted.value.mappedImagePointMm,
7081
+ profile
7082
+ });
7083
+ console.log(distorted.value.mappedImagePointMm, ideal.value.sourceImagePointMm);
7084
+ ```
7085
+
7086
+ ### Zernike wavefront PSF, longitudinal CA and cat's-eye vignetting
7087
+
7088
+ ```ts
7089
+ import {
7090
+ calculateLensComplexPupilPsf,
7091
+ calculateLongitudinalChromaticFocus,
7092
+ calculateMechanicalVignetting,
7093
+ calculateNaturalVignetting,
7094
+ calculatePolygonPupilFraunhoferPsf,
7095
+ createWavefrontPupilGrid,
7096
+ estimateMarechalStrehlRatio,
7097
+ parseLensComplexPupilProfile
7098
+ } from "@photivra/engine";
7099
+
7100
+ // Per-wavelength defocus of an achromat-like secondary spectrum at f/4.
7101
+ const blue = calculateLongitudinalChromaticFocus({
7102
+ focalLengthMm: 50,
7103
+ referenceWavelengthNm: 550,
7104
+ wavelengthNm: 460,
7105
+ apertureFNumber: 4,
7106
+ focusDistanceM: 3,
7107
+ dispersion: {
7108
+ kind: "declared-focal-shift",
7109
+ samples: [
7110
+ { wavelengthNm: 450, focalLengthShiftMicrometers: 20 },
7111
+ { wavelengthNm: 550, focalLengthShiftMicrometers: 0 },
7112
+ { wavelengthNm: 650, focalLengthShiftMicrometers: 15 }
7113
+ ]
7114
+ }
7115
+ }).value;
7116
+
7117
+ // Defocus (Z4) plus a little spherical aberration (Z11) on a 12.5 mm pupil.
7118
+ const wavefront = createWavefrontPupilGrid({
7119
+ pupilDiameterMm: 12.5,
7120
+ samplesAcross: 31,
7121
+ zernikeCoefficients: [
7122
+ { nollIndex: 4, coefficientMicrometers: blue.zernikeZ4Micrometers },
7123
+ { nollIndex: 11, coefficientMicrometers: 0.02 }
7124
+ ]
7125
+ }).value;
7126
+ console.log(estimateMarechalStrehlRatio({
7127
+ rmsWavefrontErrorMicrometers: wavefront.sampledRmsMicrometers,
7128
+ wavelengthNm: 460
7129
+ }).value.strehlRatio);
7130
+
7131
+ const psf = calculateLensComplexPupilPsf({
7132
+ profile: parseLensComplexPupilProfile({
7133
+ schemaVersion: "0.1.0",
7134
+ profileId: "example-wavefront",
7135
+ profileVersion: "1.0.0",
7136
+ scientificStatus: "approximation",
7137
+ opticalDomain: "lens-primary-optical-path-only",
7138
+ representation: "complex-pupil-amplitude-plus-opd",
7139
+ pupilCoordinateSystem: "pupil-plane-metric-aligned-to-image-plane",
7140
+ imageFieldAxes: "+X right, +Y up",
7141
+ amplitudeMeaning: "relative-complex-pupil-amplitude-shape",
7142
+ wavefrontMeaning: "optical-path-difference-micrometers",
7143
+ throughputOwnership: "separate-relative-pupil-throughput-factor",
7144
+ kernelEnergyNormalization: "unit-energy-shape",
7145
+ propagationModel: "scalar-fraunhofer-discrete-reference",
7146
+ context: {
7147
+ focalLengthMm: 50,
7148
+ focus: { kind: "finite", distanceM: 3 },
7149
+ apertureFNumber: 4,
7150
+ fieldPointMm: { x: 0, y: 0 },
7151
+ wavelengthNm: 460,
7152
+ signedDefocusImagePlaneMicrometers: blue.signedDefocusImagePlaneMicrometers
7153
+ },
7154
+ responseIncludes: { diffraction: true, aberration: true, defocus: true, pupilClippingShape: false },
7155
+ relativePupilThroughputFactor: 1,
7156
+ grid: wavefront.grid,
7157
+ sensorOpticalStackIncluded: false,
7158
+ sensorSamplingIncluded: false,
7159
+ reconstructionIncluded: false,
7160
+ strayLightIncluded: false,
7161
+ evidence: [{ sourceOrigin: "photivra", sourceReference: "usage-example", reuseStatus: "photivra-owned" }],
7162
+ uncertainty: { kind: "not-quantified", limitation: "Synthetic example wavefront." },
7163
+ limitations: ["Synthetic example."]
7164
+ })
7165
+ });
7166
+ console.log(psf.value.kernel.widthSamples);
7167
+
7168
+ // Natural cos^4 falloff and a cat's-eye pupil 20 degrees off axis.
7169
+ console.log(calculateNaturalVignetting({
7170
+ model: "cosine-fourth",
7171
+ imagePointMm: { x: 15, y: 10 },
7172
+ exitPupilDistanceMm: 60
7173
+ }).value.relativeIllumination);
7174
+ const catsEye = calculateMechanicalVignetting({
7175
+ entrancePupilDiameterMm: 12.5,
7176
+ fieldAngleDegrees: 20,
7177
+ fieldAzimuthDegrees: 30,
7178
+ apertures: [{ radiusMm: 7, shiftPerTanFieldMm: 30 }]
7179
+ }).value;
7180
+ const bokehPsf = calculatePolygonPupilFraunhoferPsf({
7181
+ pupilVerticesMm: catsEye.openPupilPolygonMm,
7182
+ pupilToImageDistanceMm: 50,
7183
+ wavelengthNm: 550,
7184
+ wavelengthBasis: "air",
7185
+ imagePointsMicrometers: [{ x: 0, y: 0 }, { x: 2, y: 0 }]
7186
+ });
7187
+ console.log(catsEye.openAreaRatio, bokehPsf.value.samples[1]?.peakNormalizedIntensity);
7188
+ ```
7189
+
7190
+ ### Polarizer and haze
7191
+
7192
+ ```ts
7193
+ import {
7194
+ calculateAtmosphericHaze,
7195
+ calculateExtinctionFromVisibility,
7196
+ calculateGeometricFocusBreathing,
7197
+ composeFrontOfLensFilterTransmission,
7198
+ parseFrontOfLensFilterProfile
7199
+ } from "@photivra/engine";
7200
+
7201
+ const polarizer = parseFrontOfLensFilterProfile({
7202
+ schemaVersion: "0.2.0",
7203
+ filterId: "generic-linear-polarizer",
7204
+ profileVersion: "1.0.0",
7205
+ identityScope: "photivra-generic-unbranded",
7206
+ position: "front-of-lens",
7207
+ scientificStatus: "approximation",
7208
+ wavelengthBasis: "air",
7209
+ wavelengthRangeNanometers: { minimum: 400, maximum: 700 },
7210
+ transmission: {
7211
+ kind: "linear-polarizer",
7212
+ principalTransmittance: { value: 0.8, evidence: [{ sourceOrigin: "photivra", sourceReference: "usage-polarizer", reuseStatus: "photivra-owned" }] },
7213
+ extinctionRatio: { value: 1000, evidence: [{ sourceOrigin: "photivra", sourceReference: "usage-polarizer", reuseStatus: "photivra-owned" }] },
7214
+ transmissionAxisDegrees: 90
7215
+ },
7216
+ uncertainty: { kind: "not-quantified", limitation: "Generic educational polarizer." },
7217
+ polarizationModeled: true,
7218
+ wavelengthChangingBehaviorModeled: false,
7219
+ evidence: [{ sourceOrigin: "photivra", sourceReference: "usage-polarizer", reuseStatus: "photivra-owned" }],
7220
+ limitations: ["Generic example."]
7221
+ });
7222
+ // Glare partially polarized horizontally (P = 0.6) is suppressed by a vertical axis.
7223
+ const filtered = composeFrontOfLensFilterTransmission({
7224
+ filters: [polarizer],
7225
+ wavelengthNanometers: 550,
7226
+ wavelengthBasis: "air",
7227
+ incidentPolarization: { degreeOfLinearPolarization: 0.6, angleDegrees: 0 }
7228
+ });
7229
+ console.log(filtered.combinedLinearTransmissionFactor, filtered.transmittedPolarization);
7230
+
7231
+ const beta = calculateExtinctionFromVisibility({ visibilityM: 15_000 }).value.extinctionPerMeter;
7232
+ const hazy = calculateAtmosphericHaze({
7233
+ pathLengthM: 4000,
7234
+ wavelengthNm: 450,
7235
+ sceneRadiance: 0.02,
7236
+ airlightRadiance: 0.08,
7237
+ extinction: {
7238
+ referenceWavelengthNm: 550,
7239
+ rayleighExtinctionPerMeter: 0,
7240
+ aerosolExtinctionPerMeter: beta,
7241
+ angstromExponent: 1.3
7242
+ }
7243
+ });
7244
+ console.log(hazy.value.hazedRadiance, hazy.value.transmittance);
7245
+
7246
+ // Unit-focus breathing of a 100 mm lens focused at 0.6 m (subject to sensor).
7247
+ console.log(calculateGeometricFocusBreathing({
7248
+ focalLengthMm: 100,
7249
+ subjectToSensorDistanceM: 0.6,
7250
+ sensorDimensionMm: 36,
7251
+ focusingMethod: "unit-focus"
7252
+ }).value.deltaDegrees);
7253
+ ```
7254
+
7255
+ ## Parallel native environment RAW (row-band partitions, 2.0)
7256
+
7257
+ `createNativeEnvironmentRawTask` runs on one thread. For several cores, split the capture into row bands with
7258
+ `calculateNativeRawRowBands`, run each band with `createNativeEnvironmentRawPartitionTask` (one per Web Worker or Node
7259
+ worker thread, each with its own provider), post the partitions back, and join them with
7260
+ `composeNativeEnvironmentRawPartitions`. The merged output is byte-identical to the single task's for the same input
7261
+ and a deterministic provider. The package ships no worker code; the caller owns the workers. See
7262
+ [parallel execution](https://github.com/Photivra/photivra/blob/main/docs/NATIVE_RAW_AND_PRINT.md#opt-in-parallel-execution-row-band-partitions-20) for the checks and limits.
7263
+
7264
+ A provider can also add `evaluateRadianceBatch(queries, signal)`: one call per tile with every query in per-query
7265
+ order, returning the results in the same order (or a promise of them). The output is identical to per-query
7266
+ callbacks with the same results; see
7267
+ [batched light sampling](https://github.com/Photivra/photivra/blob/main/docs/NATIVE_RAW_AND_PRINT.md#batched-light-sampling-evaluateradiancebatch-20).
7268
+
7269
+ ```ts
7270
+ import { createNativeEnvironmentRawPartitionTask, composeNativeEnvironmentRawPartitions, calculateNativeRawRowBands, simulateCapture,
7271
+ type NativeEnvironmentRawProvider } from "@photivra/engine";
7272
+
7273
+ const capture = await simulateCapture({ scene: { preset: "uniform" }, output: { width: 8, height: 8 } });
7274
+ const input = capture.request.environmentRaw, width = capture.plan.exposure.geometry.nativeRaster.pixelWidth;
7275
+ // Each worker builds its own provider (functions cannot be posted). Here: a constant radiance.
7276
+ const provider: NativeEnvironmentRawProvider = {
7277
+ readTile: async (r) => ({ ...r, sites: Array.from({ length: r.width }, (_, j) => capture.request.siteInput(r.y * width + r.x + j)) }),
7278
+ evaluateRadiance: (q) => ({ schemaVersion: "0.1.0", sampleId: q.sampleId, sceneId: q.sceneId, providerProfileId: q.providerProfileId,
7279
+ wavelengthNanometers: q.wavelengthNanometers, wavelengthBasis: q.wavelengthBasis, quantity: "outgoing-spectral-radiance", unit: "W/m^2/sr/nm",
7280
+ spectralRadianceWattsPerSquareMeterSteradianNanometer: 0.01, scientificStatus: "approximation",
7281
+ uncertainty: { kind: "not-quantified", limitation: "Example field." },
7282
+ evidence: [{ sourceOrigin: "photivra", sourceReference: "photivra:usage-example", reuseStatus: "photivra-owned" }], limitations: ["Example field."] }),
7283
+ yieldControl: async () => {}
7284
+ };
7285
+ const partitions = await Promise.all(calculateNativeRawRowBands(capture.plan, 2).map(async (band) => {
7286
+ const task = createNativeEnvironmentRawPartitionTask(input, band, provider); // inside a worker
7287
+ await task.run();
7288
+ return task.takeOutput(); // postMessage(partition, [partition.codes.buffer, ...])
7289
+ }));
7290
+ const merged = composeNativeEnvironmentRawPartitions(input, partitions);
7291
+ console.log(merged.raw.codes.length, merged.providerEvaluationCount);
7292
+ ```
7293
+
7294
+ ## Optional WebGPU image kernels
7295
+
7296
+ `createImageKernelAccelerator()` runs six heavy image operations: isoplanatic PSF convolution (one PSF per image patch), the temporal exposure sum, the sensor stage, native capture SDR, packed RAW development, and chart radiance.
7297
+
7298
+ - By default they run on the float64 CPU reference.
7299
+ - If you opt in, they run on the `webgpu-f32` backend (WebGPU, 32-bit floats).
7300
+ - Every result's `execution` label names the backend that actually ran, any reason for falling back, and the error bound against the reference, stated in advance.
7301
+
7302
+ See [WebGPU backend](https://github.com/Photivra/photivra/blob/main/docs/WEBGPU.md) for the bounds, the supported domain and the browser test.
7303
+
7304
+ ```ts
7305
+ import { createImageKernelAccelerator, calculateIrradianceConvolution } from "@photivra/engine";
7306
+
7307
+ const width = 16, height = 8, kernelRadius = 1;
7308
+ const input = { schemaVersion: "0.1.0" as const, width, height, kernelRadius, patchWidth: width, patchHeight: height,
7309
+ boundary: "zero-outside-grid" as const, samples: new Float64Array(width * height).fill(2),
7310
+ kernels: Float64Array.from([0, .1, 0, .1, .6, .1, 0, .1, 0]) };
6551
7311
 
6552
- The [regional Print detail diagnostic](PRINT_DETAIL_ASSESSMENT.md) measures one declared coherent sinusoidal fundamental in a selected linear-luminance ROI and records its exact source stage. Its [validation protocol](PRINT_DETAIL_VALIDATION.md) and [capability ledger](validation/print-detail-capabilities.json) distinguish mathematical conformance from blocked acquired-image, renderer and perceived-quality qualification.
7312
+ const accelerator = await createImageKernelAccelerator({ backend: "webgpu-f32", fallback: "cpu-reference" });
7313
+ const result = await accelerator.convolveIrradiance(input);
7314
+ // Without WebGPU (e.g. Node) this ran the reference and says so.
7315
+ console.log(result.execution.backend, result.execution.fallbackReason, result.execution.bitIdenticalToReference);
7316
+ // Bit-exact identities and fixtures must come from the CPU reference.
7317
+ const reference = calculateIrradianceConvolution(input).values;
7318
+ console.log(result.execution.backend === "cpu-reference" ? result.values.every((v, i) => v === reference[i]) : result.execution.errorBound);
7319
+ accelerator.dispose();
7320
+ ```
7321
+
7322
+ Two image-formation stages use the same accelerator:
7323
+
7324
+ - `sumTemporalExposure()` is exposure and motion as a weighted sum of shifted frames, with per-row weights for rolling or global readout.
7325
+ - `simulateSensorRaw()` is the sensor stage: seeded Poisson charge, read noise and ADC to RAW codes. It uses the same noise model and per-site seeds as the per-site producer.
7326
+
7327
+ On `webgpu-f32`, every sensor site not listed in `flaggedSiteIndices` equals the reference exactly. Recompute the listed sites with the reference if you need bit identity.
7328
+
7329
+ ```ts
7330
+ import { createImageKernelAccelerator, simulatePackedSensorRaw, type PackedSensorRawInput, type TemporalExposureSumInput } from "@photivra/engine";
7331
+
7332
+ const evidence = [{ sourceOrigin: "photivra" as const, sourceReference: "example", reuseStatus: "photivra-owned" as const }];
7333
+ const width = 8, height = 4;
7334
+ // Static scene, global shutter: three temporal nodes of 1/3000 s each, the image moving 0.5 site to the right per node.
7335
+ const motion: TemporalExposureSumInput = { schemaVersion: "0.1.0", width, height, boundary: "zero-outside-grid",
7336
+ samples: new Float64Array(width * height).fill(9e6), // electrons per second
7337
+ displacements: Float64Array.from([0, 0, 0.5, 0, 1, 0]), rowWeights: new Float64Array(3 * height).fill(1 / 3000) };
7338
+ const sensor = (photoElectrons: Float64Array): PackedSensorRawInput => ({ schemaVersion: "0.1.0", nativeWidth: width, nativeHeight: height,
7339
+ region: { x: 0, y: 0, width, height }, captureSeedUint32: 42, photoElectrons, darkElectrons: new Float64Array(width * height),
7340
+ physicalChargeCapacityElectrons: 30_000, regimeId: "base", readoutProfile: { schemaVersion: "0.1.0", profileId: "readout", colorSamplingProfileId: "cfa",
7341
+ channelId: "green", regimeSelectionOwnedBy: "explicit-upstream-camera-state-not-inferred-from-iso", evidence, regimes: [{ regimeId: "base",
7342
+ scientificStatus: "approximation", systemConversionGainElectronsPerCode: { value: 2, evidence },
7343
+ preAdcSaturationElectronEquivalent: { value: 32_000, evidence }, readNoiseComponents: [{ componentId: "read", rmsElectrons: { value: 2.5, evidence }, evidence }],
7344
+ adc: { bitDepth: 14, blackLevelCode: 512, digitalSaturationCode: 16_383, transfer: "uniform-round-half-up" }, evidence, limitations: ["Example regime."] }] } });
7345
+
7346
+ const accelerator = await createImageKernelAccelerator({ backend: "webgpu-f32", fallback: "cpu-reference" });
7347
+ const exposure = await accelerator.sumTemporalExposure(motion);
7348
+ const raw = await accelerator.simulateSensorRaw(sensor(Float64Array.from(exposure.values)));
7349
+ console.log(exposure.execution.errorBound, raw.execution.errorBound, raw.flaggedSiteIndices.length);
7350
+ // The CPU reference gives the same codes at every unflagged site.
7351
+ const reference = simulatePackedSensorRaw(sensor(Float64Array.from(exposure.values))).codes;
7352
+ console.log(raw.execution.backend === "cpu-reference" ? raw.codes.every((c, i) => c === reference[i]) : raw.diagnostics);
7353
+ accelerator.dispose();
7354
+ ```
7355
+ ## Image processing operators
7356
+
7357
+ These are standalone operators that can be cancelled: demosaicing, HDR merge and encoding, tone mapping, exposure fusion, focus stacking, panorama stitching from known rotations, denoising, sharpening and synthetic grain.
7358
+
7359
+ - All results are labelled approximations. See [Image processing operators](https://github.com/Photivra/photivra/blob/main/docs/PROCESSING.md) for equations, sources, measured PSNR and limits.
7360
+ - Every factory takes `(input, { yieldControl, onProgress? })` and returns a task that can run once.
7361
+
7362
+ ```ts
7363
+ import {
7364
+ createBuiltInBayerDemosaicProfiles,
7365
+ createCfaPlaneReconstructionTask,
7366
+ createAhdDemosaicTask,
7367
+ type SensorColorSamplingProfile
7368
+ } from "@photivra/engine";
7369
+
7370
+ const provider = { yieldControl: async () => {} };
7371
+ const cfa: SensorColorSamplingProfile = {
7372
+ schemaVersion: "0.1.0", profileId: "rggb", coordinateSystem: "native-sensor-color-sampling-site-index",
7373
+ evidence: [{ sourceOrigin: "photivra", sourceReference: "example", reuseStatus: "photivra-owned" }],
7374
+ layout: { kind: "periodic-mosaic", repeatWidthSites: 2, repeatHeightSites: 2,
7375
+ siteChannelIds: ["red", "green", "green", "blue"], anchor: "native-sensor-top-left-site" }
7376
+ };
7377
+ const plane = { width: 64, height: 48, captureModeId: "native", colorSamplingProfile: cfa,
7378
+ samples: new Float32Array(64 * 48).fill(0.25) };
7379
+ const channelRoles = { red: "red", green: "green", blue: "blue" };
7380
+
7381
+ // Bilinear linear kernels with mirror borders (also accepted by createNativeRawDevelopmentTask).
7382
+ const bilinear = createBuiltInBayerDemosaicProfiles({ kernel: "bilinear", captureModeId: "native",
7383
+ colorSamplingProfile: cfa, channelRoles }).value;
7384
+ const linearTask = createCfaPlaneReconstructionTask({ plane, phaseProfiles: bilinear.phaseProfiles, borderHandling: "mirror" }, provider);
7385
+ await linearTask.run();
7386
+ console.log(linearTask.takeOutput().channelIds);
7387
+
7388
+ // Adaptive homogeneity-directed (non-linear) demosaic.
7389
+ const ahd = createAhdDemosaicTask({ plane, channelRoles }, provider);
7390
+ await ahd.run();
7391
+ console.log(ahd.takeOutput().direction.length);
7392
+ ```
7393
+
7394
+ Merge linear RAW brackets with noise-optimal weights, convert to linear BT.2020 and tone map:
7395
+
7396
+ ```ts
7397
+ import {
7398
+ createHdrBracketMergeTask,
7399
+ createHdrImageEncodingTask,
7400
+ createReinhardGlobalToneMappingTask
7401
+ } from "@photivra/engine";
7402
+
7403
+ const provider = { yieldControl: async () => {} };
7404
+ const n = 32 * 32 * 3;
7405
+ const frame = (seconds: number) => ({ exposureTimeSeconds: seconds, codes: new Uint16Array(n).fill(64 + Math.round(2000 * seconds)),
7406
+ blackLevelCode: 64, whiteLevelCode: 4000, conversionGainElectronsPerCode: 1.5, readNoiseElectrons: 3 });
7407
+ const merge = createHdrBracketMergeTask({ sampleCount: n, frames: [frame(1), frame(1 / 8), frame(1 / 64)] }, provider);
7408
+ await merge.run();
7409
+ const merged = merge.takeOutput();
7410
+ const radiance = { width: 32, height: 32, data: merged.radianceElectronsPerSecond.map((v) => v / 3000) };
7411
+
7412
+ const rec2020 = createHdrImageEncodingTask({ image: radiance, inputColorSpace: "linear-srgb-d65", transfer: "linear-float32" }, provider);
7413
+ await rec2020.run();
7414
+ console.log(rec2020.takeOutput().linear.slice(0, 3));
7415
+
7416
+ const sdr = createReinhardGlobalToneMappingTask({ image: radiance, inputColorSpace: "linear-srgb-d65", key: 0.18 }, provider);
7417
+ await sdr.run();
7418
+ console.log(sdr.takeOutput().logAverageLuminance);
7419
+ ```
7420
+
7421
+ `createDurandDorseyToneMappingTask`, `createExposureFusionTask`, `createFocusStackTask`, `createBilateralFilterTask`, `createNonLocalMeansTask`, `createUnsharpMaskTask` and `createSyntheticFilmGrainTask` work the same way. Panorama stitching uses each frame's known rotation, in the engine's pitch/yaw/roll convention:
7422
+
7423
+ ```ts
7424
+ import { createPanoramaTask } from "@photivra/engine";
7425
+
7426
+ const image = { width: 48, height: 36, data: new Float32Array(48 * 36 * 3).fill(0.5) };
7427
+ const panorama = createPanoramaTask({
7428
+ frames: [-0.5, 0, 0.5].map((yaw) => ({ image, focalLengthPixels: 40, rotationRad: { pitch: 0, yaw, roll: 0 } })),
7429
+ canvas: { projection: "equirectangular", width: 160, height: 48, longitudeRangeRad: [-1.1, 1.1], latitudeRangeRad: [-0.4, 0.4] },
7430
+ blending: "multi-band", exposureCompensation: "gain"
7431
+ }, { yieldControl: async () => {} });
7432
+ await panorama.run();
7433
+ console.log(panorama.takeOutput().gains);
7434
+ ```
7435
+ ### Fisheye-to-rectilinear and Brown–Conrady geometric mappings
7436
+
7437
+ ```ts
7438
+ import {
7439
+ calculateComposedGeometricMapping,
7440
+ calculateForwardGeometricMapping,
7441
+ prepareGeometricMapping
7442
+ } from "@photivra/engine";
7443
+
7444
+ // Destination-to-source: a rectilinear 10 mm view sampled from an 8 mm equidistant fisheye capture,
7445
+ // after a Brown–Conrady correction of the fisheye's residual distortion.
7446
+ const mapping = prepareGeometricMapping({
7447
+ frameTimeSeconds: 0,
7448
+ transforms: [
7449
+ {
7450
+ id: "fisheye-to-rectilinear", version: "1", domain: "reconstructed-linear", purpose: "projection",
7451
+ kind: "projection-remap",
7452
+ destinationProjection: { projectionModel: "rectilinear", focalLengthMm: 10 },
7453
+ sourceProjection: { projectionModel: "fisheye-equidistant", focalLengthMm: 8 }
7454
+ },
7455
+ {
7456
+ id: "residual-distortion", version: "1", domain: "reconstructed-linear", purpose: "distortion",
7457
+ kind: "brown-conrady",
7458
+ profile: { normalizationRadiusMm: 12, maximumNormalizedRadius: 1,
7459
+ coefficients: { k1: -0.02, k2: 0, k3: 0, p1: 0.001, p2: 0 } }
7460
+ }
7461
+ ]
7462
+ }).value;
7463
+ const point = calculateComposedGeometricMapping({ mapping, destinationPointMm: { x: 4, y: -2 } }).value;
7464
+ console.log(mapping.schemaVersion, point.sourcePointMm, point.determinant);
7465
+ console.log(calculateForwardGeometricMapping({ mapping, sourcePointMm: point.sourcePointMm }).value);
7466
+ ```
7467
+ ## Simulate a capture with the facade
7468
+
7469
+ `simulateCapture()` is a facade: one simple call that combines the generic tier presets, scene metering,
7470
+ the exposure-mode resolver, the native environment RAW task, native RAW development and, optionally, the
7471
+ print JPEG encoder. It returns the image, the packed RAW, the complete request and plan it ran, provenance,
7472
+ and an `assumptions` list naming every default and modelling choice. See the [Quick Start](QUICK_START.md)
7473
+ for every option, the chart catalogue and the limits.
7474
+
7475
+ ```ts
7476
+ import { simulateCapture } from "@photivra/engine";
7477
+
7478
+ const star = await simulateCapture({
7479
+ camera: "prosumer",
7480
+ exposure: { mode: "aperture-priority", iso: 200 },
7481
+ lens: { fNumber: 5.6 },
7482
+ scene: { preset: "siemens-star", cycles: 24 },
7483
+ illuminant: { kind: "planckian", temperatureKelvin: 5000 },
7484
+ illuminanceLux: 8000,
7485
+ output: { width: 12, height: 8, toneCurve: "positive-reinhard-per-channel", whiteBalance: { kelvin: 5000 } }
7486
+ });
7487
+
7488
+ console.log(star.exposure.settings.shutterSeconds, star.exposure.ev100);
7489
+ console.log(star.assumptions.filter((a) => a.kind === "default").map((a) => a.id));
7490
+ console.log(star.request.siteInput(0).environment.optics.nominalFNumber);
7491
+ ```
7492
+
7493
+ In `aperture-priority` mode, the shutter comes from the meter (CIPA DC-004 relations S = 10/H and H = 0.65·L·t/N²) through
7494
+ `resolveAperturePriorityExposureMode()`.
7495
+
7496
+ The chart presets are also available on their own:
7497
+
7498
+ - `resolveChartScene()` gives the analytic reflectance at any point in the frame.
7499
+ - `createPlanarSceneRadianceProvider()` turns a chart into an `evaluateRadiance` callback for the native
7500
+ environment RAW contract.
7501
+
7502
+ ### Facade 2.0 options: output, demosaic, optics and motion
7503
+
7504
+ Each 2.0 option is opt-in and uses an existing public contract. Without them, the facade's output is
7505
+ byte-identical to 1.0.
7506
+
7507
+ - `output.format: "png"` writes `serializePngImage` output. That is either the developed sRGB samples, or,
7508
+ with `colorEncoding`, the unclipped linear development converted through CIE XYZ to Display P3 or Rec. 2020,
7509
+ signalled with cICP.
7510
+ - `output.gamutMapping: "perceptual"` replaces the component clip with `prepareRgbGamutMapping`.
7511
+ - `format: "dng"` writes the packed native RAW with `serializeNativeRawDng`.
7512
+
7513
+ See [Output formats](QUICK_START.md#output-formats).
7514
+
7515
+ ```ts
7516
+ import { simulateCapture } from "@photivra/engine";
7517
+
7518
+ const p3 = await simulateCapture({ scene: { preset: "checkerboard" }, output: { width: 8, height: 8, format: "png", colorEncoding: "display-p3", bitDepth: 16 } });
7519
+ console.log(p3.image.png?.length, p3.image.colorEncoding, p3.provenance.composedContracts.includes("serializePngImage"));
7520
+ ```
7521
+
7522
+ `output.demosaic` selects kernels from `createBuiltInBayerDemosaicProfiles`. `output.borderHandling:
7523
+ "mirror"` renders no border sites; it reads reflect-101 mirror images at the frame edge instead.
7524
+
7525
+ ```ts
7526
+ import { simulateCapture } from "@photivra/engine";
7527
+
7528
+ const mirrored = await simulateCapture({ scene: { preset: "uniform" }, output: { width: 6, height: 4, demosaic: "bilinear", borderHandling: "mirror" } });
7529
+ console.log(mirrored.plan.exposure.geometry.nativeRaster, mirrored.assumptions.find((a) => a.id === "output.demosaic")?.value);
7530
+ ```
7531
+
7532
+ `lens.projection` and `lens.distortion` become the environment `lensFieldMapping`. `lens.vignetting`
7533
+ becomes radial field throughput, and `lens.psf` is a sampled PSF you supply. Haze and polarizers are not
7534
+ offered, because the scene-radiance provider contract declares neither volumetric transport nor
7535
+ polarization. See [Optics](QUICK_START.md#optics).
7536
+
7537
+ ```ts
7538
+ import { simulateCapture } from "@photivra/engine";
7539
+
7540
+ const barrel = await simulateCapture({
7541
+ scene: { preset: "uniform" },
7542
+ lens: { distortion: { kind: "brown-conrady", profile: { normalizationRadiusMm: 0.05, maximumNormalizedRadius: 1, coefficients: { k1: -0.2, k2: 0, k3: 0, p1: 0.01, p2: 0 } } } },
7543
+ output: { width: 8, height: 8 }
7544
+ });
7545
+ console.log(barrel.request.siteInput(0).environment.lensFieldMapping?.distortion.kind);
7546
+ ```
7547
+
7548
+ `motion.angularVelocityRadPerSec` rotates the camera during the exposure (8 time midpoints by
7549
+ default). `motion.shutter: "rolling"` derives each site's exposure window from the body's rolling
7550
+ readout, with `resolveElectronicRollingShutterExposureSchedule`. See [Motion](QUICK_START.md#motion).
7551
+
7552
+ ```ts
7553
+ import { simulateCapture } from "@photivra/engine";
7554
+
7555
+ const rolling = await simulateCapture({ scene: { preset: "checkerboard" }, motion: { angularVelocityRadPerSec: { pitch: 0.02 }, shutter: "rolling" }, output: { width: 6, height: 6 } });
7556
+ console.log(rolling.request.siteInput(7 * 8).charge.completenessProfile.startOffsetSecondsFromOpeningReference);
7557
+ ```