@photivra/engine 2.8.0 → 2.9.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.
- package/CHANGELOG.md +92 -5
- package/CITATION.cff +1 -1
- package/README.md +8 -6
- package/SECURITY.md +1 -1
- package/dist/capture/export-jpeg.d.ts +19 -0
- package/dist/capture/export-jpeg.js +190 -95
- package/dist/capture/export-jpeg.js.map +1 -1
- package/dist/capture/native-environment-lean.js +19 -9
- package/dist/capture/native-environment-lean.js.map +1 -1
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/core/version.js.map +1 -1
- package/dist/facade/diffraction-provider.js +3 -10
- package/dist/facade/diffraction-provider.js.map +1 -1
- package/dist/facade/expected-signal-setup.d.ts +1 -1
- package/dist/facade/expected-signal-setup.js +2 -6
- package/dist/facade/expected-signal-setup.js.map +1 -1
- package/dist/facade/expected-signal.d.ts +4 -1
- package/dist/facade/expected-signal.js +151 -31
- package/dist/facade/expected-signal.js.map +1 -1
- package/dist/facade/scene-radiance.d.ts +4 -1
- package/dist/facade/scene-radiance.js +4 -4
- package/dist/facade/scene-radiance.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +26 -1
- package/dist/facade/simulate-capture.js +151 -26
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.js +2 -1
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/optics/diffraction-psf-profile.d.ts +19 -17
- package/dist/optics/diffraction-psf-profile.js +49 -18
- package/dist/optics/diffraction-psf-profile.js.map +1 -1
- package/dist/optics/site-diffraction-kernel.d.ts +5 -0
- package/dist/optics/site-diffraction-kernel.js +32 -0
- package/dist/optics/site-diffraction-kernel.js.map +1 -0
- package/dist/output/print-jpeg.d.ts +28 -1
- package/dist/output/print-jpeg.js +148 -48
- package/dist/output/print-jpeg.js.map +1 -1
- package/dist/sensor/stochastic-seed.d.ts +12 -0
- package/dist/sensor/stochastic-seed.js +25 -0
- package/dist/sensor/stochastic-seed.js.map +1 -1
- package/docs/API_REFERENCE.md +8 -1
- package/docs/QUICK_START.md +59 -0
- package/docs/api/capture-export-jpeg.md +10 -0
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +37 -2
- package/docs/api/facade-scene-radiance.md +2 -1
- package/docs/api/facade-simulate-capture.md +28 -0
- package/docs/api/optics-diffraction-psf-profile.md +1 -1
- package/docs/api/output-print-jpeg.md +41 -0
- package/package.json +1 -1
package/dist/core/version.d.ts
CHANGED
package/dist/core/version.js
CHANGED
package/dist/core/version.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version.js","sourceRoot":"","sources":["../../src/core/version.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,OAAgB,CAAC","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Version of the engine release. Since 1.0.1 it equals the npm package version.\n *\n * New captures and plans record this version as their creator. Archived records keep the\n * version they were made with. Proof-of-concept (POC), schema and model contracts have their\n * own versions. Release checks confirm it matches package.json and the packed package.\n * @see docs/RELEASE_1_0_1.md\n */\nexport const ENGINE_API_VERSION = \"2.
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../../src/core/version.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,OAAgB,CAAC","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Version of the engine release. Since 1.0.1 it equals the npm package version.\n *\n * New captures and plans record this version as their creator. Archived records keep the\n * version they were made with. Proof-of-concept (POC), schema and model contracts have their\n * own versions. Release checks confirm it matches package.json and the packed package.\n * @see docs/RELEASE_1_0_1.md\n */\nexport const ENGINE_API_VERSION = \"2.9.0\" as const;\n"]}
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
43
43
|
import { isInternalSceneRadianceEvaluator, markInternalSceneRadianceEvaluator, registerInternalRadianceAccess } from "../core/internal-radiance-evaluator.js";
|
|
44
44
|
import { calculateIrradianceConvolution } from "../optics/irradiance-convolution.js";
|
|
45
|
-
import {
|
|
45
|
+
import { siteDiffractionKernel } from "../optics/site-diffraction-kernel.js";
|
|
46
46
|
/** Rows of native sites per convolution band, and the bands kept. Banding changes memory only, never values. */
|
|
47
47
|
const BAND_ROWS = 32;
|
|
48
48
|
const CACHED_BANDS = 4;
|
|
@@ -71,15 +71,8 @@ export function createDiffractedPlanarProvider(input) {
|
|
|
71
71
|
if (!(lx0 < lx1 && ly0 < ly1))
|
|
72
72
|
throw new InvalidConfigurationError("Diffraction window must overlap the native raster.");
|
|
73
73
|
const NW = lx1 - lx0;
|
|
74
|
-
// One kernel per wavelength, on the site grid, from the engine's own tap-to-site mapping (field point 0, 0).
|
|
75
|
-
const kernels = wavelengthsNm.map((wavelengthNm) =>
|
|
76
|
-
const k = resolveLensPsfConvolutionKernels({ schemaVersion: "0.1.0", psf: input.psf, spectralWeights: [{ wavelengthNm, weight: 1 }],
|
|
77
|
-
grid: { width: 1, height: 1, sitePitchMicrometers: { x: pitch, y: pitch }, opticalAxisSite: { x: 0.5, y: 0.5 } }, patchWidth: 1, patchHeight: 1,
|
|
78
|
-
kernelRadius: R }).value;
|
|
79
|
-
if (!k.exactSiteSampling || k.kernelRadius !== R)
|
|
80
|
-
throw new InvalidConfigurationError("Diffraction kernels must sample the site pitch exactly.");
|
|
81
|
-
return k.kernels;
|
|
82
|
-
});
|
|
74
|
+
// One kernel per wavelength, on the site grid, from the engine's own tap-to-site mapping (field point 0, 0); kept across calls.
|
|
75
|
+
const kernels = wavelengthsNm.map((wavelengthNm) => siteDiffractionKernel(input.psf, wavelengthNm, pitch, R, "Diffraction kernels must sample the site pitch exactly."));
|
|
83
76
|
const gridWidth = NW + 2 * R;
|
|
84
77
|
const now = () => globalThis.performance?.now() ?? Date.now();
|
|
85
78
|
const bands = new Map();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"diffraction-provider.js","sourceRoot":"","sources":["../../src/facade/diffraction-provider.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Fast execution of `simulateCapture`'s `lens.diffraction` (2.1): an object-space convolution in the scene\n * provider, instead of one scene query per PSF tap in the environment PSF quadrature.\n *\n * The environment quadrature computes, at each site centre q and spectral node λ,\n * E(q, λ) = Σ_k K_λ(k)·T·L(s_k(q), λ),\n * where s_k(q) is the tap's source point (`sensorPsfSourcePoint`), L the scene radiance at the plane point\n * that source point sees, and T the radiance-to-irradiance factor. For the facade's planar scene seen\n * through the ideal rectilinear projection with a still camera and no lens field mapping, field throughput or\n * pupil rays:\n * - T is the same at every sensor point (paraxial π/(4·N_w²), no cos⁴ term), and\n * - with the kernel sampled on the site pitch, every source point is the centre of another site, so the plane\n * points form one regular lattice: u = (i + ½ − W/2)·2/w, v = −(j + ½ − H/2)·2/w (frame units, native\n * site (i, j), W×H native sites, w output width).\n * So E(q, λ) = T·B(q, λ) with B = K_λ ⊛ L sampled on that lattice: the same sum, in a different floating-point\n * order. B is computed once per spectral node with the engine's isoplanatic convolution\n * (`calculateIrradianceConvolution`, kernels from `resolveLensPsfConvolutionKernels`, exact site sampling) over a\n * grid that holds every source (the native raster plus the kernel radius on each side), in row bands. The\n * provider then answers each site-centre query with B instead of L, and the environment request declares no\n * PSF. Incoherent imaging is a linear superposition of intensity PSFs, so convolving radiance on the object\n * plane and convolving irradiance on the image plane agree for a planar object and a space-invariant PSF\n * (MIT OpenCourseWare 2.71, lecture 22, free; also see: Goodman, Introduction to Fourier Optics, 4th ed., §7.1).\n *\n * With an anti-aliasing point-splitting filter (2.1, `sensor.antiAliasing`) the environment queries the PSF taps\n * around each pre-filter source point q − a (a = a split offset) instead of around q. Those taps lie on the site\n * lattice shifted by a, so the same argument holds on each shifted lattice: B is computed once per distinct\n * fractional shift (offsets that differ by whole sites share one lattice; the centred four-spot filter with a\n * one-site separation needs a single half-site-shifted lattice) over the native raster widened by the largest\n * offset, and the environment's spatial quadrature still forms the weighted split sum.\n *\n * Region renders (2.6, `window`): only the lattice rows and columns that the region's sites can query (the window\n * plus the largest offset on each side) are convolved, over a grid that adds the kernel radius on each side. Every\n * output value is the same gather as in the whole render: the grid always holds every tap of every value it keeps\n * (real scene radiance out to the kernel radius, never a truncated edge), the single kernel patch does not depend on\n * the grid's origin, and `convolveIrradianceRows` sums each value in a fixed tap order (v outer, u inner). Narrowing\n * the grid therefore removes only values that no query reads, and the kept values are bit-identical.\n *\n * A query that does not land on one of the lattices (within 1e-6 site) is refused rather than approximated: the\n * facade uses this path only when the lattice argument above holds. A query outside the window is refused too.\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { isInternalSceneRadianceEvaluator, markInternalSceneRadianceEvaluator, registerInternalRadianceAccess } from \"../core/internal-radiance-evaluator.js\";\nimport type { SceneRadianceEvaluationRequest, SceneRadianceEvaluationResult } from \"../schema/scene-radiance.js\";\nimport { calculateIrradianceConvolution } from \"../optics/irradiance-convolution.js\";\nimport { resolveLensPsfConvolutionKernels } from \"../optics/lens-psf-kernels.js\";\nimport type { ResolveLensSampledPsfInput } from \"../optics/lens-psf-profile.js\";\nimport type { PlanarSceneRadianceProvider } from \"./scene-radiance.js\";\n\n/** Rows of native sites per convolution band, and the bands kept. Banding changes memory only, never values. */\nconst BAND_ROWS = 32;\nconst CACHED_BANDS = 4;\nconst LATTICE_TOLERANCE_SITES = 1e-6;\n\nexport interface DiffractedPlanarProviderInput {\n base: PlanarSceneRadianceProvider;\n /** The optical state of the sampled PSF, as for `resolveLensSampledPsf`, without field point and wavelength. */\n psf: Omit<ResolveLensSampledPsfInput, \"fieldPointMm\" | \"wavelengthNm\">;\n /** Spectral nodes the environment queries (the profile's wavelength nodes). */\n wavelengthsNm: readonly number[];\n kernelRadius: number;\n nativeWidth: number;\n nativeHeight: number;\n /** Output (active) width in sites: frame units are 2/outputWidth per site. */\n outputWidth: number;\n sitePitchMicrometers: number;\n /** Planar geometry of the base provider, to map query directions to plane points exactly as it does. */\n distanceM: number;\n metresPerFrameUnit: number;\n /**\n * Offsets of the query points from the site centres, in sites (+x right, +y down): the anti-aliasing split\n * offsets with their sign reversed (the environment queries q − a). Default: one zero offset.\n */\n queryOffsetsSites?: readonly { x: number; y: number }[];\n /**\n * Region renders (2.6): the native sites that will be queried (x, y, width, height in the native raster). Only the\n * lattice they can reach is convolved; values are bit-identical to the whole render's. Default: the whole raster.\n */\n window?: { x: number; y: number; width: number; height: number };\n /** Called with the duration in ms of each band convolution (diagnostics only; never changes values). */\n onConvolution?: (milliseconds: number) => void;\n}\n\n/** Internal (not exported from the package): the base provider with diffraction applied as an object-space convolution. */\nexport function createDiffractedPlanarProvider(input: DiffractedPlanarProviderInput): PlanarSceneRadianceProvider {\n const { base, wavelengthsNm, nativeWidth: W, nativeHeight: H, kernelRadius: R } = input;\n const g = 2 / input.outputWidth, pitch = input.sitePitchMicrometers;\n const index = new Map(wavelengthsNm.map((nm, i) => [nm, i]));\n // One lattice per distinct fractional query offset; M extra sites per side hold the offset queries.\n const offsets = input.queryOffsetsSites ?? [{ x: 0, y: 0 }];\n const fraction = (value: number): number => { const f = value - Math.floor(value); return f > 1 - LATTICE_TOLERANCE_SITES ? 0 : f < LATTICE_TOLERANCE_SITES ? 0 : f; };\n const shifts: { x: number; y: number }[] = [];\n for (const o of offsets) {\n const f = { x: fraction(o.x), y: fraction(o.y) };\n if (!shifts.some((s) => Math.abs(s.x - f.x) <= LATTICE_TOLERANCE_SITES && Math.abs(s.y - f.y) <= LATTICE_TOLERANCE_SITES)) shifts.push(f);\n }\n const M = Math.max(0, ...offsets.map((o) => Math.ceil(Math.max(Math.abs(o.x), Math.abs(o.y)) - LATTICE_TOLERANCE_SITES)));\n const LW = W + 2 * M, LH = H + 2 * M;\n // Lattice window (indices i + M, j + M): the sites the region queries plus the largest offset on each side, clipped\n // to the lattice. A site i queries i + floor(offset), and floor(offset) lies in [-M, M].\n const win = input.window ?? { x: 0, y: 0, width: W, height: H };\n const lx0 = Math.max(0, win.x), lx1 = Math.min(LW, win.x + win.width + 2 * M);\n const ly0 = Math.max(0, win.y), ly1 = Math.min(LH, win.y + win.height + 2 * M);\n if (!(lx0 < lx1 && ly0 < ly1)) throw new InvalidConfigurationError(\"Diffraction window must overlap the native raster.\");\n const NW = lx1 - lx0;\n // One kernel per wavelength, on the site grid, from the engine's own tap-to-site mapping (field point 0, 0).\n const kernels = wavelengthsNm.map((wavelengthNm) => {\n const k = resolveLensPsfConvolutionKernels({ schemaVersion: \"0.1.0\", psf: input.psf, spectralWeights: [{ wavelengthNm, weight: 1 }],\n grid: { width: 1, height: 1, sitePitchMicrometers: { x: pitch, y: pitch }, opticalAxisSite: { x: 0.5, y: 0.5 } }, patchWidth: 1, patchHeight: 1,\n kernelRadius: R }).value;\n if (!k.exactSiteSampling || k.kernelRadius !== R) throw new InvalidConfigurationError(\"Diffraction kernels must sample the site pitch exactly.\");\n return k.kernels;\n });\n const gridWidth = NW + 2 * R;\n const now = (): number => globalThis.performance?.now() ?? Date.now();\n const bands = new Map<number, Float64Array[][]>();\n function band(b: number): Float64Array[][] {\n const cached = bands.get(b);\n if (cached) { bands.delete(b); bands.set(b, cached); return cached; }\n const started = input.onConvolution ? now() : 0;\n const y0 = ly0 + b * BAND_ROWS, rows = Math.min(BAND_ROWS, ly1 - y0), gridHeight = rows + 2 * R;\n const values = shifts.map((shift) => wavelengthsNm.map((nm, w) => {\n const samples = new Float64Array(gridWidth * gridHeight);\n for (let gy = 0; gy < gridHeight; gy++) {\n const v = shift.y === 0 ? -(y0 + gy - R - M + 0.5 - H / 2) * g : -(y0 + gy - R - M + 0.5 + shift.y - H / 2) * g;\n for (let gx = 0, x = lx0; gx < gridWidth; gx++, x++) samples[gy * gridWidth + gx] = base.radianceAt(shift.x === 0 ? (x - R - M + 0.5 - W / 2) * g : (x - R - M + 0.5 + shift.x - W / 2) * g, v, nm);\n }\n const blurred = calculateIrradianceConvolution({ schemaVersion: \"0.1.0\", width: gridWidth, height: gridHeight, samples, kernelRadius: R,\n patchWidth: gridWidth, patchHeight: gridHeight, kernels: kernels[w]!, boundary: \"zero-outside-grid\" }).values;\n const out = new Float64Array(NW * rows);\n for (let y = 0; y < rows; y++) out.set(blurred.subarray((y + R) * gridWidth + R, (y + R) * gridWidth + R + NW), y * NW);\n return out;\n }));\n bands.set(b, values);\n if (bands.size > CACHED_BANDS) bands.delete(bands.keys().next().value!);\n if (input.onConvolution) input.onConvolution(now() - started);\n return values;\n }\n function blurredAt(u: number, v: number, nm: number): number {\n const w = index.get(nm), fi = u / g - 0.5 + W / 2, fj = -v / g - 0.5 + H / 2;\n if (w !== undefined) for (let s = 0; s < shifts.length; s++) {\n const shift = shifts[s]!, i = Math.round(fi - shift.x), j = Math.round(fj - shift.y);\n if (Math.abs(fi - shift.x - i) > LATTICE_TOLERANCE_SITES || Math.abs(fj - shift.y - j) > LATTICE_TOLERANCE_SITES) continue;\n const li = i + M, lj = j + M;\n if (li < lx0 || lj < ly0 || li >= lx1 || lj >= ly1) break;\n return band(Math.floor((lj - ly0) / BAND_ROWS))[s]![w]![((lj - ly0) % BAND_ROWS) * NW + li - lx0]!;\n }\n throw new InvalidConfigurationError(\"Diffraction query is off the site lattice; the object-space convolution does not apply.\");\n }\n const scale = input.metresPerFrameUnit, distance = input.distanceM, baseEvaluate = base.evaluateRadiance;\n const evaluateRadiance = (request: Readonly<SceneRadianceEvaluationRequest>): SceneRadianceEvaluationResult => {\n const result = baseEvaluate.call(base, request), o = (request.target as { outgoingDirectionUnitVector: { x: number; y: number; z: number } }).outgoingDirectionUnitVector;\n const t = distance / -o.z;\n return { ...result, spectralRadianceWattsPerSquareMeterSteradianNanometer: blurredAt(-o.x * t / scale, -o.y * t / scale, request.wavelengthNanometers) };\n };\n // 2.6: the result is the base result with only its radiance replaced (same keys and records), so an engine-built base\n // evaluator keeps its mark (see core/internal-radiance-evaluator.ts).\n if (isInternalSceneRadianceEvaluator(baseEvaluate)) {\n markInternalSceneRadianceEvaluator(evaluateRadiance);\n // #394: its radiance is the convolved value at one lattice site (shift s, lattice column li, row lj) per wavelength, so\n // the lean executor may read it by that key: the same plane point, lattice choice and band value as evaluateRadiance.\n registerInternalRadianceAccess(evaluateRadiance, {\n tabulated: false,\n keyOfDirection(o): number | undefined {\n if (!(-o.z > 0)) return undefined;\n const t = distance / -o.z, u = -o.x * t / scale, v = -o.y * t / scale, fi = u / g - 0.5 + W / 2, fj = -v / g - 0.5 + H / 2;\n for (let s = 0; s < shifts.length; s++) {\n const shift = shifts[s]!, i = Math.round(fi - shift.x), j = Math.round(fj - shift.y);\n if (Math.abs(fi - shift.x - i) > LATTICE_TOLERANCE_SITES || Math.abs(fj - shift.y - j) > LATTICE_TOLERANCE_SITES) continue;\n const li = i + M, lj = j + M;\n if (li < lx0 || lj < ly0 || li >= lx1 || lj >= ly1) return undefined;\n return (s * LH + lj) * LW + li;\n }\n return undefined;\n },\n radianceOfKey(key, nm): number {\n const w = index.get(nm);\n if (w === undefined) throw new InvalidConfigurationError(\"Diffraction query is off the site lattice; the object-space convolution does not apply.\");\n const li = key % LW, rest = (key - li) / LW, lj = rest % LH, s = (rest - lj) / LH;\n return band(Math.floor((lj - ly0) / BAND_ROWS))[s]![w]![((lj - ly0) % BAND_ROWS) * NW + li - lx0]!;\n }\n });\n }\n return {\n sceneId: base.sceneId, providerProfileId: base.providerProfileId, radianceAt: base.radianceAt,\n evaluateRadiance,\n evaluateApertureRadiance(): SceneRadianceEvaluationResult {\n throw new InvalidConfigurationError(\"Diffraction by object-space convolution does not take pupil rays.\");\n }\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"diffraction-provider.js","sourceRoot":"","sources":["../../src/facade/diffraction-provider.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Fast execution of `simulateCapture`'s `lens.diffraction` (2.1): an object-space convolution in the scene\n * provider, instead of one scene query per PSF tap in the environment PSF quadrature.\n *\n * The environment quadrature computes, at each site centre q and spectral node λ,\n * E(q, λ) = Σ_k K_λ(k)·T·L(s_k(q), λ),\n * where s_k(q) is the tap's source point (`sensorPsfSourcePoint`), L the scene radiance at the plane point\n * that source point sees, and T the radiance-to-irradiance factor. For the facade's planar scene seen\n * through the ideal rectilinear projection with a still camera and no lens field mapping, field throughput or\n * pupil rays:\n * - T is the same at every sensor point (paraxial π/(4·N_w²), no cos⁴ term), and\n * - with the kernel sampled on the site pitch, every source point is the centre of another site, so the plane\n * points form one regular lattice: u = (i + ½ − W/2)·2/w, v = −(j + ½ − H/2)·2/w (frame units, native\n * site (i, j), W×H native sites, w output width).\n * So E(q, λ) = T·B(q, λ) with B = K_λ ⊛ L sampled on that lattice: the same sum, in a different floating-point\n * order. B is computed once per spectral node with the engine's isoplanatic convolution\n * (`calculateIrradianceConvolution`, kernels from `resolveLensPsfConvolutionKernels`, exact site sampling) over a\n * grid that holds every source (the native raster plus the kernel radius on each side), in row bands. The\n * provider then answers each site-centre query with B instead of L, and the environment request declares no\n * PSF. Incoherent imaging is a linear superposition of intensity PSFs, so convolving radiance on the object\n * plane and convolving irradiance on the image plane agree for a planar object and a space-invariant PSF\n * (MIT OpenCourseWare 2.71, lecture 22, free; also see: Goodman, Introduction to Fourier Optics, 4th ed., §7.1).\n *\n * With an anti-aliasing point-splitting filter (2.1, `sensor.antiAliasing`) the environment queries the PSF taps\n * around each pre-filter source point q − a (a = a split offset) instead of around q. Those taps lie on the site\n * lattice shifted by a, so the same argument holds on each shifted lattice: B is computed once per distinct\n * fractional shift (offsets that differ by whole sites share one lattice; the centred four-spot filter with a\n * one-site separation needs a single half-site-shifted lattice) over the native raster widened by the largest\n * offset, and the environment's spatial quadrature still forms the weighted split sum.\n *\n * Region renders (2.6, `window`): only the lattice rows and columns that the region's sites can query (the window\n * plus the largest offset on each side) are convolved, over a grid that adds the kernel radius on each side. Every\n * output value is the same gather as in the whole render: the grid always holds every tap of every value it keeps\n * (real scene radiance out to the kernel radius, never a truncated edge), the single kernel patch does not depend on\n * the grid's origin, and `convolveIrradianceRows` sums each value in a fixed tap order (v outer, u inner). Narrowing\n * the grid therefore removes only values that no query reads, and the kept values are bit-identical.\n *\n * A query that does not land on one of the lattices (within 1e-6 site) is refused rather than approximated: the\n * facade uses this path only when the lattice argument above holds. A query outside the window is refused too.\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { isInternalSceneRadianceEvaluator, markInternalSceneRadianceEvaluator, registerInternalRadianceAccess } from \"../core/internal-radiance-evaluator.js\";\nimport type { SceneRadianceEvaluationRequest, SceneRadianceEvaluationResult } from \"../schema/scene-radiance.js\";\nimport { calculateIrradianceConvolution } from \"../optics/irradiance-convolution.js\";\nimport { siteDiffractionKernel } from \"../optics/site-diffraction-kernel.js\";\nimport type { ResolveLensSampledPsfInput } from \"../optics/lens-psf-profile.js\";\nimport type { PlanarSceneRadianceProvider } from \"./scene-radiance.js\";\n\n/** Rows of native sites per convolution band, and the bands kept. Banding changes memory only, never values. */\nconst BAND_ROWS = 32;\nconst CACHED_BANDS = 4;\nconst LATTICE_TOLERANCE_SITES = 1e-6;\n\nexport interface DiffractedPlanarProviderInput {\n base: PlanarSceneRadianceProvider;\n /** The optical state of the sampled PSF, as for `resolveLensSampledPsf`, without field point and wavelength. */\n psf: Omit<ResolveLensSampledPsfInput, \"fieldPointMm\" | \"wavelengthNm\">;\n /** Spectral nodes the environment queries (the profile's wavelength nodes). */\n wavelengthsNm: readonly number[];\n kernelRadius: number;\n nativeWidth: number;\n nativeHeight: number;\n /** Output (active) width in sites: frame units are 2/outputWidth per site. */\n outputWidth: number;\n sitePitchMicrometers: number;\n /** Planar geometry of the base provider, to map query directions to plane points exactly as it does. */\n distanceM: number;\n metresPerFrameUnit: number;\n /**\n * Offsets of the query points from the site centres, in sites (+x right, +y down): the anti-aliasing split\n * offsets with their sign reversed (the environment queries q − a). Default: one zero offset.\n */\n queryOffsetsSites?: readonly { x: number; y: number }[];\n /**\n * Region renders (2.6): the native sites that will be queried (x, y, width, height in the native raster). Only the\n * lattice they can reach is convolved; values are bit-identical to the whole render's. Default: the whole raster.\n */\n window?: { x: number; y: number; width: number; height: number };\n /** Called with the duration in ms of each band convolution (diagnostics only; never changes values). */\n onConvolution?: (milliseconds: number) => void;\n}\n\n/** Internal (not exported from the package): the base provider with diffraction applied as an object-space convolution. */\nexport function createDiffractedPlanarProvider(input: DiffractedPlanarProviderInput): PlanarSceneRadianceProvider {\n const { base, wavelengthsNm, nativeWidth: W, nativeHeight: H, kernelRadius: R } = input;\n const g = 2 / input.outputWidth, pitch = input.sitePitchMicrometers;\n const index = new Map(wavelengthsNm.map((nm, i) => [nm, i]));\n // One lattice per distinct fractional query offset; M extra sites per side hold the offset queries.\n const offsets = input.queryOffsetsSites ?? [{ x: 0, y: 0 }];\n const fraction = (value: number): number => { const f = value - Math.floor(value); return f > 1 - LATTICE_TOLERANCE_SITES ? 0 : f < LATTICE_TOLERANCE_SITES ? 0 : f; };\n const shifts: { x: number; y: number }[] = [];\n for (const o of offsets) {\n const f = { x: fraction(o.x), y: fraction(o.y) };\n if (!shifts.some((s) => Math.abs(s.x - f.x) <= LATTICE_TOLERANCE_SITES && Math.abs(s.y - f.y) <= LATTICE_TOLERANCE_SITES)) shifts.push(f);\n }\n const M = Math.max(0, ...offsets.map((o) => Math.ceil(Math.max(Math.abs(o.x), Math.abs(o.y)) - LATTICE_TOLERANCE_SITES)));\n const LW = W + 2 * M, LH = H + 2 * M;\n // Lattice window (indices i + M, j + M): the sites the region queries plus the largest offset on each side, clipped\n // to the lattice. A site i queries i + floor(offset), and floor(offset) lies in [-M, M].\n const win = input.window ?? { x: 0, y: 0, width: W, height: H };\n const lx0 = Math.max(0, win.x), lx1 = Math.min(LW, win.x + win.width + 2 * M);\n const ly0 = Math.max(0, win.y), ly1 = Math.min(LH, win.y + win.height + 2 * M);\n if (!(lx0 < lx1 && ly0 < ly1)) throw new InvalidConfigurationError(\"Diffraction window must overlap the native raster.\");\n const NW = lx1 - lx0;\n // One kernel per wavelength, on the site grid, from the engine's own tap-to-site mapping (field point 0, 0); kept across calls.\n const kernels = wavelengthsNm.map((wavelengthNm) => siteDiffractionKernel(input.psf, wavelengthNm, pitch, R, \"Diffraction kernels must sample the site pitch exactly.\"));\n const gridWidth = NW + 2 * R;\n const now = (): number => globalThis.performance?.now() ?? Date.now();\n const bands = new Map<number, Float64Array[][]>();\n function band(b: number): Float64Array[][] {\n const cached = bands.get(b);\n if (cached) { bands.delete(b); bands.set(b, cached); return cached; }\n const started = input.onConvolution ? now() : 0;\n const y0 = ly0 + b * BAND_ROWS, rows = Math.min(BAND_ROWS, ly1 - y0), gridHeight = rows + 2 * R;\n const values = shifts.map((shift) => wavelengthsNm.map((nm, w) => {\n const samples = new Float64Array(gridWidth * gridHeight);\n for (let gy = 0; gy < gridHeight; gy++) {\n const v = shift.y === 0 ? -(y0 + gy - R - M + 0.5 - H / 2) * g : -(y0 + gy - R - M + 0.5 + shift.y - H / 2) * g;\n for (let gx = 0, x = lx0; gx < gridWidth; gx++, x++) samples[gy * gridWidth + gx] = base.radianceAt(shift.x === 0 ? (x - R - M + 0.5 - W / 2) * g : (x - R - M + 0.5 + shift.x - W / 2) * g, v, nm);\n }\n const blurred = calculateIrradianceConvolution({ schemaVersion: \"0.1.0\", width: gridWidth, height: gridHeight, samples, kernelRadius: R,\n patchWidth: gridWidth, patchHeight: gridHeight, kernels: kernels[w]!, boundary: \"zero-outside-grid\" }).values;\n const out = new Float64Array(NW * rows);\n for (let y = 0; y < rows; y++) out.set(blurred.subarray((y + R) * gridWidth + R, (y + R) * gridWidth + R + NW), y * NW);\n return out;\n }));\n bands.set(b, values);\n if (bands.size > CACHED_BANDS) bands.delete(bands.keys().next().value!);\n if (input.onConvolution) input.onConvolution(now() - started);\n return values;\n }\n function blurredAt(u: number, v: number, nm: number): number {\n const w = index.get(nm), fi = u / g - 0.5 + W / 2, fj = -v / g - 0.5 + H / 2;\n if (w !== undefined) for (let s = 0; s < shifts.length; s++) {\n const shift = shifts[s]!, i = Math.round(fi - shift.x), j = Math.round(fj - shift.y);\n if (Math.abs(fi - shift.x - i) > LATTICE_TOLERANCE_SITES || Math.abs(fj - shift.y - j) > LATTICE_TOLERANCE_SITES) continue;\n const li = i + M, lj = j + M;\n if (li < lx0 || lj < ly0 || li >= lx1 || lj >= ly1) break;\n return band(Math.floor((lj - ly0) / BAND_ROWS))[s]![w]![((lj - ly0) % BAND_ROWS) * NW + li - lx0]!;\n }\n throw new InvalidConfigurationError(\"Diffraction query is off the site lattice; the object-space convolution does not apply.\");\n }\n const scale = input.metresPerFrameUnit, distance = input.distanceM, baseEvaluate = base.evaluateRadiance;\n const evaluateRadiance = (request: Readonly<SceneRadianceEvaluationRequest>): SceneRadianceEvaluationResult => {\n const result = baseEvaluate.call(base, request), o = (request.target as { outgoingDirectionUnitVector: { x: number; y: number; z: number } }).outgoingDirectionUnitVector;\n const t = distance / -o.z;\n return { ...result, spectralRadianceWattsPerSquareMeterSteradianNanometer: blurredAt(-o.x * t / scale, -o.y * t / scale, request.wavelengthNanometers) };\n };\n // 2.6: the result is the base result with only its radiance replaced (same keys and records), so an engine-built base\n // evaluator keeps its mark (see core/internal-radiance-evaluator.ts).\n if (isInternalSceneRadianceEvaluator(baseEvaluate)) {\n markInternalSceneRadianceEvaluator(evaluateRadiance);\n // #394: its radiance is the convolved value at one lattice site (shift s, lattice column li, row lj) per wavelength, so\n // the lean executor may read it by that key: the same plane point, lattice choice and band value as evaluateRadiance.\n registerInternalRadianceAccess(evaluateRadiance, {\n tabulated: false,\n keyOfDirection(o): number | undefined {\n if (!(-o.z > 0)) return undefined;\n const t = distance / -o.z, u = -o.x * t / scale, v = -o.y * t / scale, fi = u / g - 0.5 + W / 2, fj = -v / g - 0.5 + H / 2;\n for (let s = 0; s < shifts.length; s++) {\n const shift = shifts[s]!, i = Math.round(fi - shift.x), j = Math.round(fj - shift.y);\n if (Math.abs(fi - shift.x - i) > LATTICE_TOLERANCE_SITES || Math.abs(fj - shift.y - j) > LATTICE_TOLERANCE_SITES) continue;\n const li = i + M, lj = j + M;\n if (li < lx0 || lj < ly0 || li >= lx1 || lj >= ly1) return undefined;\n return (s * LH + lj) * LW + li;\n }\n return undefined;\n },\n radianceOfKey(key, nm): number {\n const w = index.get(nm);\n if (w === undefined) throw new InvalidConfigurationError(\"Diffraction query is off the site lattice; the object-space convolution does not apply.\");\n const li = key % LW, rest = (key - li) / LW, lj = rest % LH, s = (rest - lj) / LH;\n return band(Math.floor((lj - ly0) / BAND_ROWS))[s]![w]![((lj - ly0) % BAND_ROWS) * NW + li - lx0]!;\n }\n });\n }\n return {\n sceneId: base.sceneId, providerProfileId: base.providerProfileId, radianceAt: base.radianceAt,\n evaluateRadiance,\n evaluateApertureRadiance(): SceneRadianceEvaluationResult {\n throw new InvalidConfigurationError(\"Diffraction by object-space convolution does not take pupil rays.\");\n }\n };\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolveLensPsfConvolutionKernels } from "../optics/lens-psf-kernels.js";
|
|
1
|
+
import type { resolveLensPsfConvolutionKernels } from "../optics/lens-psf-kernels.js";
|
|
2
2
|
import type { DisplayViewExpectedSignalInput } from "./display-view.js";
|
|
3
3
|
type Psf = Parameters<typeof resolveLensPsfConvolutionKernels>[0]["psf"];
|
|
4
4
|
export interface ExpectedSignalSetupInput {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* the same way: query offsets of each site, the environment's query points, the diffraction kernels and the scene tables.
|
|
6
6
|
*/
|
|
7
7
|
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
8
|
-
import {
|
|
8
|
+
import { siteDiffractionKernel } from "../optics/site-diffraction-kernel.js";
|
|
9
9
|
import { resolveProjectionDistanceMm } from "../motion/camera-rotation.js";
|
|
10
10
|
/** The query offsets of each site and the expected-signal input (see the module comment). */
|
|
11
11
|
export function buildExpectedSignalInput(input) {
|
|
@@ -22,11 +22,7 @@ export function buildExpectedSignalInput(input) {
|
|
|
22
22
|
kernels = { radius, perNode: wavelengths.map((wavelengthNm) => {
|
|
23
23
|
if (!diffraction.profileWavelengths.includes(wavelengthNm))
|
|
24
24
|
throw new InvalidConfigurationError(input.caller + ": a spectral node has no diffraction kernel.");
|
|
25
|
-
|
|
26
|
-
grid: { width: 1, height: 1, sitePitchMicrometers: { x: pitch, y: pitch }, opticalAxisSite: { x: 0.5, y: 0.5 } }, patchWidth: 1, patchHeight: 1, kernelRadius: radius }).value;
|
|
27
|
-
if (!k.exactSiteSampling || k.kernelRadius !== radius)
|
|
28
|
-
throw new InvalidConfigurationError(input.caller + ": diffraction kernels must sample the site pitch exactly.");
|
|
29
|
-
return Float64Array.from(k.kernels);
|
|
25
|
+
return siteDiffractionKernel(diffraction.psf, wavelengthNm, pitch, radius, input.caller + ": diffraction kernels must sample the site pitch exactly.");
|
|
30
26
|
}) };
|
|
31
27
|
}
|
|
32
28
|
// Chart presets and (2.7) caller material scenes take the material path: each material tabulated once per node.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"expected-signal-setup.js","sourceRoot":"","sources":["../../src/facade/expected-signal-setup.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC;;;;GAIG;AAEH,OAAO,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAC3E,OAAO,EAAE,gCAAgC,EAAE,MAAM,+BAA+B,CAAC;AACjF,OAAO,EAAE,2BAA2B,EAAE,MAAM,8BAA8B,CAAC;AAgC3E,6FAA6F;AAC7F,MAAM,UAAU,wBAAwB,CAAC,KAA+B;IACtE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,GAAG,KAAK,CAAC;IACpG,gJAAgJ;IAChJ,MAAM,KAAK,GAAG,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,gBAAgB,EAAE,CAAC,EAAE,CAAC,CAAC;IAChI,MAAM,OAAO,GAAG,KAAK,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,iBAAiB,CAAC,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,iBAAiB,CAAC,CAAC,GAAG,KAAK;QAC5J,MAAM,EAAE,CAAC,CAAC,gBAAgB,GAAG,CAAC,UAAU,GAAG,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC9D,gHAAgH;IAChH,IAAI,OAAgE,CAAC;IACrE,MAAM,WAAW,GAAG,KAAK,CAAC,WAAW,CAAC;IACtC,IAAI,WAAW,EAAE,CAAC;QAChB,MAAM,MAAM,GAAG,WAAW,CAAC,MAAM,CAAC;QAClC,OAAO,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,CAAC,GAAG,CAAC,CAAC,YAAY,EAAE,EAAE;gBAC5D,IAAI,CAAC,WAAW,CAAC,kBAAkB,CAAC,QAAQ,CAAC,YAAY,CAAC;oBAAE,MAAM,IAAI,yBAAyB,CAAC,KAAK,CAAC,MAAM,GAAG,8CAA8C,CAAC,CAAC;gBAC/J,MAAM,CAAC,GAAG,gCAAgC,CAAC,EAAE,aAAa,EAAE,OAAO,EAAE,GAAG,EAAE,WAAW,CAAC,GAAG,EAAE,eAAe,EAAE,CAAC,EAAE,YAAY,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;oBACvI,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,oBAAoB,EAAE,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,eAAe,EAAE,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,UAAU,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,YAAY,EAAE,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC;gBACjL,IAAI,CAAC,CAAC,CAAC,iBAAiB,IAAI,CAAC,CAAC,YAAY,KAAK,MAAM;oBAAE,MAAM,IAAI,yBAAyB,CAAC,KAAK,CAAC,MAAM,GAAG,2DAA2D,CAAC,CAAC;gBACvK,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;YACtC,CAAC,CAAC,EAAE,CAAC;IACP,CAAC;IACD,gHAAgH;IAChH,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC;IAClC,MAAM,KAAK,GAAqB,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,CAAC,UAAU;QAC/F,SAAS,EAAE,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,YAAY,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;QACtJ,aAAa,EAAE,YAAY,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,SAAS,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;QACtF,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,CAAC;IACrD,MAAM,YAAY,GAAG,CAAC,CAAS,EAAE,CAAS,EAAU,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3H,0HAA0H;IAC1H,yGAAyG;IACzG,MAAM,kBAAkB,GAAG,2BAA2B,CAAC,KAAK,CAAC,aAAa,EAAE,KAAK,CAAC,cAAc,CAAC,CAAC,oBAAoB,EAAE,WAAW,GAAG,KAAK,GAAG,IAAI,CAAC;IACnJ,MAAM,UAAU,GAAG,KAAK,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,KAAK,GAAG,UAAU,GAAG,GAAG,CAAC,GAAG,UAAU,EAAE,MAAM,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,UAAU,CAAC,GAAG,GAAG,CAAC,GAAG,UAAU;QAC3L,GAAG,EAAE,CAAC,CAAC,iBAAiB,CAAC,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,iBAAiB,CAAC,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAC5E,MAAM,UAAU,GAAG,CAAC,CAAS,EAAE,CAAS,EAAE,CAAS,EAAE,GAAiB,EAAQ,EAAE;QAC9E,MAAM,IAAI,GAAG,UAAU,CAAC,CAAC,CAAE,CAAC;QAC5B,MAAM,OAAO,GAAG,CAAC,WAAW,CAAC,OAAO,GAAG,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,GAAG,CAAC,WAAW,CAAC,QAAQ,GAAG,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC;QACxJ,MAAM,OAAO,GAAG,CAAC,OAAO,GAAG,WAAW,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,GAAG,WAAW,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,OAAO,GAAG,CAAC,OAAO,GAAG,WAAW,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,GAAG,WAAW,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC;QACjK,MAAM,EAAE,GAAG,kBAAkB,GAAG,CAAC,OAAO,GAAG,kBAAkB,CAAC,EAAE,EAAE,GAAG,kBAAkB,GAAG,CAAC,CAAC,OAAO,GAAG,kBAAkB,CAAC,CAAC;QAC1H,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,EAAE,kBAAkB,CAAC,EAAE,CAAC,GAAG,gBAAgB,GAAG,CAAC,kBAAkB,GAAG,MAAM,CAAC,CAAC;QAC5G,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,MAAM,GAAG,CAAC,GAAG,kBAAkB,CAAC;QAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,MAAM,GAAG,CAAC,GAAG,kBAAkB,CAAC;IAC/F,CAAC,CAAC;IACF,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,YAAY,EAAE,KAAK,CAAC,YAAY,EAAE,WAAW,EAAE,KAAK,CAAC,KAAK,EAAE,aAAa,EAAE,WAAW;YACrJ,oBAAoB,EAAE,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,EAAE,CAAC;AAClI,CAAC","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\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { resolveLensPsfConvolutionKernels } from \"../optics/lens-psf-kernels.js\";\nimport { resolveProjectionDistanceMm } from \"../motion/camera-rotation.js\";\nimport type { DisplayViewExpectedSignalInput, DisplayViewScene } from \"./display-view.js\";\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 /** 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 const k = resolveLensPsfConvolutionKernels({ schemaVersion: \"0.1.0\", psf: diffraction.psf, spectralWeights: [{ wavelengthNm, weight: 1 }],\n grid: { width: 1, height: 1, sitePitchMicrometers: { x: pitch, y: pitch }, opticalAxisSite: { x: 0.5, y: 0.5 } }, patchWidth: 1, patchHeight: 1, kernelRadius: radius }).value;\n if (!k.exactSiteSampling || k.kernelRadius !== radius) throw new InvalidConfigurationError(input.caller + \": diffraction kernels must sample the site pitch exactly.\");\n return Float64Array.from(k.kernels);\n }) };\n }\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 = Math.hypot(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 }), scene, channelAt: channelIndex } };\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\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 { resolveProjectionDistanceMm } from \"../motion/camera-rotation.js\";\nimport type { DisplayViewExpectedSignalInput, DisplayViewScene } from \"./display-view.js\";\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 /** 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 // 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 = Math.hypot(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 }), scene, channelAt: channelIndex } };\n}\n"]}
|
|
@@ -79,5 +79,8 @@ export interface DisplayViewExpectedSignal {
|
|
|
79
79
|
* With diffraction, offsets that share a fractional shift read one lattice, and their kernels are combined in advance:
|
|
80
80
|
* C_c(d) = Σ_members w·Σ_λ A_c(λ)·K_λ(t)·L(λ) over the taps t that reach lattice offset d. A neutral material's radiance
|
|
81
81
|
* is its reflectance times the white radiance, so all neutral materials share one combined kernel per channel.
|
|
82
|
+
*
|
|
83
|
+
* `spanColumns`: how many native columns the render's windows move across beyond one window's width (a filled band is never
|
|
84
|
+
* built wider). Only a speed hint: a window past it rebuilds its band, with the same values.
|
|
82
85
|
*/
|
|
83
|
-
export declare function createDisplayViewExpectedSignal(input: DisplayViewExpectedSignalInput): DisplayViewExpectedSignal;
|
|
86
|
+
export declare function createDisplayViewExpectedSignal(input: DisplayViewExpectedSignalInput, spanColumns?: number): DisplayViewExpectedSignal;
|
|
@@ -11,23 +11,14 @@ const CACHE_LIMIT = 1 << 22;
|
|
|
11
11
|
/** Largest band of looked-up lattice materials per group, in lattice points (a band that would be larger restarts at the window). */
|
|
12
12
|
const BAND_LIMIT = 1 << 22;
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* facade's environment chain forms for a still, rectilinear, distortion-free planar scene, where the
|
|
17
|
-
* radiance-to-irradiance factor is the same at every site (docs/DISPLAY_VIEW.md, "Expected signal").
|
|
18
|
-
*
|
|
19
|
-
* With diffraction, offsets that share a fractional shift read one lattice, and their kernels are combined in advance:
|
|
20
|
-
* C_c(d) = Σ_members w·Σ_λ A_c(λ)·K_λ(t)·L(λ) over the taps t that reach lattice offset d. A neutral material's radiance
|
|
21
|
-
* is its reflectance times the white radiance, so all neutral materials share one combined kernel per channel.
|
|
14
|
+
* Lattice-row ring (refinement windows): rows kept per group, the tallest band that uses it, and the columns kept beyond each
|
|
15
|
+
* raster edge (kernel radius, demosaic reach and query offsets; columns beyond it are looked up directly).
|
|
22
16
|
*/
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (!input.kernels && !input.queryPoint)
|
|
29
|
-
throw new InvalidConfigurationError("Display view needs the environment query points without diffraction.");
|
|
30
|
-
const A = input.electronsPerRadiance, scene = input.scene, offsets = input.offsets, channelAt = input.channelAt;
|
|
17
|
+
const LATTICE_RING_ROWS = 128, LATTICE_RING_MAXIMUM_BAND_ROWS = 64, LATTICE_RING_MARGIN = 64;
|
|
18
|
+
const PREPARED = new WeakMap();
|
|
19
|
+
function prepareSignal(input) {
|
|
20
|
+
const nodes = input.wavelengthsNm.length, R = input.kernels ? input.kernels.radius : 0, size = 2 * R + 1;
|
|
21
|
+
const A = input.electronsPerRadiance, scene = input.scene, offsets = input.offsets;
|
|
31
22
|
const kernelAt = (node, t) => (input.kernels ? input.kernels.perNode[node][t] : 1);
|
|
32
23
|
// Materials: neutral ones by reflectance, spectral ones by index.
|
|
33
24
|
let reflectance = new Float64Array(0), spectralIndex = new Int32Array(0);
|
|
@@ -92,6 +83,46 @@ export function createDisplayViewExpectedSignal(input) {
|
|
|
92
83
|
s += A[c][n] * scene.whiteRadiance[n]; return s; }) : [];
|
|
93
84
|
const spectralDirect = [0, 1, 2].map((c) => spectral.map((radiance) => { let s = 0; for (let n = 0; n < nodes; n++)
|
|
94
85
|
s += A[c][n] * radiance[n]; return s; }));
|
|
86
|
+
const uniformSite = scene.kind === "materials" && input.kernels ? [0, 1, 2].map((c) => Float64Array.from(scene.materials, (_, m) => {
|
|
87
|
+
const si = spectralIndex[m];
|
|
88
|
+
let sum = 0;
|
|
89
|
+
for (const group of groups) {
|
|
90
|
+
const nk = group.neutral[c], sk = si < 0 ? nk : group.spectral[c][si];
|
|
91
|
+
for (let t = 0; t < group.cw * group.ch; t++)
|
|
92
|
+
sum += si < 0 ? reflectance[m] * nk[t] : sk[t];
|
|
93
|
+
}
|
|
94
|
+
return sum;
|
|
95
|
+
})) : [];
|
|
96
|
+
const prepared = { reflectance, spectralIndex, spectral, groups, neutralDirect, spectralDirect, uniformSite, rings: groups.map(() => undefined) };
|
|
97
|
+
PREPARED.set(input, prepared);
|
|
98
|
+
return prepared;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* 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
|
|
102
|
+
* query point of offset o and K the diffraction kernel (a single unit tap without diffraction). This is the sum the
|
|
103
|
+
* facade's environment chain forms for a still, rectilinear, distortion-free planar scene, where the
|
|
104
|
+
* radiance-to-irradiance factor is the same at every site (docs/DISPLAY_VIEW.md, "Expected signal").
|
|
105
|
+
*
|
|
106
|
+
* With diffraction, offsets that share a fractional shift read one lattice, and their kernels are combined in advance:
|
|
107
|
+
* C_c(d) = Σ_members w·Σ_λ A_c(λ)·K_λ(t)·L(λ) over the taps t that reach lattice offset d. A neutral material's radiance
|
|
108
|
+
* is its reflectance times the white radiance, so all neutral materials share one combined kernel per channel.
|
|
109
|
+
*
|
|
110
|
+
* `spanColumns`: how many native columns the render's windows move across beyond one window's width (a filled band is never
|
|
111
|
+
* built wider). Only a speed hint: a window past it rebuilds its band, with the same values.
|
|
112
|
+
*/
|
|
113
|
+
export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
|
|
114
|
+
const W = input.nativeWidth, H = input.nativeHeight, g = 2 / input.outputWidth, nodes = input.wavelengthsNm.length;
|
|
115
|
+
const R = input.kernels ? input.kernels.radius : 0;
|
|
116
|
+
if (input.kernels && input.kernels.perNode.length !== nodes)
|
|
117
|
+
throw new InvalidConfigurationError("Display view kernels must match the spectral nodes.");
|
|
118
|
+
if (!input.kernels && !input.queryPoint)
|
|
119
|
+
throw new InvalidConfigurationError("Display view needs the environment query points without diffraction.");
|
|
120
|
+
const A = input.electronsPerRadiance, scene = input.scene, offsets = input.offsets, channelAt = input.channelAt;
|
|
121
|
+
// The request's tables (materials, lattice groups, combined kernels, per-material sums) are the same for every render of a
|
|
122
|
+
// prepared view, which passes the same input object: computed once per input. Each render gets its own groups' window caches.
|
|
123
|
+
const prepared = PREPARED.get(input) ?? prepareSignal(input);
|
|
124
|
+
const { reflectance, spectralIndex, spectral, neutralDirect, spectralDirect, uniformSite } = prepared;
|
|
125
|
+
const groups = prepared.groups.map((group) => ({ ...group, cache: new Int32Array(0), cx0: 0, cy0: 0, cpw: 0, cph: 0 }));
|
|
95
126
|
let lookups = 0;
|
|
96
127
|
const point = new Float64Array(2);
|
|
97
128
|
const materialSignalDirect = (m, c) => {
|
|
@@ -190,24 +221,31 @@ export function createDisplayViewExpectedSignal(input) {
|
|
|
190
221
|
// kernelSite, so it is bit-identical. The scene lookup count stays the count kernelSite's lazy window cache makes (each
|
|
191
222
|
// cache point a computed site reads counts once per window, points outside the cache once per read): the rectangles of
|
|
192
223
|
// the sites computed in a window are counted when the window closes (windowLookups).
|
|
193
|
-
const uniformSite = scene.kind === "materials" && input.kernels ? [0, 1, 2].map((c) => Float64Array.from(scene.materials, (_, m) => {
|
|
194
|
-
const si = spectralIndex[m];
|
|
195
|
-
let sum = 0;
|
|
196
|
-
for (const group of groups) {
|
|
197
|
-
const nk = group.neutral[c], sk = si < 0 ? nk : group.spectral[c][si];
|
|
198
|
-
for (let t = 0; t < group.cw * group.ch; t++)
|
|
199
|
-
sum += si < 0 ? reflectance[m] * nk[t] : sk[t];
|
|
200
|
-
}
|
|
201
|
-
return sum;
|
|
202
|
-
})) : [];
|
|
203
224
|
const allNeutral = spectral.length === 0;
|
|
204
225
|
const bands = groups.map(() => ({ x0: 0, y0: 0, rows: 0, capacity: 0, filled: 0, material: new Int32Array(0), reflectance: new Float64Array(0),
|
|
205
226
|
changeLeft: new Int32Array(0), changeUp: new Int32Array(0), v: new Float64Array(0) }));
|
|
227
|
+
// Lattice rows of material indices per group, kept across windows (filled windows only): rows LATTICE_RING_ROWS apart share a
|
|
228
|
+
// slot; columns from −LATTICE_RING_MARGIN. Int16 when every index fits (about 3 MB for four groups of a 6000-site row). A
|
|
229
|
+
// lattice point's material depends only on the input, so the rings are kept with its prepared tables, across renders.
|
|
230
|
+
const ringWide = scene.kind === "materials" && scene.materials.length <= 32767;
|
|
231
|
+
const rings = prepared.rings;
|
|
232
|
+
function latticeRing(g) {
|
|
233
|
+
let ring = rings[g];
|
|
234
|
+
if (!ring) {
|
|
235
|
+
const width = W + 2 * LATTICE_RING_MARGIN, size = LATTICE_RING_ROWS * width;
|
|
236
|
+
ring = rings[g] = { width, material: ringWide ? new Int16Array(size) : new Int32Array(size), rowId: new Int32Array(LATTICE_RING_ROWS).fill(-2147483648) };
|
|
237
|
+
}
|
|
238
|
+
return ring;
|
|
239
|
+
}
|
|
206
240
|
/** Makes the band of group g cover lattice columns [x0, x1) of rows [y0, y0 + rows), extending it to the right when it can. */
|
|
207
241
|
function coverBand(g, x0, x1, y0, rows) {
|
|
208
242
|
const group = groups[g], band = bands[g], materials = scene;
|
|
209
|
-
if (!(band.
|
|
210
|
-
|
|
243
|
+
if (!(y0 >= band.y0 && y0 + rows <= band.y0 + band.rows && x0 >= band.x0 && x1 - band.x0 <= band.capacity)) {
|
|
244
|
+
// A band with the ring is built twice as tall, so the next display rows' windows (a few lattice rows lower) reuse its
|
|
245
|
+
// filled columns; a lattice point's material and the change counts inside a rectangle do not depend on the band's origin.
|
|
246
|
+
if (rows <= LATTICE_RING_MAXIMUM_BAND_ROWS)
|
|
247
|
+
rows *= 2;
|
|
248
|
+
const capacity = Math.max(x1 - x0, Math.min(Math.floor(BAND_LIMIT / rows), W + 2 * (x1 - x0), spanColumns + x1 - x0));
|
|
211
249
|
const size = rows * capacity, tables = (rows + 1) * (capacity + 1);
|
|
212
250
|
band.x0 = x0;
|
|
213
251
|
band.y0 = y0;
|
|
@@ -234,10 +272,29 @@ export function createDisplayViewExpectedSignal(input) {
|
|
|
234
272
|
band.v[j] = latticeV(group, y0 + j);
|
|
235
273
|
}
|
|
236
274
|
const stride = band.capacity, S1 = stride + 1, material = band.material, left = band.changeLeft, up = band.changeUp;
|
|
275
|
+
rows = band.rows;
|
|
276
|
+
y0 = band.y0;
|
|
277
|
+
const ring = rows <= 2 * LATTICE_RING_MAXIMUM_BAND_ROWS ? latticeRing(g) : undefined;
|
|
237
278
|
for (let i = band.filled; i < x1 - band.x0; i++) {
|
|
238
|
-
const u = latticeU(group,
|
|
279
|
+
const p = band.x0 + i, u = latticeU(group, p), column = p + LATTICE_RING_MARGIN;
|
|
239
280
|
for (let j = 0; j < rows; j++) {
|
|
240
|
-
|
|
281
|
+
let m;
|
|
282
|
+
if (ring && column >= 0 && column < ring.width) {
|
|
283
|
+
// A lattice point's material is a function of the point alone, so one looked up for an earlier window (the rows a
|
|
284
|
+
// display row shares with the next) is the same material; the ring only spares the repeated lookup.
|
|
285
|
+
const q = y0 + j, slot = q & (LATTICE_RING_ROWS - 1), index = slot * ring.width + column;
|
|
286
|
+
if (ring.rowId[slot] !== q) {
|
|
287
|
+
ring.rowId[slot] = q;
|
|
288
|
+
ring.material.fill(-1, slot * ring.width, (slot + 1) * ring.width);
|
|
289
|
+
}
|
|
290
|
+
m = ring.material[index];
|
|
291
|
+
if (m < 0) {
|
|
292
|
+
m = materials.materialAt(u, band.v[j]);
|
|
293
|
+
ring.material[index] = m;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
else
|
|
297
|
+
m = materials.materialAt(u, band.v[j]);
|
|
241
298
|
if (spectralIndex[m] === undefined)
|
|
242
299
|
throw new InvalidConfigurationError("Unknown chart material.");
|
|
243
300
|
material[j * stride + i] = m;
|
|
@@ -391,13 +448,17 @@ export function createDisplayViewExpectedSignal(input) {
|
|
|
391
448
|
const v = ringRead(x, y, i, j);
|
|
392
449
|
siteCache[j * sw + i] = v;
|
|
393
450
|
windowMaterial[j * sw + i] = lastMaterial;
|
|
451
|
+
markFilled(i, j);
|
|
394
452
|
return v;
|
|
395
453
|
}
|
|
396
454
|
if (filled && !cached)
|
|
397
455
|
outsideRead(i, j);
|
|
398
456
|
const v = filled ? filledSite(x, y, i, j) : input.kernels ? kernelSite(x, y) : directSite(x, y);
|
|
399
|
-
if (cached)
|
|
457
|
+
if (cached) {
|
|
400
458
|
siteCache[j * sw + i] = v;
|
|
459
|
+
if (filled)
|
|
460
|
+
markFilled(i, j);
|
|
461
|
+
}
|
|
401
462
|
return v;
|
|
402
463
|
}
|
|
403
464
|
/**
|
|
@@ -406,6 +467,40 @@ export function createDisplayViewExpectedSignal(input) {
|
|
|
406
467
|
*/
|
|
407
468
|
function windowLookups() {
|
|
408
469
|
let total = 0;
|
|
470
|
+
if (outsideSites.length === 0 && filledCount === 0)
|
|
471
|
+
return 0;
|
|
472
|
+
if (outsideSites.length === 0 && sw <= 31) {
|
|
473
|
+
// Computed sites as a bit mask per window row. Cache point (ci, cj) is covered when a computed site (i, j) has
|
|
474
|
+
// i ≤ ci < i + cw and j ≤ cj < j + ch; a union of dilations is the dilation of the union, so per cache row the
|
|
475
|
+
// covered columns are the dilation of the OR of the masks of window rows cj − ch + 1 … cj.
|
|
476
|
+
for (const group of groups) {
|
|
477
|
+
const cw = group.cw, ch = group.ch, cpw = group.cpw, cph = group.cph, last = Math.min(cph - 1, fy1 + ch - 1);
|
|
478
|
+
let previous = -1, width = 0;
|
|
479
|
+
for (let cj = fy0; cj <= last; cj++) {
|
|
480
|
+
let m = 0;
|
|
481
|
+
for (let j = Math.max(fy0, cj - ch + 1), top = Math.min(fy1, cj); j <= top; j++)
|
|
482
|
+
m |= rowMask[j];
|
|
483
|
+
if (m !== previous) {
|
|
484
|
+
previous = m;
|
|
485
|
+
width = 0;
|
|
486
|
+
let end = -1;
|
|
487
|
+
for (let i = 0; m >>> i !== 0; i++) {
|
|
488
|
+
if (((m >>> i) & 1) === 0)
|
|
489
|
+
continue;
|
|
490
|
+
const to = Math.min(i + cw - 1, cpw - 1);
|
|
491
|
+
if (i > end)
|
|
492
|
+
width += to - i + 1;
|
|
493
|
+
else if (to > end)
|
|
494
|
+
width += to - end;
|
|
495
|
+
if (to > end)
|
|
496
|
+
end = to;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
total += width;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return total;
|
|
503
|
+
}
|
|
409
504
|
for (const group of groups) {
|
|
410
505
|
const cw = group.cw, ch = group.ch, cpw = group.cpw, cph = group.cph, ew = cpw + cw - 1, eh = cph + ch - 1;
|
|
411
506
|
// Top-left marks in extended coordinates (window coordinates + (cw − 1, ch − 1)), and their bounding box.
|
|
@@ -466,6 +561,27 @@ export function createDisplayViewExpectedSignal(input) {
|
|
|
466
561
|
return total;
|
|
467
562
|
}
|
|
468
563
|
let sx0 = 0, sy0 = 0, sw = 0, sh = 0, siteCache = new Float64Array(0);
|
|
564
|
+
// The computed entries of a filled window's site cache (windowLookups): their count, bounding box and a bit mask per row
|
|
565
|
+
// (columns below 31).
|
|
566
|
+
let filledCount = 0, fx0 = 0, fx1 = -1, fy0 = 0, fy1 = -1;
|
|
567
|
+
let rowMask = new Int32Array(0);
|
|
568
|
+
function markFilled(i, j) {
|
|
569
|
+
if (i < 31)
|
|
570
|
+
rowMask[j] = rowMask[j] | (1 << i);
|
|
571
|
+
if (filledCount++ === 0) {
|
|
572
|
+
fx0 = fx1 = i;
|
|
573
|
+
fy0 = fy1 = j;
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
if (i < fx0)
|
|
577
|
+
fx0 = i;
|
|
578
|
+
else if (i > fx1)
|
|
579
|
+
fx1 = i;
|
|
580
|
+
if (j < fy0)
|
|
581
|
+
fy0 = j;
|
|
582
|
+
else if (j > fy1)
|
|
583
|
+
fy1 = j;
|
|
584
|
+
}
|
|
469
585
|
const pilotNode = input.wavelengthsNm[Math.floor(nodes / 2)];
|
|
470
586
|
return {
|
|
471
587
|
get lookups() { return lookups + (filled ? windowLookups() : 0); },
|
|
@@ -478,6 +594,8 @@ export function createDisplayViewExpectedSignal(input) {
|
|
|
478
594
|
filled = false;
|
|
479
595
|
ringOn = false;
|
|
480
596
|
outsideSites = [];
|
|
597
|
+
filledCount = 0;
|
|
598
|
+
rowMask.fill(0, 0, sh);
|
|
481
599
|
sx0 = x0;
|
|
482
600
|
sy0 = y0;
|
|
483
601
|
sw = Math.max(0, x1 - x0);
|
|
@@ -488,6 +606,8 @@ export function createDisplayViewExpectedSignal(input) {
|
|
|
488
606
|
}
|
|
489
607
|
if (siteCache.length < sw * sh)
|
|
490
608
|
siteCache = new Float64Array(sw * sh);
|
|
609
|
+
if (rowMask.length < sh)
|
|
610
|
+
rowMask = new Int32Array(sh);
|
|
491
611
|
siteCache.fill(NaN, 0, sw * sh);
|
|
492
612
|
if (!input.kernels) {
|
|
493
613
|
// A ring of at least twice the window's rows keeps the rows of the previous display row's windows.
|