@photivra/engine 2.19.0 → 2.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/CHANGELOG.md +289 -1
  2. package/CITATION.cff +1 -1
  3. package/NODE_SUPPORT.md +7 -5
  4. package/README.md +7 -6
  5. package/SECURITY.md +1 -1
  6. package/THIRD_PARTY.md +21 -10
  7. package/dist/capture/dng-lens-correction-opcodes.d.ts +32 -0
  8. package/dist/capture/dng-lens-correction-opcodes.js +119 -0
  9. package/dist/capture/dng-lens-correction-opcodes.js.map +1 -0
  10. package/dist/capture/exposure-build-up-stage.d.ts +16 -0
  11. package/dist/capture/exposure-build-up-stage.js +4 -0
  12. package/dist/capture/exposure-build-up-stage.js.map +1 -0
  13. package/dist/capture/native-environment-lean.d.ts +2 -1
  14. package/dist/capture/native-environment-lean.js +42 -78
  15. package/dist/capture/native-environment-lean.js.map +1 -1
  16. package/dist/capture/native-environment-tabulated.js +4 -3
  17. package/dist/capture/native-environment-tabulated.js.map +1 -1
  18. package/dist/capture/native-raw-dng.d.ts +6 -0
  19. package/dist/capture/native-raw-dng.js +16 -2
  20. package/dist/capture/native-raw-dng.js.map +1 -1
  21. package/dist/capture/native-raw.d.ts +3 -2
  22. package/dist/capture/native-raw.js +5 -4
  23. package/dist/capture/native-raw.js.map +1 -1
  24. package/dist/capture/sensor-raw-producer.d.ts +4 -1
  25. package/dist/capture/sensor-raw-producer.js +15 -8
  26. package/dist/capture/sensor-raw-producer.js.map +1 -1
  27. package/dist/core/shared-helpers.d.ts +2 -0
  28. package/dist/core/shared-helpers.js +4 -0
  29. package/dist/core/shared-helpers.js.map +1 -1
  30. package/dist/core/version.d.ts +1 -1
  31. package/dist/core/version.js +1 -1
  32. package/dist/core/version.js.map +1 -1
  33. package/dist/equipment/generic-lens-catalogue.d.ts +11 -4
  34. package/dist/equipment/generic-lens-catalogue.js +49 -7
  35. package/dist/equipment/generic-lens-catalogue.js.map +1 -1
  36. package/dist/equipment/generic-tier-assets.d.ts +32 -0
  37. package/dist/equipment/generic-tier-assets.js +18 -14
  38. package/dist/equipment/generic-tier-assets.js.map +1 -1
  39. package/dist/equipment/generic-tier-presets.js +2 -3
  40. package/dist/equipment/generic-tier-presets.js.map +1 -1
  41. package/dist/equipment/sensor-profile-body.d.ts +78 -0
  42. package/dist/equipment/sensor-profile-body.js +77 -0
  43. package/dist/equipment/sensor-profile-body.js.map +1 -0
  44. package/dist/equipment/sensor-profiles.d.ts +8 -0
  45. package/dist/equipment/sensor-profiles.js +82 -0
  46. package/dist/equipment/sensor-profiles.js.map +1 -0
  47. package/dist/facade/catalogue-lens-signal.d.ts +2 -2
  48. package/dist/facade/catalogue-lens-signal.js +58 -71
  49. package/dist/facade/catalogue-lens-signal.js.map +1 -1
  50. package/dist/facade/chart-radiance-batch.js +2 -1
  51. package/dist/facade/chart-radiance-batch.js.map +1 -1
  52. package/dist/facade/defocus-image-space-provider.d.ts +7 -0
  53. package/dist/facade/defocus-image-space-provider.js +64 -76
  54. package/dist/facade/defocus-image-space-provider.js.map +1 -1
  55. package/dist/facade/display-view-gpu-session.js +2 -0
  56. package/dist/facade/display-view-gpu-session.js.map +1 -1
  57. package/dist/facade/display-view-gpu.d.ts +13 -5
  58. package/dist/facade/display-view-gpu.js +4 -4
  59. package/dist/facade/display-view-gpu.js.map +1 -1
  60. package/dist/facade/display-view-lens-signal.d.ts +1 -1
  61. package/dist/facade/display-view-lens-signal.js +30 -26
  62. package/dist/facade/display-view-lens-signal.js.map +1 -1
  63. package/dist/facade/display-view.d.ts +3 -1
  64. package/dist/facade/display-view.js +6 -24
  65. package/dist/facade/display-view.js.map +1 -1
  66. package/dist/facade/expected-signal-setup.d.ts +3 -1
  67. package/dist/facade/expected-signal-setup.js +1 -1
  68. package/dist/facade/expected-signal-setup.js.map +1 -1
  69. package/dist/facade/expected-signal.d.ts +47 -14
  70. package/dist/facade/expected-signal.js +117 -112
  71. package/dist/facade/expected-signal.js.map +1 -1
  72. package/dist/facade/exposure-build-up.d.ts +70 -0
  73. package/dist/facade/exposure-build-up.js +164 -0
  74. package/dist/facade/exposure-build-up.js.map +1 -0
  75. package/dist/facade/fast-methods.d.ts +50 -0
  76. package/dist/facade/fast-methods.js +58 -0
  77. package/dist/facade/fast-methods.js.map +1 -0
  78. package/dist/facade/fast-resolution.d.ts +12 -0
  79. package/dist/facade/fast-resolution.js +18 -0
  80. package/dist/facade/fast-resolution.js.map +1 -0
  81. package/dist/facade/generic-lens-capture.d.ts +5 -4
  82. package/dist/facade/generic-lens-capture.js +5 -4
  83. package/dist/facade/generic-lens-capture.js.map +1 -1
  84. package/dist/facade/image-plane-enable.d.ts +7 -0
  85. package/dist/facade/image-plane-enable.js +17 -0
  86. package/dist/facade/image-plane-enable.js.map +1 -0
  87. package/dist/facade/image-plane-lattice-data.d.ts +6 -0
  88. package/dist/facade/image-plane-lattice-data.js +10 -0
  89. package/dist/facade/image-plane-lattice-data.js.map +1 -0
  90. package/dist/facade/image-plane-support.d.ts +33 -0
  91. package/dist/facade/image-plane-support.js +11 -0
  92. package/dist/facade/image-plane-support.js.map +1 -0
  93. package/dist/facade/image-plane.d.ts +36 -0
  94. package/dist/facade/image-plane.js +353 -0
  95. package/dist/facade/image-plane.js.map +1 -0
  96. package/dist/facade/mixed-illumination.d.ts +108 -0
  97. package/dist/facade/mixed-illumination.js +174 -0
  98. package/dist/facade/mixed-illumination.js.map +1 -0
  99. package/dist/facade/representative-light-sources.d.ts +72 -0
  100. package/dist/facade/representative-light-sources.js +124 -0
  101. package/dist/facade/representative-light-sources.js.map +1 -0
  102. package/dist/facade/scene-radiance.d.ts +129 -4
  103. package/dist/facade/scene-radiance.js +48 -13
  104. package/dist/facade/scene-radiance.js.map +1 -1
  105. package/dist/facade/simulate-capture.d.ts +88 -19
  106. package/dist/facade/simulate-capture.js +374 -145
  107. package/dist/facade/simulate-capture.js.map +1 -1
  108. package/dist/facade/simulate-display-view.d.ts +3 -2
  109. package/dist/facade/simulate-display-view.js +36 -18
  110. package/dist/facade/simulate-display-view.js.map +1 -1
  111. package/dist/gpu/chart-composite-gpu.d.ts +13 -1
  112. package/dist/gpu/chart-composite-gpu.js +48 -9
  113. package/dist/gpu/chart-composite-gpu.js.map +1 -1
  114. package/dist/gpu/chart-radiance-gpu.js +4 -30
  115. package/dist/gpu/chart-radiance-gpu.js.map +1 -1
  116. package/dist/gpu/display-view-composite-material-module.d.ts +10 -4
  117. package/dist/gpu/display-view-composite-material-module.js +39 -13
  118. package/dist/gpu/display-view-composite-material-module.js.map +1 -1
  119. package/dist/gpu/display-view-image-material-module.d.ts +86 -0
  120. package/dist/gpu/display-view-image-material-module.js +339 -0
  121. package/dist/gpu/display-view-image-material-module.js.map +1 -0
  122. package/dist/gpu/display-view-image-quality.d.ts +45 -0
  123. package/dist/gpu/display-view-image-quality.js +380 -0
  124. package/dist/gpu/display-view-image-quality.js.map +1 -0
  125. package/dist/gpu/display-view-quality-gpu.d.ts +67 -1
  126. package/dist/gpu/display-view-quality-gpu.js +136 -205
  127. package/dist/gpu/display-view-quality-gpu.js.map +1 -1
  128. package/dist/gpu/display-view-speed-gpu.d.ts +51 -3
  129. package/dist/gpu/display-view-speed-gpu.js +131 -93
  130. package/dist/gpu/display-view-speed-gpu.js.map +1 -1
  131. package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +29 -15
  132. package/dist/gpu/wgsl-display-view-quality-kernel.js +58 -36
  133. package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
  134. package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +21 -6
  135. package/dist/gpu/wgsl-display-view-speed-kernel.js +46 -11
  136. package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
  137. package/dist/gpu/wgsl-exact-sum.d.ts +20 -0
  138. package/dist/gpu/wgsl-exact-sum.js +42 -0
  139. package/dist/gpu/wgsl-exact-sum.js.map +1 -0
  140. package/dist/index.d.ts +18 -3
  141. package/dist/index.js +12 -1
  142. package/dist/index.js.map +1 -1
  143. package/dist/motion/subject-motion.d.ts +66 -0
  144. package/dist/motion/subject-motion.js +52 -0
  145. package/dist/motion/subject-motion.js.map +1 -0
  146. package/dist/optics/aperture-diameter.d.ts +47 -0
  147. package/dist/optics/aperture-diameter.js +50 -0
  148. package/dist/optics/aperture-diameter.js.map +1 -0
  149. package/dist/optics/detail-limit.d.ts +182 -0
  150. package/dist/optics/detail-limit.js +336 -0
  151. package/dist/optics/detail-limit.js.map +1 -0
  152. package/dist/optics/generic-lens-lateral-color.d.ts +83 -0
  153. package/dist/optics/generic-lens-lateral-color.js +77 -0
  154. package/dist/optics/generic-lens-lateral-color.js.map +1 -0
  155. package/dist/optics/generic-lens-psf.d.ts +18 -2
  156. package/dist/optics/generic-lens-psf.js +79 -48
  157. package/dist/optics/generic-lens-psf.js.map +1 -1
  158. package/dist/optics/lens-psf-profile.d.ts +7 -0
  159. package/dist/optics/lens-psf-profile.js +2 -1
  160. package/dist/optics/lens-psf-profile.js.map +1 -1
  161. package/dist/optics/scene-to-sensor-irradiance.d.ts +13 -0
  162. package/dist/optics/scene-to-sensor-irradiance.js +7 -0
  163. package/dist/optics/scene-to-sensor-irradiance.js.map +1 -1
  164. package/dist/output/crop-to-match-focal-length.d.ts +62 -0
  165. package/dist/output/crop-to-match-focal-length.js +63 -0
  166. package/dist/output/crop-to-match-focal-length.js.map +1 -0
  167. package/dist/output/developed-lens-corrections.d.ts +19 -5
  168. package/dist/output/developed-lens-corrections.js +30 -11
  169. package/dist/output/developed-lens-corrections.js.map +1 -1
  170. package/dist/output/generic-lens-corrections.d.ts +41 -3
  171. package/dist/output/generic-lens-corrections.js +57 -9
  172. package/dist/output/generic-lens-corrections.js.map +1 -1
  173. package/dist/output/lens-corrections.d.ts +12 -2
  174. package/dist/output/lens-corrections.js +22 -7
  175. package/dist/output/lens-corrections.js.map +1 -1
  176. package/dist/output/print-contrast-reference.js +1 -1
  177. package/dist/output/print-contrast-reference.js.map +1 -1
  178. package/dist/output/print-plan.d.ts +41 -5
  179. package/dist/output/print-plan.js +59 -14
  180. package/dist/output/print-plan.js.map +1 -1
  181. package/dist/output/print-size-blur-noise.d.ts +110 -0
  182. package/dist/output/print-size-blur-noise.js +138 -0
  183. package/dist/output/print-size-blur-noise.js.map +1 -0
  184. package/dist/sensor/binomial-sample.d.ts +3 -0
  185. package/dist/sensor/binomial-sample.js +86 -0
  186. package/dist/sensor/binomial-sample.js.map +1 -0
  187. package/dist/sensor/eqe-electron-rate.js +1 -7
  188. package/dist/sensor/eqe-electron-rate.js.map +1 -1
  189. package/dist/sensor/exposure-window.d.ts +2 -0
  190. package/dist/sensor/exposure-window.js +2 -2
  191. package/dist/sensor/exposure-window.js.map +1 -1
  192. package/dist/sensor/readout-timing.js +2 -23
  193. package/dist/sensor/readout-timing.js.map +1 -1
  194. package/dist/sensor/response-application-compatibility.js +1 -6
  195. package/dist/sensor/response-application-compatibility.js.map +1 -1
  196. package/dist/sensor/responsivity-photocurrent.js +1 -7
  197. package/dist/sensor/responsivity-photocurrent.js.map +1 -1
  198. package/dist/sensor/sensor-characteristics.d.ts +147 -0
  199. package/dist/sensor/sensor-characteristics.js +126 -0
  200. package/dist/sensor/sensor-characteristics.js.map +1 -0
  201. package/dist/sensor/spatial-sample-reduction.js +1 -5
  202. package/dist/sensor/spatial-sample-reduction.js.map +1 -1
  203. package/dist/sensor/spectral-quadrature.js +1 -6
  204. package/dist/sensor/spectral-quadrature.js.map +1 -1
  205. package/dist/sensor/temporal-photo-signal.d.ts +3 -0
  206. package/dist/sensor/temporal-photo-signal.js +5 -2
  207. package/dist/sensor/temporal-photo-signal.js.map +1 -1
  208. package/dist/stabilization/system.d.ts +9 -0
  209. package/dist/stabilization/system.js +5 -6
  210. package/dist/stabilization/system.js.map +1 -1
  211. package/docs/AGENT_GUIDE.md +5 -0
  212. package/docs/API_REFERENCE.md +173 -2
  213. package/docs/QUICK_START.md +213 -6
  214. package/docs/USAGE.md +157 -3
  215. package/docs/api/capture-native-raw-dng.md +6 -0
  216. package/docs/api/core-version.md +1 -1
  217. package/docs/api/equipment-generic-lens-catalogue.md +17 -7
  218. package/docs/api/equipment-sensor-profile-body.md +64 -0
  219. package/docs/api/equipment-sensor-profiles.md +86 -0
  220. package/docs/api/exports.json +357 -2
  221. package/docs/api/facade-display-view-gpu.md +5 -2
  222. package/docs/api/facade-exposure-build-up.md +105 -0
  223. package/docs/api/facade-fast-methods.md +99 -0
  224. package/docs/api/facade-image-plane-enable.md +16 -0
  225. package/docs/api/facade-mixed-illumination.md +67 -0
  226. package/docs/api/facade-representative-light-sources.md +131 -0
  227. package/docs/api/facade-scene-radiance.md +54 -3
  228. package/docs/api/facade-simulate-capture.md +60 -15
  229. package/docs/api/facade-simulate-display-view.md +3 -2
  230. package/docs/api/motion-subject-motion.md +23 -0
  231. package/docs/api/optics-aperture-diameter.md +51 -0
  232. package/docs/api/optics-detail-limit.md +203 -0
  233. package/docs/api/optics-generic-lens-lateral-color.md +79 -0
  234. package/docs/api/output-crop-to-match-focal-length.md +67 -0
  235. package/docs/api/output-developed-lens-corrections.md +10 -1
  236. package/docs/api/output-generic-lens-corrections.md +40 -2
  237. package/docs/api/output-lens-corrections.md +11 -1
  238. package/docs/api/output-print-plan.md +35 -1
  239. package/docs/api/output-print-size-blur-noise.md +115 -0
  240. package/docs/api/sensor-sensor-characteristics.md +105 -0
  241. package/llms.txt +6 -0
  242. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import type { resolveLensPsfConvolutionKernels } from "../optics/lens-psf-kernels.js";
2
2
  import { type SiteKernelRuns } from "../optics/site-kernel-runs.js";
3
- import type { DisplayViewExpectedSignalInput } from "./display-view.js";
3
+ import type { DisplayViewExpectedSignalInput, DisplayViewScene } from "./display-view.js";
4
4
  type Psf = Parameters<typeof resolveLensPsfConvolutionKernels>[0]["psf"];
5
5
  export interface ExpectedSignalSetupInput {
6
6
  nativeWidth: number;
@@ -62,6 +62,8 @@ export interface ExpectedSignalSetupInput {
62
62
  whiteRadiance(nm: number): number;
63
63
  } | undefined;
64
64
  radianceAt(u: number, v: number, nm: number): number;
65
+ /** #581, image-plane scenes: the provider's `imageSignalScene` (the radiance, and the direct path's signals integrated from each pixel's reconstructed spectrum). */
66
+ imageSignalScene?: ((wavelengths: readonly number[], perNode: readonly Float64Array[]) => DisplayViewScene) | undefined;
65
67
  imagingArea: {
66
68
  widthMm: number;
67
69
  heightMm: number;
@@ -64,7 +64,7 @@ export function buildExpectedSignalInput(input) {
64
64
  const scene = materials ? { kind: "materials", materialAt: materials.materialAt,
65
65
  materials: materials.list.map((m) => ("neutral" in m ? { neutral: m.neutral } : { radiance: Float64Array.from(wavelengths, (nm) => m.radiance(nm)) })),
66
66
  whiteRadiance: Float64Array.from(wavelengths, (nm) => materials.whiteRadiance(nm)) } :
67
- { kind: "radiance", radianceAt: input.radianceAt };
67
+ input.imageSignalScene ? input.imageSignalScene(wavelengths, input.perNode) : { kind: "radiance", radianceAt: input.radianceAt };
68
68
  const channelIndex = (x, y) => ((y & 1) === 0 ? ((x & 1) === 0 ? 0 : 1) : ((x & 1) === 0 ? 1 : 2));
69
69
  // Without diffraction the environment's own query arithmetic (native-environment-lean.ts `geometry`, then the rectilinear
70
70
  // still-camera ray and the planar provider's plane point), so boundary points resolve as in the capture.
@@ -1 +1 @@
1
- {"version":3,"file":"expected-signal-setup.js","sourceRoot":"","sources":["../../src/facade/expected-signal-setup.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Internal (#394, not exported from the package): the input of the tabulated expected signal (`createDisplayViewExpectedSignal`)\n * for a resolved `simulateCapture` request, shared by the display view and the capture's tabulated RAW path so both build it\n * the same way: query offsets of each site, the environment's query points, the diffraction kernels and the scene tables.\n *\n * Defocus (#492, `lens.defocus`): the geometric disc D on the site lattice (`createDefocusDiscSiteKernel`) enters as a site\n * kernel, so every query stays on the lattice. Without diffraction every spectral node takes D itself (a wavelength-independent\n * kernel). With diffraction node λ takes K_λ ∗ D, the full 2-D convolution of the diffraction kernel with the disc (radius\n * R_diff + R_defocus): a labelled approximation, since the defocused wave's PSF is not the in-focus Airy pattern convolved\n * with a geometric disc. The convolution is linear, so Σ_t (K_λ ∗ D)(t)·L(p − t) is the diffracted radiance blurred by D, the\n * order the capture's object-space convolutions apply them in. The kernels are built as the capture builds them\n * (`siteDiffractionKernel`, then `convolveSiteKernels`), so the two have the same bits.\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport type { resolveLensPsfConvolutionKernels } from \"../optics/lens-psf-kernels.js\";\nimport { siteDiffractionKernel } from \"../optics/site-diffraction-kernel.js\";\nimport { convolveSiteKernels } from \"../optics/defocus-disc-kernel.js\";\nimport { DEFOCUS_DIRECT_KERNEL_MAXIMUM_RADIUS_SITES, siteKernelRuns, type SiteKernelRuns } from \"../optics/site-kernel-runs.js\";\nimport { resolveProjectionDistanceMm } from \"../motion/camera-rotation.js\";\nimport { hypot3 } from \"../core/portable-math.js\";\nimport type { DisplayViewExpectedSignalInput, DisplayViewScene } from \"./display-view.js\";\n\n/** A unit site kernel of radius 0 (shared: the tables only read it). */\nconst UNIT_TAP = Float64Array.of(1);\n\ntype Psf = Parameters<typeof resolveLensPsfConvolutionKernels>[0][\"psf\"];\n\nexport interface ExpectedSignalSetupInput {\n nativeWidth: number;\n nativeHeight: number;\n /** Frame output width (plane coordinates are 2 / width frame units per site). */\n width: number;\n wavelengths: readonly number[];\n /** Per CFA channel (red, green, blue): expected electrons per unit spectral radiance at each node. */\n perNode: readonly [Float64Array, Float64Array, Float64Array];\n /** Pixel-aperture nodes in sites from the site centre (`apertureNodeOffsets(boxSamples)`) and the box sample count. */\n apertureNodes: readonly { x: number; y: number }[];\n boxSamples: number;\n antiAliasing?: { components: readonly { offsetMicrometers: { x: number; y: number }; normalizedWeight: number }[] } | undefined;\n pitch: number;\n /** Object-space diffraction: the kernel radius, the PSF and its profile wavelengths. */\n diffraction?: { radius: number; psf: Psf; profileWavelengths: readonly number[] } | undefined;\n /**\n * #569: a lens PSF's kernels per spectral node on the site lattice (the catalogue lens's at the frame centre; the display view's\n * lens signal reads each field zone's own), used when there is no diffraction and no defocus disc (the lens's PSF includes both).\n */\n lensKernels?: { radius: number; perNode: Float64Array[] } | undefined;\n /** Defocus (#492): the geometric disc on the site lattice, (2R + 1)² row-major weights; absent or null in focus. */\n defocus?: { radius: number; weights: Float64Array } | null | undefined;\n /**\n * #564: a defocus kernel above the direct radius, as runs, used instead of `defocus`: the tables keep the in-focus kernels\n * (diffraction, or a unit tap per node for the lattice formula) and the expected signal convolves the result with it. A\n * `defocus` disc of radius above 64 sites takes this path too.\n */\n largeDefocus?: SiteKernelRuns | undefined;\n /** Material scenes: the materials (neutral reflectances, or radiance per wavelength), the material lookup and the white radiance. */\n materials?: { list: readonly ({ neutral: number } | { radiance: (nm: number) => number })[]; materialAt(u: number, v: number): number; whiteRadiance(nm: number): number } | undefined;\n radianceAt(u: number, v: number, nm: number): number;\n imagingArea: { widthMm: number; heightMm: number };\n focalLengthMm: number;\n focusDistanceM: number;\n subjectDistanceM: number;\n metresPerFrameUnit: number;\n /** Prefix of the error messages (\"simulateDisplayView\", \"simulateCapture\"). */\n caller: string;\n}\n\n/** The query offsets of each site and the expected-signal input (see the module comment). */\nexport function buildExpectedSignalInput(input: ExpectedSignalSetupInput): { offsets: { x: number; y: number; weight: number }[]; signalInput: DisplayViewExpectedSignalInput } {\n const { boxSamples, pitch, wavelengths, imagingArea, subjectDistanceM, metresPerFrameUnit } = input;\n // Query offsets of each site: pixel-aperture nodes minus anti-aliasing split offsets (sites, +x right, +y down), as the environment forms them.\n const spots = input.antiAliasing ? input.antiAliasing.components : [{ offsetMicrometers: { x: 0, y: 0 }, normalizedWeight: 1 }];\n const offsets = input.apertureNodes.flatMap((node) => spots.map((c) => ({ x: node.x - c.offsetMicrometers.x / pitch, y: node.y - c.offsetMicrometers.y / pitch,\n weight: c.normalizedWeight / (boxSamples * boxSamples) })));\n // Diffraction: the same per-node site kernels as the object-space convolution (createDiffractedPlanarProvider).\n let kernels: { radius: number; perNode: Float64Array[] } | undefined;\n const diffraction = input.diffraction;\n if (diffraction) {\n const radius = diffraction.radius;\n kernels = { radius, perNode: wavelengths.map((wavelengthNm) => {\n if (!diffraction.profileWavelengths.includes(wavelengthNm)) throw new InvalidConfigurationError(input.caller + \": a spectral node has no diffraction kernel.\");\n return siteDiffractionKernel(diffraction.psf, wavelengthNm, pitch, radius, input.caller + \": diffraction kernels must sample the site pitch exactly.\");\n }) };\n }\n if (input.lensKernels) {\n if (diffraction || input.defocus || input.largeDefocus) throw new InvalidConfigurationError(input.caller + \": a lens PSF includes its own diffraction and defocus.\");\n if (input.lensKernels.perNode.length !== wavelengths.length) throw new InvalidConfigurationError(input.caller + \": the lens PSF needs one kernel per spectral node.\");\n kernels = input.lensKernels;\n }\n // #564: a disc above the direct radius (64 sites) is convolved by runs after the in-focus signal instead (below).\n const largeDefocus = input.largeDefocus ?? (input.defocus && input.defocus.radius > DEFOCUS_DIRECT_KERNEL_MAXIMUM_RADIUS_SITES ?\n siteKernelRuns(input.defocus.weights, input.defocus.radius) : undefined);\n // Defocus (#492): the disc alone at every node (one shared array), or each node's diffraction kernel convolved with it.\n const defocus = largeDefocus ? undefined : input.defocus;\n if (defocus) {\n const diffracted = kernels;\n kernels = diffracted ? { radius: diffracted.radius + defocus.radius, perNode: diffracted.perNode.map((k) => convolveSiteKernels(k, diffracted.radius, defocus.weights, defocus.radius)) } :\n { radius: defocus.radius, perNode: wavelengths.map(() => defocus.weights) };\n }\n // #564: a large defocus kernel is applied after the in-focus signal (expected-signal.ts); without diffraction the in-focus\n // kernels are a unit tap per node, so the tables read the scene on the lattice formula as the defocus path does.\n if (largeDefocus && !kernels) kernels = { radius: 0, perNode: wavelengths.map(() => UNIT_TAP) };\n // Chart presets and (2.7) caller material scenes take the material path: each material tabulated once per node.\n const materials = input.materials;\n const scene: DisplayViewScene = materials ? { kind: \"materials\", materialAt: materials.materialAt,\n materials: materials.list.map((m) => (\"neutral\" in m ? { neutral: m.neutral } : { radiance: Float64Array.from(wavelengths, (nm) => m.radiance(nm)) })),\n whiteRadiance: Float64Array.from(wavelengths, (nm) => materials.whiteRadiance(nm)) } :\n { kind: \"radiance\", radianceAt: input.radianceAt };\n const channelIndex = (x: number, y: number): number => ((y & 1) === 0 ? ((x & 1) === 0 ? 0 : 1) : ((x & 1) === 0 ? 1 : 2));\n // Without diffraction the environment's own query arithmetic (native-environment-lean.ts `geometry`, then the rectilinear\n // still-camera ray and the planar provider's plane point), so boundary points resolve as in the capture.\n const projectionDistance = resolveProjectionDistanceMm(input.focalLengthMm, input.focusDistanceM).projectionDistanceMm, siteWidthMm = pitch / 1000;\n const nodePhases = input.apertureNodes.flatMap((_, index) => spots.map((c) => ({ xPhase: (index % boxSamples + 0.5) / boxSamples, yPhase: (Math.floor(index / boxSamples) + 0.5) / boxSamples,\n aaX: c.offsetMicrometers.x / 1000, aaY: c.offsetMicrometers.y / 1000 })));\n const queryPoint = (x: number, y: number, o: number, out: Float64Array): void => {\n const node = nodePhases[o]!;\n const centerX = -imagingArea.widthMm / 2 + (pitch / 2 + x * pitch + 0) / 1000, centerY = -imagingArea.heightMm / 2 + (pitch / 2 + y * pitch + 0) / 1000;\n const sourceX = (centerX - siteWidthMm / 2 + node.xPhase * siteWidthMm) - node.aaX, sourceY = (centerY - siteWidthMm / 2 + node.yPhase * siteWidthMm) - node.aaY;\n const rx = projectionDistance * (sourceX / projectionDistance), ry = projectionDistance * (-sourceY / projectionDistance);\n const length = hypot3(rx, ry, projectionDistance), t = subjectDistanceM / (projectionDistance / length);\n out[0] = rx / length * t / metresPerFrameUnit; out[1] = ry / length * t / metresPerFrameUnit;\n };\n return { offsets, signalInput: { nativeWidth: input.nativeWidth, nativeHeight: input.nativeHeight, outputWidth: input.width, wavelengthsNm: wavelengths,\n electronsPerRadiance: input.perNode, offsets, ...(kernels ? { kernels } : { queryPoint }), ...(largeDefocus ? { largeKernel: largeDefocus } : {}), scene, channelAt: channelIndex } };\n}\n\n"]}
1
+ {"version":3,"file":"expected-signal-setup.js","sourceRoot":"","sources":["../../src/facade/expected-signal-setup.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Internal (#394, not exported from the package): the input of the tabulated expected signal (`createDisplayViewExpectedSignal`)\n * for a resolved `simulateCapture` request, shared by the display view and the capture's tabulated RAW path so both build it\n * the same way: query offsets of each site, the environment's query points, the diffraction kernels and the scene tables.\n *\n * Defocus (#492, `lens.defocus`): the geometric disc D on the site lattice (`createDefocusDiscSiteKernel`) enters as a site\n * kernel, so every query stays on the lattice. Without diffraction every spectral node takes D itself (a wavelength-independent\n * kernel). With diffraction node λ takes K_λ ∗ D, the full 2-D convolution of the diffraction kernel with the disc (radius\n * R_diff + R_defocus): a labelled approximation, since the defocused wave's PSF is not the in-focus Airy pattern convolved\n * with a geometric disc. The convolution is linear, so Σ_t (K_λ ∗ D)(t)·L(p − t) is the diffracted radiance blurred by D, the\n * order the capture's object-space convolutions apply them in. The kernels are built as the capture builds them\n * (`siteDiffractionKernel`, then `convolveSiteKernels`), so the two have the same bits.\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport type { resolveLensPsfConvolutionKernels } from \"../optics/lens-psf-kernels.js\";\nimport { siteDiffractionKernel } from \"../optics/site-diffraction-kernel.js\";\nimport { convolveSiteKernels } from \"../optics/defocus-disc-kernel.js\";\nimport { DEFOCUS_DIRECT_KERNEL_MAXIMUM_RADIUS_SITES, siteKernelRuns, type SiteKernelRuns } from \"../optics/site-kernel-runs.js\";\nimport { resolveProjectionDistanceMm } from \"../motion/camera-rotation.js\";\nimport { hypot3 } from \"../core/portable-math.js\";\nimport type { DisplayViewExpectedSignalInput, DisplayViewScene } from \"./display-view.js\";\n\n/** A unit site kernel of radius 0 (shared: the tables only read it). */\nconst UNIT_TAP = Float64Array.of(1);\n\ntype Psf = Parameters<typeof resolveLensPsfConvolutionKernels>[0][\"psf\"];\n\nexport interface ExpectedSignalSetupInput {\n nativeWidth: number;\n nativeHeight: number;\n /** Frame output width (plane coordinates are 2 / width frame units per site). */\n width: number;\n wavelengths: readonly number[];\n /** Per CFA channel (red, green, blue): expected electrons per unit spectral radiance at each node. */\n perNode: readonly [Float64Array, Float64Array, Float64Array];\n /** Pixel-aperture nodes in sites from the site centre (`apertureNodeOffsets(boxSamples)`) and the box sample count. */\n apertureNodes: readonly { x: number; y: number }[];\n boxSamples: number;\n antiAliasing?: { components: readonly { offsetMicrometers: { x: number; y: number }; normalizedWeight: number }[] } | undefined;\n pitch: number;\n /** Object-space diffraction: the kernel radius, the PSF and its profile wavelengths. */\n diffraction?: { radius: number; psf: Psf; profileWavelengths: readonly number[] } | undefined;\n /**\n * #569: a lens PSF's kernels per spectral node on the site lattice (the catalogue lens's at the frame centre; the display view's\n * lens signal reads each field zone's own), used when there is no diffraction and no defocus disc (the lens's PSF includes both).\n */\n lensKernels?: { radius: number; perNode: Float64Array[] } | undefined;\n /** Defocus (#492): the geometric disc on the site lattice, (2R + 1)² row-major weights; absent or null in focus. */\n defocus?: { radius: number; weights: Float64Array } | null | undefined;\n /**\n * #564: a defocus kernel above the direct radius, as runs, used instead of `defocus`: the tables keep the in-focus kernels\n * (diffraction, or a unit tap per node for the lattice formula) and the expected signal convolves the result with it. A\n * `defocus` disc of radius above 64 sites takes this path too.\n */\n largeDefocus?: SiteKernelRuns | undefined;\n /** Material scenes: the materials (neutral reflectances, or radiance per wavelength), the material lookup and the white radiance. */\n materials?: { list: readonly ({ neutral: number } | { radiance: (nm: number) => number })[]; materialAt(u: number, v: number): number; whiteRadiance(nm: number): number } | undefined;\n radianceAt(u: number, v: number, nm: number): number;\n /** #581, image-plane scenes: the provider's `imageSignalScene` (the radiance, and the direct path's signals integrated from each pixel's reconstructed spectrum). */\n imageSignalScene?: ((wavelengths: readonly number[], perNode: readonly Float64Array[]) => DisplayViewScene) | undefined;\n imagingArea: { widthMm: number; heightMm: number };\n focalLengthMm: number;\n focusDistanceM: number;\n subjectDistanceM: number;\n metresPerFrameUnit: number;\n /** Prefix of the error messages (\"simulateDisplayView\", \"simulateCapture\"). */\n caller: string;\n}\n\n/** The query offsets of each site and the expected-signal input (see the module comment). */\nexport function buildExpectedSignalInput(input: ExpectedSignalSetupInput): { offsets: { x: number; y: number; weight: number }[]; signalInput: DisplayViewExpectedSignalInput } {\n const { boxSamples, pitch, wavelengths, imagingArea, subjectDistanceM, metresPerFrameUnit } = input;\n // Query offsets of each site: pixel-aperture nodes minus anti-aliasing split offsets (sites, +x right, +y down), as the environment forms them.\n const spots = input.antiAliasing ? input.antiAliasing.components : [{ offsetMicrometers: { x: 0, y: 0 }, normalizedWeight: 1 }];\n const offsets = input.apertureNodes.flatMap((node) => spots.map((c) => ({ x: node.x - c.offsetMicrometers.x / pitch, y: node.y - c.offsetMicrometers.y / pitch,\n weight: c.normalizedWeight / (boxSamples * boxSamples) })));\n // Diffraction: the same per-node site kernels as the object-space convolution (createDiffractedPlanarProvider).\n let kernels: { radius: number; perNode: Float64Array[] } | undefined;\n const diffraction = input.diffraction;\n if (diffraction) {\n const radius = diffraction.radius;\n kernels = { radius, perNode: wavelengths.map((wavelengthNm) => {\n if (!diffraction.profileWavelengths.includes(wavelengthNm)) throw new InvalidConfigurationError(input.caller + \": a spectral node has no diffraction kernel.\");\n return siteDiffractionKernel(diffraction.psf, wavelengthNm, pitch, radius, input.caller + \": diffraction kernels must sample the site pitch exactly.\");\n }) };\n }\n if (input.lensKernels) {\n if (diffraction || input.defocus || input.largeDefocus) throw new InvalidConfigurationError(input.caller + \": a lens PSF includes its own diffraction and defocus.\");\n if (input.lensKernels.perNode.length !== wavelengths.length) throw new InvalidConfigurationError(input.caller + \": the lens PSF needs one kernel per spectral node.\");\n kernels = input.lensKernels;\n }\n // #564: a disc above the direct radius (64 sites) is convolved by runs after the in-focus signal instead (below).\n const largeDefocus = input.largeDefocus ?? (input.defocus && input.defocus.radius > DEFOCUS_DIRECT_KERNEL_MAXIMUM_RADIUS_SITES ?\n siteKernelRuns(input.defocus.weights, input.defocus.radius) : undefined);\n // Defocus (#492): the disc alone at every node (one shared array), or each node's diffraction kernel convolved with it.\n const defocus = largeDefocus ? undefined : input.defocus;\n if (defocus) {\n const diffracted = kernels;\n kernels = diffracted ? { radius: diffracted.radius + defocus.radius, perNode: diffracted.perNode.map((k) => convolveSiteKernels(k, diffracted.radius, defocus.weights, defocus.radius)) } :\n { radius: defocus.radius, perNode: wavelengths.map(() => defocus.weights) };\n }\n // #564: a large defocus kernel is applied after the in-focus signal (expected-signal.ts); without diffraction the in-focus\n // kernels are a unit tap per node, so the tables read the scene on the lattice formula as the defocus path does.\n if (largeDefocus && !kernels) kernels = { radius: 0, perNode: wavelengths.map(() => UNIT_TAP) };\n // Chart presets and (2.7) caller material scenes take the material path: each material tabulated once per node.\n const materials = input.materials;\n const scene: DisplayViewScene = materials ? { kind: \"materials\", materialAt: materials.materialAt,\n materials: materials.list.map((m) => (\"neutral\" in m ? { neutral: m.neutral } : { radiance: Float64Array.from(wavelengths, (nm) => m.radiance(nm)) })),\n whiteRadiance: Float64Array.from(wavelengths, (nm) => materials.whiteRadiance(nm)) } :\n input.imageSignalScene ? input.imageSignalScene(wavelengths, input.perNode) : { kind: \"radiance\", radianceAt: input.radianceAt };\n const channelIndex = (x: number, y: number): number => ((y & 1) === 0 ? ((x & 1) === 0 ? 0 : 1) : ((x & 1) === 0 ? 1 : 2));\n // Without diffraction the environment's own query arithmetic (native-environment-lean.ts `geometry`, then the rectilinear\n // still-camera ray and the planar provider's plane point), so boundary points resolve as in the capture.\n const projectionDistance = resolveProjectionDistanceMm(input.focalLengthMm, input.focusDistanceM).projectionDistanceMm, siteWidthMm = pitch / 1000;\n const nodePhases = input.apertureNodes.flatMap((_, index) => spots.map((c) => ({ xPhase: (index % boxSamples + 0.5) / boxSamples, yPhase: (Math.floor(index / boxSamples) + 0.5) / boxSamples,\n aaX: c.offsetMicrometers.x / 1000, aaY: c.offsetMicrometers.y / 1000 })));\n const queryPoint = (x: number, y: number, o: number, out: Float64Array): void => {\n const node = nodePhases[o]!;\n const centerX = -imagingArea.widthMm / 2 + (pitch / 2 + x * pitch + 0) / 1000, centerY = -imagingArea.heightMm / 2 + (pitch / 2 + y * pitch + 0) / 1000;\n const sourceX = (centerX - siteWidthMm / 2 + node.xPhase * siteWidthMm) - node.aaX, sourceY = (centerY - siteWidthMm / 2 + node.yPhase * siteWidthMm) - node.aaY;\n const rx = projectionDistance * (sourceX / projectionDistance), ry = projectionDistance * (-sourceY / projectionDistance);\n const length = hypot3(rx, ry, projectionDistance), t = subjectDistanceM / (projectionDistance / length);\n out[0] = rx / length * t / metresPerFrameUnit; out[1] = ry / length * t / metresPerFrameUnit;\n };\n return { offsets, signalInput: { nativeWidth: input.nativeWidth, nativeHeight: input.nativeHeight, outputWidth: input.width, wavelengthsNm: wavelengths,\n electronsPerRadiance: input.perNode, offsets, ...(kernels ? { kernels } : { queryPoint }), ...(largeDefocus ? { largeKernel: largeDefocus } : {}), scene, channelAt: channelIndex } };\n}\n"]}
@@ -1,3 +1,4 @@
1
+ import type { ImagePlaneSignalScene } from "./scene-radiance.js";
1
2
  import { type SiteKernelRuns } from "../optics/site-kernel-runs.js";
2
3
  /** A query offset from a site centre in sites (+x right, +y down) and its weight (pixel-aperture node × filter spot). */
3
4
  export interface DisplayViewQueryOffset {
@@ -19,7 +20,14 @@ export type DisplayViewScene = {
19
20
  whiteRadiance: Float64Array;
20
21
  } | {
21
22
  kind: "radiance";
22
- radianceAt(u: number, v: number, nm: number): number;
23
+ radianceAt(u: number, v: number, nm: number, t?: number): number;
24
+ /**
25
+ * #581, an image-plane scene's direct path: the signal of channel c, each pixel's Σ_n A_c(λ_n)·L_n integrated from its
26
+ * reconstructed spectrum and interpolated between pixels (a site without kernels takes it instead of one radiance lookup per node).
27
+ */
28
+ signalAt?(u: number, v: number, c: number): number;
29
+ /** #581: the image-plane print behind `signalAt` (its scene, spectral nodes and surround), for the GPU's image-plane module. */
30
+ image?: ImagePlaneSignalScene["image"];
23
31
  };
24
32
  export interface DisplayViewExpectedSignalInput {
25
33
  nativeWidth: number;
@@ -103,21 +111,31 @@ export interface DisplayViewExpectedSignal {
103
111
  markReads(xs: Int32Array, ys: Int32Array, values: Float64Array, materials: Int32Array, count: number): void;
104
112
  }
105
113
  /**
106
- * The tables of `createDisplayViewExpectedSignal` (see there) that are a function of the input's numbers alone: the materials,
107
- * the lattice groups' geometry and combined kernels, and the per-material sums. Written only while they are built, never after:
108
- * they are shared by every render of an input and, for chart materials, by every input of the same content (#449). Everything a
109
- * render changes (each group's window cache and its origin and size, the bands, rings and counts) lives in the render.
114
+ * The tables of `createDisplayViewExpectedSignal` (see there) that are a function of the input's numbers alone: the materials'
115
+ * spectra, the lattice groups' geometry and their combined kernels per spectral node, and each channel's kernel sums per node.
116
+ * Written only while they are built, never after: they are shared by every render of an input and, for chart materials, by every
117
+ * input of the same content (#449). Everything a render changes (each group's window cache and its origin and size, the bands,
118
+ * rings and counts) lives in the render.
119
+ *
120
+ * Signals are integrated from the material spectrum per site (2.21.0): no table holds a material's value for a channel. A
121
+ * material is kept as its spectral radiance at the nodes, L_m(λ_n) (a neutral one: its reflectance times the white radiance),
122
+ * and a site's signal is formed from the spectra of the materials it reads, against its own channel's response, when it is read.
110
123
  */
111
124
  export interface SignalTables {
112
- readonly reflectance: Float64Array;
113
- readonly spectralIndex: Int32Array;
114
- readonly spectral: readonly Float64Array[];
125
+ /** Per chart material, its spectral radiance at each node (empty for radiance scenes). */
126
+ readonly spectra: readonly Float64Array[];
115
127
  readonly groups: readonly GroupTables[];
116
- readonly neutralDirect: readonly number[];
117
- readonly spectralDirect: readonly (readonly number[])[];
118
- readonly uniformSite: readonly Float64Array[];
128
+ /**
129
+ * With kernels, chart materials: per channel c and node n, S_c(n) = Σ_groups Σ_t C_c(t, n), summed in the order a site reads its
130
+ * taps (groups in order, taps row-major), so a site whose every tap reads one material is the same sum as any other site.
131
+ */
132
+ readonly kernelSums: readonly Float64Array[];
119
133
  }
120
- /** A lattice group's fixed part: its fractional shift, members, index bounds, rectangle and combined kernels. */
134
+ /**
135
+ * A lattice group's fixed part: its fractional shift, members, index bounds, rectangle and combined kernels. Chart materials:
136
+ * `nodeKernels[c]` holds C_c(t, n) = Σ_members w·A_c(λ_n)·K_n(t) at t·N + n (a channel's response and kernel per node, no
137
+ * material). Radiance scenes: `perNode[n][c]` the same values per node.
138
+ */
121
139
  export interface GroupTables {
122
140
  fx: number;
123
141
  fy: number;
@@ -132,8 +150,7 @@ export interface GroupTables {
132
150
  maxIy: number;
133
151
  cw: number;
134
152
  ch: number;
135
- neutral: Float64Array[];
136
- spectral: Float64Array[][];
153
+ nodeKernels: Float64Array[];
137
154
  perNode: Float64Array[][];
138
155
  }
139
156
  /** Chart-material tables kept by content across input objects (#449), least recently used first. */
@@ -157,6 +174,22 @@ export declare function displayViewSignalTables(input: DisplayViewExpectedSignal
157
174
  * which would only evict the view's own entries).
158
175
  */
159
176
  export declare function buildTables(input: DisplayViewExpectedSignalInput): SignalTables;
177
+ /**
178
+ * A site's per-material kernel sums (2.21.0, signals integrated from the material spectrum per site). For each distinct chart
179
+ * material the site's taps read, P_m(n) = Σ_{taps t that read m} C_c(t, n), added in tap order; the site's signal is then
180
+ * Σ_m Σ_n L_m(λ_n)·P_m(n), the materials in the order of their first tap. A site whose every tap reads material m has P_m = S_c
181
+ * (`SignalTables.kernelSums`, summed in the same order), so adding S_c once gives the same value without reading its taps.
182
+ */
183
+ export interface MaterialSums {
184
+ reset(): void;
185
+ /** Adds f·k[at .. at + N) to material m's sums (f: a tap's field factor; 1, an exact product, without one). */
186
+ add(m: number, k: Float64Array, at: number, f?: number): void;
187
+ /** Σ_m Σ_n L_m(λ_n)·P_m(n) over the materials added since the last reset. */
188
+ value(): number;
189
+ }
190
+ export declare function createMaterialSums(nodes: number, spectra: readonly Float64Array[]): MaterialSums;
191
+ /** A chart material's signal for channel response a without kernels: Σ_n A_c(λ_n)·L_m(λ_n), integrated from its spectrum. */
192
+ export declare function materialSpectrumSignal(a: ArrayLike<number>, spectrum: ArrayLike<number> | undefined, nodes: number): number;
160
193
  /**
161
194
  * Expected photo electrons per site: μ_c(i, j) = Σ_o w_o Σ_λ A_c(λ) Σ_t K_λ(t)·L(p_o(i, j) − t, λ), where p_o is the
162
195
  * query point of offset o and K the diffraction kernel (a single unit tap without diffraction). This is the sum the
@@ -111,20 +111,10 @@ export function buildTables(input) {
111
111
  const nodes = input.wavelengthsNm.length, R = input.kernels ? input.kernels.radius : 0, size = 2 * R + 1;
112
112
  const A = input.electronsPerRadiance, scene = input.scene, offsets = input.offsets;
113
113
  const kernelAt = (node, t) => (input.kernels ? input.kernels.perNode[node][t] : 1);
114
- // Materials: neutral ones by reflectance, spectral ones by index.
115
- let reflectance = new Float64Array(0), spectralIndex = new Int32Array(0);
116
- const spectral = [];
117
- if (scene.kind === "materials") {
118
- reflectance = new Float64Array(scene.materials.length);
119
- spectralIndex = new Int32Array(scene.materials.length).fill(-1);
120
- // A copy of each spectral radiance: the tables may outlive the input and serve other inputs of the same content.
121
- scene.materials.forEach((m, index) => { if ("neutral" in m)
122
- reflectance[index] = m.neutral;
123
- else {
124
- spectralIndex[index] = spectral.length;
125
- spectral.push(Float64Array.from(m.radiance));
126
- } });
127
- }
114
+ // Each material's spectral radiance at the nodes (a copy: the tables may outlive the input and serve other inputs of the same
115
+ // content). A neutral material's spectrum is its reflectance times the white radiance.
116
+ const spectra = scene.kind === "materials" ?
117
+ scene.materials.map((m) => ("neutral" in m ? Float64Array.from(scene.whiteRadiance, (w) => m.neutral * w) : Float64Array.from(m.radiance))) : [];
128
118
  // Offsets grouped by fractional shift: offsets that differ by whole sites share one lattice.
129
119
  const frac = (v) => { const f = v - Math.floor(v); return f > 1 - OFFSET_TOLERANCE || f < OFFSET_TOLERANCE ? 0 : f; };
130
120
  const groups = [];
@@ -132,7 +122,7 @@ export function buildTables(input) {
132
122
  const fx = frac(o.x), fy = frac(o.y), ix = Math.round(o.x - fx), iy = Math.round(o.y - fy);
133
123
  let group = groups.find((q) => Math.abs(q.fx - fx) <= OFFSET_TOLERANCE && Math.abs(q.fy - fy) <= OFFSET_TOLERANCE);
134
124
  if (!group) {
135
- group = { fx, fy, members: [], minIx: ix, maxIx: ix, minIy: iy, maxIy: iy, cw: 0, ch: 0, neutral: [], spectral: [], perNode: [] };
125
+ group = { fx, fy, members: [], minIx: ix, maxIx: ix, minIy: iy, maxIy: iy, cw: 0, ch: 0, nodeKernels: [], perNode: [] };
136
126
  groups.push(group);
137
127
  }
138
128
  group.members.push({ ix, iy, weight: o.weight });
@@ -161,34 +151,89 @@ export function buildTables(input) {
161
151
  return out;
162
152
  });
163
153
  if (scene.kind === "materials") {
164
- group.neutral = combine((c, n, t) => A[c][n] * kernelAt(n, t) * scene.whiteRadiance[n]);
165
- const perMaterial = spectral.map((radiance) => combine((c, n, t) => A[c][n] * kernelAt(n, t) * radiance[n]));
166
- group.spectral = [0, 1, 2].map((c) => perMaterial.map((tables) => tables[c]));
154
+ // The channel's response and kernel per node, C_c(t, n) at t·N + n: no material radiance is folded in.
155
+ group.nodeKernels = [0, 1, 2].map((c) => {
156
+ const out = new Float64Array(cw * ch * nodes);
157
+ for (const member of group.members)
158
+ for (let tv = -R, t = 0; tv <= R; tv++)
159
+ for (let tu = -R; tu <= R; tu++, t++) {
160
+ const index = ((member.iy - tv - group.minIy + R) * cw + (member.ix - tu - group.minIx + R)) * nodes;
161
+ for (let n = 0; n < nodes; n++)
162
+ out[index + n] = out[index + n] + member.weight * (A[c][n] * kernelAt(n, t));
163
+ }
164
+ return out;
165
+ });
167
166
  }
168
167
  else {
169
168
  // Radiance scenes: combined kernels per node, applied to the cached radiance at each node.
170
169
  group.perNode = Array.from({ length: nodes }, (_, n) => combine((c, node, t) => (node === n ? A[c][n] * kernelAt(n, t) : 0)));
171
170
  }
172
171
  }
173
- // Neutral tap-0 signal per channel and spectral per material, for the direct (no-kernel) path.
174
- const neutralDirect = scene.kind === "materials" ? [0, 1, 2].map((c) => { let s = 0; for (let n = 0; n < nodes; n++)
175
- s += A[c][n] * scene.whiteRadiance[n]; return s; }) : [];
176
- const spectralDirect = [0, 1, 2].map((c) => spectral.map((radiance) => { let s = 0; for (let n = 0; n < nodes; n++)
177
- s += A[c][n] * radiance[n]; return s; }));
178
- const uniformSite = scene.kind === "materials" && input.kernels ? [0, 1, 2].map((c) => Float64Array.from(scene.materials, (_, m) => {
179
- const si = spectralIndex[m];
180
- let sum = 0;
172
+ // Per channel the kernel sums per node, in a site's tap order (see SignalTables.kernelSums): response and kernels only.
173
+ const kernelSums = scene.kind === "materials" && input.kernels ? [0, 1, 2].map((c) => {
174
+ const out = new Float64Array(nodes);
181
175
  for (const group of groups) {
182
- const nk = group.neutral[c], sk = si < 0 ? nk : group.spectral[c][si];
176
+ const k = group.nodeKernels[c];
183
177
  for (let t = 0; t < group.cw * group.ch; t++)
184
- sum += si < 0 ? reflectance[m] * nk[t] : sk[t];
178
+ for (let n = 0; n < nodes; n++)
179
+ out[n] = out[n] + k[t * nodes + n];
185
180
  }
186
- return sum;
187
- })) : [];
181
+ return out;
182
+ }) : [];
188
183
  // Frozen records: a render copies each group before it gives it a window (see createDisplayViewExpectedSignal).
189
184
  for (const group of groups)
190
185
  Object.freeze(group);
191
- return Object.freeze({ reflectance, spectralIndex, spectral, groups, neutralDirect, spectralDirect, uniformSite });
186
+ return Object.freeze({ spectra, groups, kernelSums });
187
+ }
188
+ export function createMaterialSums(nodes, spectra) {
189
+ // A site reads at most every material once: one slot per material, in the order of the site's first taps.
190
+ let count = 0, last = -1, lastAt = 0;
191
+ const ids = new Int32Array(spectra.length), sums = new Float64Array(spectra.length * nodes);
192
+ /** The offset of material m's sums, a new zeroed slot when the site has not read m yet. */
193
+ const slot = (m) => {
194
+ if (m === last)
195
+ return lastAt;
196
+ let a = 0;
197
+ while (a < count && ids[a] !== m)
198
+ a++;
199
+ if (a === count) {
200
+ if (spectra[m] === undefined)
201
+ throw new InvalidConfigurationError("Unknown chart material.");
202
+ ids[count++] = m;
203
+ sums.fill(0, a * nodes, (a + 1) * nodes);
204
+ }
205
+ last = m;
206
+ lastAt = a * nodes;
207
+ return lastAt;
208
+ };
209
+ return {
210
+ reset() { count = 0; last = -1; },
211
+ add(m, k, at, f = 1) {
212
+ const base = slot(m);
213
+ for (let n = 0; n < nodes; n++)
214
+ sums[base + n] = sums[base + n] + f * k[at + n];
215
+ },
216
+ value() {
217
+ let sum = 0;
218
+ for (let a = 0; a < count; a++) {
219
+ const L = spectra[ids[a]], base = a * nodes;
220
+ let v = 0;
221
+ for (let n = 0; n < nodes; n++)
222
+ v += L[n] * sums[base + n];
223
+ sum += v;
224
+ }
225
+ return sum;
226
+ }
227
+ };
228
+ }
229
+ /** A chart material's signal for channel response a without kernels: Σ_n A_c(λ_n)·L_m(λ_n), integrated from its spectrum. */
230
+ export function materialSpectrumSignal(a, spectrum, nodes) {
231
+ if (!spectrum)
232
+ throw new InvalidConfigurationError("Unknown chart material.");
233
+ let s = 0;
234
+ for (let n = 0; n < nodes; n++)
235
+ s += a[n] * spectrum[n];
236
+ return s;
192
237
  }
193
238
  /**
194
239
  * Expected photo electrons per site: μ_c(i, j) = Σ_o w_o Σ_λ A_c(λ) Σ_t K_λ(t)·L(p_o(i, j) − t, λ), where p_o is the
@@ -217,16 +262,10 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
217
262
  // prepared view, which passes the same input object: computed once per input. Each render gets its own groups' window caches.
218
263
  // Chart-material tables are also shared by inputs of the same content (#449): each render gets its own groups' window state.
219
264
  const prepared = PREPARED.get(input) ?? prepareSignal(input);
220
- const { reflectance, spectralIndex, spectral, neutralDirect, spectralDirect, uniformSite } = prepared.tables;
265
+ const { spectra, kernelSums } = prepared.tables;
221
266
  const groups = prepared.tables.groups.map((group) => ({ ...group, cache: new Int32Array(0), cx0: 0, cy0: 0, cpw: 0, cph: 0 }));
222
267
  let lookups = 0;
223
- const point = new Float64Array(2);
224
- const materialSignalDirect = (m, c) => {
225
- const si = spectralIndex[m];
226
- if (si === undefined)
227
- throw new InvalidConfigurationError("Unknown chart material.");
228
- return si < 0 ? reflectance[m] * neutralDirect[c] : spectralDirect[c][si];
229
- };
268
+ const point = new Float64Array(2), acc = createMaterialSums(nodes, spectra);
230
269
  /**
231
270
  * Without diffraction: one site from the environment's own query points. Also sets lastMaterial: the chart material every
232
271
  * query point read, or −1 when they read more than one (or the scene is a radiance scene). A site whose points all read
@@ -234,14 +273,23 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
234
273
  */
235
274
  function directSite(x, y) {
236
275
  const c = channelAt(x, y);
237
- let sum = 0, uniform = -1;
276
+ let sum = 0, uniform = -1, lastM = -1, lastValue = 0;
238
277
  for (let o = 0; o < offsets.length; o++) {
239
278
  input.queryPoint(x, y, o, point);
240
279
  let value;
241
280
  if (scene.kind === "materials") {
242
281
  const m = scene.materialAt(point[0], point[1]);
243
282
  uniform = o === 0 || uniform === m ? m : -2;
244
- value = materialSignalDirect(m, c);
283
+ // Integrated from the material's spectrum against the site's channel, once per material change within the site.
284
+ if (m !== lastM) {
285
+ lastValue = materialSpectrumSignal(A[c], spectra[m], nodes);
286
+ lastM = m;
287
+ }
288
+ value = lastValue;
289
+ lookups++;
290
+ }
291
+ else if (scene.signalAt) {
292
+ value = scene.signalAt(point[0], point[1], c);
245
293
  lookups++;
246
294
  }
247
295
  else {
@@ -293,17 +341,14 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
293
341
  function kernelSite(x, y) {
294
342
  const c = channelAt(x, y);
295
343
  let sum = 0;
344
+ acc.reset();
296
345
  for (const group of groups) {
297
346
  const p0 = x + group.minIx - R, q0 = y + group.minIy - R, cw = group.cw, ch = group.ch;
298
347
  if (scene.kind === "materials") {
299
- const nk = group.neutral[c], sk = group.spectral[c];
348
+ const k = group.nodeKernels[c];
300
349
  for (let dy = 0; dy < ch; dy++)
301
- for (let dx = 0; dx < cw; dx++) {
302
- const m = latticeMaterial(group, p0 + dx, q0 + dy), si = spectralIndex[m];
303
- if (si === undefined)
304
- throw new InvalidConfigurationError("Unknown chart material.");
305
- sum += si < 0 ? reflectance[m] * nk[dy * cw + dx] : sk[si][dy * cw + dx];
306
- }
350
+ for (let dx = 0; dx < cw; dx++)
351
+ acc.add(latticeMaterial(group, p0 + dx, q0 + dy), k, (dy * cw + dx) * nodes);
307
352
  }
308
353
  else {
309
354
  for (let dy = 0; dy < ch; dy++)
@@ -314,18 +359,9 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
314
359
  }
315
360
  }
316
361
  }
317
- return sum;
362
+ return scene.kind === "materials" ? acc.value() : sum;
318
363
  }
319
- // Filled windows (chart materials with diffraction, every lattice point of the window cacheable): each group keeps a band of
320
- // looked-up lattice materials that covers the window's rows and grows to the right as the windows of a display row move
321
- // right, with summed-area tables of material changes. A site whose kernel reads one material only (each group's rectangle
322
- // uniform, the same material in all) takes the value of the same sum over that material, precomputed below in the same
323
- // order; other sites run the same sum from the band. Either way each site's value is the same sequence of operations as
324
- // kernelSite, so it is bit-identical. The scene lookup count stays the count kernelSite's lazy window cache makes (each
325
- // cache point a computed site reads counts once per window, points outside the cache once per read): the rectangles of
326
- // the sites computed in a window are counted when the window closes (windowLookups).
327
- const allNeutral = spectral.length === 0;
328
- const bands = groups.map(() => ({ x0: 0, y0: 0, rows: 0, capacity: 0, filled: 0, material: new Int32Array(0), reflectance: new Float64Array(0),
364
+ const bands = groups.map(() => ({ x0: 0, y0: 0, rows: 0, capacity: 0, filled: 0, material: new Int32Array(0),
329
365
  changeLeft: new Int32Array(0), changeUp: new Int32Array(0), v: new Float64Array(0) }));
330
366
  // Lattice rows of material indices per group, kept across windows (filled windows only): rows LATTICE_RING_ROWS apart share a
331
367
  // slot; columns from −LATTICE_RING_MARGIN. Int16 when every index fits (about 3 MB for four groups of a 6000-site row). A
@@ -356,10 +392,8 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
356
392
  band.rows = rows;
357
393
  band.capacity = capacity;
358
394
  band.filled = 0;
359
- if (band.material.length < size) {
395
+ if (band.material.length < size)
360
396
  band.material = new Int32Array(size);
361
- band.reflectance = new Float64Array(allNeutral ? size : 0);
362
- }
363
397
  if (band.changeLeft.length < tables) {
364
398
  band.changeLeft = new Int32Array(tables);
365
399
  band.changeUp = new Int32Array(tables);
@@ -399,11 +433,9 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
399
433
  }
400
434
  else
401
435
  m = materials.materialAt(u, band.v[j]);
402
- if (spectralIndex[m] === undefined)
436
+ if (!spectra[m])
403
437
  throw new InvalidConfigurationError("Unknown chart material.");
404
438
  material[j * stride + i] = m;
405
- if (allNeutral)
406
- band.reflectance[j * stride + i] = reflectance[m];
407
439
  const changedLeft = i > 0 && material[j * stride + i - 1] !== m ? 1 : 0, changedUp = j > 0 && material[(j - 1) * stride + i] !== m ? 1 : 0;
408
440
  left[(j + 1) * S1 + i + 1] = left[j * S1 + i + 1] + left[(j + 1) * S1 + i] - left[j * S1 + i] + changedLeft;
409
441
  up[(j + 1) * S1 + i + 1] = up[j * S1 + i + 1] + up[(j + 1) * S1 + i] - up[j * S1 + i] + changedUp;
@@ -437,49 +469,25 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
437
469
  }
438
470
  if (m >= 0) {
439
471
  lastMaterial = m;
440
- return uniformSite[c][m];
472
+ acc.reset();
473
+ acc.add(m, kernelSums[c], 0);
474
+ return acc.value();
441
475
  }
442
476
  }
443
477
  lastMaterial = -1;
444
- let sum = 0;
478
+ acc.reset();
445
479
  for (let g = 0; g < groups.length; g++) {
446
- const group = groups[g], band = bands[g], cw = group.cw, ch = group.ch, nk = group.neutral[c], sk = group.spectral[c], stride = band.capacity;
480
+ const group = groups[g], band = bands[g], cw = group.cw, ch = group.ch, k = group.nodeKernels[c], stride = band.capacity;
447
481
  const p0 = x + group.minIx - R, q0 = y + group.minIy - R, bi = p0 - band.x0, bj = q0 - band.y0;
448
- if (inside && allNeutral) {
449
- const refl = band.reflectance;
450
- for (let dy = 0, t = 0; dy < ch; dy++) {
451
- const row = (bj + dy) * stride + bi;
452
- for (let dx = 0; dx < cw; dx++, t++)
453
- sum += refl[row + dx] * nk[t];
482
+ // A site inside the window reads every tap from the band; one outside it looks up the taps beyond the window cache.
483
+ for (let dy = 0, t = 0; dy < ch; dy++)
484
+ for (let dx = 0; dx < cw; dx++, t++) {
485
+ const ci = i + dx, cj = j + dy;
486
+ acc.add(inside || (ci >= 0 && cj >= 0 && ci < group.cpw && cj < group.cph) ? band.material[(bj + dy) * stride + bi + dx] :
487
+ scene.materialAt(latticeU(group, p0 + dx), latticeV(group, q0 + dy)), k, t * nodes);
454
488
  }
455
- }
456
- else if (inside) {
457
- const material = band.material;
458
- for (let dy = 0, t = 0; dy < ch; dy++) {
459
- const row = (bj + dy) * stride + bi;
460
- for (let dx = 0; dx < cw; dx++, t++) {
461
- const m = material[row + dx], si = spectralIndex[m];
462
- sum += si < 0 ? reflectance[m] * nk[t] : sk[si][t];
463
- }
464
- }
465
- }
466
- else {
467
- for (let dy = 0; dy < ch; dy++)
468
- for (let dx = 0; dx < cw; dx++) {
469
- const ci = i + dx, cj = j + dy;
470
- let m;
471
- if (ci >= 0 && cj >= 0 && ci < group.cpw && cj < group.cph)
472
- m = band.material[(bj + dy) * stride + bi + dx];
473
- else
474
- m = scene.materialAt(latticeU(group, p0 + dx), latticeV(group, q0 + dy));
475
- const si = spectralIndex[m];
476
- if (si === undefined)
477
- throw new InvalidConfigurationError("Unknown chart material.");
478
- sum += si < 0 ? reflectance[m] * nk[dy * cw + dx] : sk[si][dy * cw + dx];
479
- }
480
- }
481
489
  }
482
- return sum;
490
+ return acc.value();
483
491
  }
484
492
  /** Lattice points of an outside site's rectangles that lie outside the window caches (kernelSite looks these up on every read). */
485
493
  function outsideLookups(i, j) {
@@ -519,7 +527,7 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
519
527
  // depend on the window). A ring hit counts the lookups directSite would have made, so `lookups` is unchanged. The site's
520
528
  // material (directSite's lastMaterial) is kept beside its value, so the display view's one-material memo applies (#444).
521
529
  let directRows = 0, directOn = false, directRing = new Float64Array(0), directRowId = new Int32Array(0), directRingMaterial = new Int32Array(0);
522
- const directLookups = offsets.length * (scene.kind === "materials" ? 1 : nodes);
530
+ const directLookups = offsets.length * (scene.kind === "materials" || scene.signalAt ? 1 : nodes);
523
531
  function directRead(x, y) {
524
532
  const slot = y & (directRows - 1), q = slot * W + x;
525
533
  if (directRowId[slot] === y) {
@@ -917,7 +925,7 @@ function createLargeKernelSignal(input) {
917
925
  }
918
926
  function prepareLargeKernel(input) {
919
927
  const W = input.nativeWidth, H = input.nativeHeight, g = 2 / input.outputWidth, nodes = input.wavelengthsNm.length, R = input.kernels.radius;
920
- const tables = displayViewSignalTables(input), { reflectance, spectralIndex, groups } = tables, scene = input.scene;
928
+ const tables = displayViewSignalTables(input), { spectra, groups } = tables, scene = input.scene, acc = createMaterialSums(nodes, spectra);
921
929
  const state = { lookups: 0 };
922
930
  const latticeU = (group, p) => (group.fx === 0 ? (p + 0.5 - W / 2) * g : (p + 0.5 + group.fx - W / 2) * g);
923
931
  const latticeV = (group, q) => (group.fy === 0 ? -(q + 0.5 - H / 2) * g : -(q + 0.5 + group.fy - H / 2) * g);
@@ -935,18 +943,15 @@ function prepareLargeKernel(input) {
935
943
  }
936
944
  state.lookups += gw * gh;
937
945
  for (let c = 0; c < 3; c++) {
938
- const nk = group.neutral[c], sk = group.spectral[c], e = out[c];
946
+ const k = group.nodeKernels[c], e = out[c];
939
947
  for (let y = 0; y < T; y++)
940
948
  for (let x = 0; x < T; x++) {
941
- let sum = 0;
949
+ // Each group's part of the site, integrated from its taps' material spectra (MaterialSums).
950
+ acc.reset();
942
951
  for (let dy = 0; dy < ch; dy++)
943
- for (let dx = 0; dx < cw; dx++) {
944
- const m = material[(y + dy) * gw + x + dx], si = spectralIndex[m];
945
- if (si === undefined)
946
- throw new InvalidConfigurationError("Unknown chart material.");
947
- sum += si < 0 ? reflectance[m] * nk[dy * cw + dx] : sk[si][dy * cw + dx];
948
- }
949
- e[y * T + x] = e[y * T + x] + sum;
952
+ for (let dx = 0; dx < cw; dx++)
953
+ acc.add(material[(y + dy) * gw + x + dx], k, (dy * cw + dx) * nodes);
954
+ e[y * T + x] = e[y * T + x] + acc.value();
950
955
  }
951
956
  }
952
957
  }