@photivra/engine 2.7.0 → 2.8.1

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 (49) hide show
  1. package/CHANGELOG.md +114 -1
  2. package/CITATION.cff +1 -1
  3. package/README.md +6 -6
  4. package/SECURITY.md +1 -1
  5. package/dist/capture/export-jpeg.js +25 -12
  6. package/dist/capture/export-jpeg.js.map +1 -1
  7. package/dist/capture/native-environment-lean.d.ts +35 -5
  8. package/dist/capture/native-environment-lean.js +175 -16
  9. package/dist/capture/native-environment-lean.js.map +1 -1
  10. package/dist/capture/native-environment-raw.d.ts +9 -1
  11. package/dist/capture/native-environment-raw.js +14 -9
  12. package/dist/capture/native-environment-raw.js.map +1 -1
  13. package/dist/capture/native-environment-tabulated.d.ts +58 -0
  14. package/dist/capture/native-environment-tabulated.js +91 -0
  15. package/dist/capture/native-environment-tabulated.js.map +1 -0
  16. package/dist/core/internal-radiance-evaluator.d.ts +28 -0
  17. package/dist/core/internal-radiance-evaluator.js +30 -0
  18. package/dist/core/internal-radiance-evaluator.js.map +1 -1
  19. package/dist/core/version.d.ts +1 -1
  20. package/dist/core/version.js +1 -1
  21. package/dist/core/version.js.map +1 -1
  22. package/dist/facade/diffraction-provider.js +33 -12
  23. package/dist/facade/diffraction-provider.js.map +1 -1
  24. package/dist/facade/display-view.d.ts +14 -83
  25. package/dist/facade/display-view.js +331 -846
  26. package/dist/facade/display-view.js.map +1 -1
  27. package/dist/facade/expected-signal-setup.d.ts +65 -0
  28. package/dist/facade/expected-signal-setup.js +52 -0
  29. package/dist/facade/expected-signal-setup.js.map +1 -0
  30. package/dist/facade/expected-signal.d.ts +83 -0
  31. package/dist/facade/expected-signal.js +571 -0
  32. package/dist/facade/expected-signal.js.map +1 -0
  33. package/dist/facade/scene-radiance.d.ts +6 -0
  34. package/dist/facade/scene-radiance.js +31 -10
  35. package/dist/facade/scene-radiance.js.map +1 -1
  36. package/dist/facade/simulate-capture.js +109 -28
  37. package/dist/facade/simulate-capture.js.map +1 -1
  38. package/dist/facade/simulate-display-view.js +26 -49
  39. package/dist/facade/simulate-display-view.js.map +1 -1
  40. package/dist/optics/site-diffraction-kernel.d.ts +5 -0
  41. package/dist/optics/site-diffraction-kernel.js +32 -0
  42. package/dist/optics/site-diffraction-kernel.js.map +1 -0
  43. package/dist/output/print-jpeg.js +70 -42
  44. package/dist/output/print-jpeg.js.map +1 -1
  45. package/docs/API_REFERENCE.md +1 -1
  46. package/docs/QUICK_START.md +1 -1
  47. package/docs/api/core-version.md +1 -1
  48. package/docs/api/exports.json +2 -2
  49. package/package.json +1 -1
@@ -30,7 +30,7 @@ const REDUCTION_TOLERANCE = 1e-10;
30
30
  /** Disables the lean executor process-wide (internal; tests compare both paths). */
31
31
  let leanDisabled = false;
32
32
  /** Internal counters of lean work (tests assert the lean path actually runs). Not exported from the package. */
33
- export const nativeEnvironmentLeanStatistics = { plannedSites: 0, executedSites: 0, sensorSites: 0, psfFallbacks: 0 };
33
+ export const nativeEnvironmentLeanStatistics = { plannedSites: 0, executedSites: 0, sensorSites: 0, psfFallbacks: 0, keySites: 0 };
34
34
  export function setNativeEnvironmentLeanExecutionEnabled(enabled) { leanDisabled = !enabled; }
35
35
  function isPlainRecord(value) {
36
36
  const prototype = Object.getPrototypeOf(value);
@@ -137,8 +137,12 @@ function siteOf(environment) {
137
137
  const site = plainChild(plainChild(plainChild(environment, "sensor"), "spatialSampling"), "site");
138
138
  return site;
139
139
  }
140
- /** Plans the site-dependent parts with the full planner's arithmetic. Returns undefined on any doubt, and the caller plans in full. */
141
- function geometry(t, k, frame, environment, index) {
140
+ /**
141
+ * Plans the site-dependent parts with the full planner's arithmetic. Returns undefined on any doubt, and the caller plans in full.
142
+ * With `access` (#394: a verified key-mode template, no pupil or lens mapping), each tap keeps the key its request would read
143
+ * instead of the request.
144
+ */
145
+ function geometry(t, k, frame, environment, index, access) {
142
146
  try {
143
147
  const x = index % frame.nativeWidth, y = Math.floor(index / frame.nativeWidth), site = siteOf(environment);
144
148
  // Object.is: a -0 coordinate would flow into the full path's outputs, so it takes the full path.
@@ -233,14 +237,22 @@ function geometry(t, k, frame, environment, index) {
233
237
  const displacement = { pitch: omega.pitch * time, yaw: omega.yaw * time, roll: omega.roll * time };
234
238
  const taps = [];
235
239
  for (let gi = 0; gi < S * P; gi++) {
240
+ // #394: without a PSF a key tap depends only on its spatial node (same point, direction, field and key at
241
+ // every spectral node), so groups after the first spectral node share the first one's tap.
242
+ if (access !== undefined && destinationFields !== undefined && gi >= P) {
243
+ taps.push(taps[gi % P]);
244
+ continue;
245
+ }
236
246
  const s = Math.floor(gi / P), points = destinationFields === undefined ? tapPoints[gi] : undefined;
237
247
  const tapCount = points === undefined ? 1 : points.length;
238
248
  for (let pi = 0; pi < tapCount; pi++) {
239
- const point = points === undefined ? sources[gi % P] : points[pi], sampleId = `${id}-t${ti}-n${gi}-p${pi}`;
249
+ const point = points === undefined ? sources[gi % P] : points[pi], sampleId = access === undefined ? `${id}-t${ti}-n${gi}-p${pi}` : "";
240
250
  const apertureRays = pupil === undefined ? undefined : sensorApertureRayValues({ sourcePointNativeSensorMm: point, focalLengthMm,
241
251
  nominalFNumber, angularVelocityRadPerSec: omega, timeSecondsFromOpeningReference: time }, pupil, k.parsedFocus).rays;
242
- let request, imagePointMm, distortionAreaFactor;
252
+ let request, key, imagePointMm, distortionAreaFactor;
243
253
  if (k.lensMapping !== undefined) {
254
+ if (access !== undefined)
255
+ return undefined;
244
256
  // The full planner's own mapped query, with the same input; only its result envelope is not kept.
245
257
  const query = calculateMappedSensorEnvironmentRadianceQuery({ ...motion, sourcePointNativeSensorMm: point, focalLengthMm, focus: k.focus,
246
258
  timeSecondsFromOpeningReference: time, request: { schemaVersion: "0.1.0", sampleId, providerProfileId: base.providerProfileId, sceneId: base.sceneId,
@@ -272,15 +284,26 @@ function geometry(t, k, frame, environment, index) {
272
284
  const norm = Math.hypot(direction.x, direction.y, direction.z);
273
285
  if (!Number.isFinite(norm) || Math.abs(norm - 1) > 1e-9)
274
286
  return undefined;
275
- request = Object.freeze({ schemaVersion: "0.1.0", sampleId, providerProfileId: base.providerProfileId,
276
- sceneId: base.sceneId, illuminationProfileId: base.illuminationProfileId, materialResponseProfileId: base.materialResponseProfileId,
277
- target: Object.freeze({ kind: "environment-direction", outgoingDirectionUnitVector: Object.freeze(direction) }),
278
- timeSecondsFromExposureStart: time, wavelengthNanometers: k.wavelengths[s],
279
- wavelengthBasis: base.wavelengthBasis });
287
+ // #394: the key the evaluator would read for this request (undefined where it would throw: the full path decides).
288
+ if (access !== undefined) {
289
+ key = access.keyOfDirection(direction);
290
+ if (key === undefined)
291
+ return undefined;
292
+ }
293
+ else
294
+ request = Object.freeze({ schemaVersion: "0.1.0", sampleId, providerProfileId: base.providerProfileId,
295
+ sceneId: base.sceneId, illuminationProfileId: base.illuminationProfileId, materialResponseProfileId: base.materialResponseProfileId,
296
+ target: Object.freeze({ kind: "environment-direction", outgoingDirectionUnitVector: Object.freeze(direction) }),
297
+ timeSecondsFromExposureStart: time, wavelengthNanometers: k.wavelengths[s],
298
+ wavelengthBasis: base.wavelengthBasis });
280
299
  }
281
300
  const f = destinationFields === undefined ? tapFields[gi][pi] : destinationFields[gi % P];
282
301
  // Built without object spread (cheaper per tap); optional fields are added only when present.
283
- const leanTap = { request, sourcePointImagePlaneMm: imagePointMm, fieldThroughput: f.value, fieldFactor: f.factor, fieldValid: f.valid };
302
+ const leanTap = { sourcePointImagePlaneMm: imagePointMm, fieldThroughput: f.value, fieldFactor: f.factor, fieldValid: f.valid };
303
+ if (request !== undefined)
304
+ leanTap.request = request;
305
+ else
306
+ leanTap.key = key;
284
307
  if (apertureRays !== undefined)
285
308
  leanTap.apertureRays = apertureRays;
286
309
  if (distortionAreaFactor !== undefined)
@@ -291,7 +314,7 @@ function geometry(t, k, frame, environment, index) {
291
314
  instants.push({ time, taps });
292
315
  }
293
316
  return { lean: true, template: t, index, owned: environment, count: k.count, site: { x, y }, start, end, duration, instants,
294
- ...(k.psf === undefined ? {} : { kernels }) };
317
+ ...(k.psf === undefined ? {} : { kernels }), ...(access === undefined ? {} : { keyTaps: true }) };
295
318
  }
296
319
  catch {
297
320
  return undefined;
@@ -610,7 +633,45 @@ function executionConstants(k, environment, plan, result, self) {
610
633
  const mine = photo(k, c, values, environment, full.site, full.startOffsetSecondsFromOpeningReference, full.endOffsetSecondsFromOpeningReference, full.localExposureDurationSeconds, plan.instants.map(i => i.time));
611
634
  return mine && sameData(mine, full, new WeakMap()) ? c : undefined;
612
635
  }
613
- export function createNativeEnvironmentLeanExecutor(frame) {
636
+ /** #394: the evaluator's radiance for a key at spectral node s (tabulated keys from the template's table, computed once). */
637
+ function keyRadiance(t, k, access, key, s) {
638
+ // Untabulated keys (lattice sites) are read from the evaluator's own lookup on each use.
639
+ if (!access.tabulated)
640
+ return access.radianceOfKey(key, k.wavelengths[s]);
641
+ const table = t.radiance, row = table[key] ??= new Float64Array(k.spectralCount).fill(NaN);
642
+ let value = row[s];
643
+ if (Number.isNaN(value)) {
644
+ value = access.radianceOfKey(key, k.wavelengths[s]);
645
+ row[s] = value;
646
+ }
647
+ return value;
648
+ }
649
+ /**
650
+ * #394: true when the key taps of the template's own site give, tap by tap in callback order, exactly the radiance of each
651
+ * evaluation of its full execution. Only then do its sites take radiances from the key lookup.
652
+ */
653
+ function keyModeReproduced(t, k, access, self, result) {
654
+ if (!self || self.keyTaps !== true || k.pupilCount !== 1 || self.instants.length !== result.value.instants.length)
655
+ return false;
656
+ const S = k.spectralCount, P = k.spatialCount;
657
+ for (let ti = 0; ti < self.instants.length; ti++) {
658
+ const evaluations = result.value.instants[ti].evaluations, taps = self.instants[ti].taps;
659
+ if (evaluations.length !== taps.length)
660
+ return false;
661
+ for (let gi = 0; gi < S * P; gi++)
662
+ for (let i = k.tapOffsets[gi]; i < k.tapOffsets[gi + 1]; i++) {
663
+ const key = taps[i].key;
664
+ if (key === undefined || !Object.is(keyRadiance(t, k, access, key, Math.floor(gi / P)), evaluations[i].result.spectralRadianceWattsPerSquareMeterSteradianNanometer))
665
+ return false;
666
+ }
667
+ }
668
+ return true;
669
+ }
670
+ /**
671
+ * Creates the per-event lean executor. With `access` (#394: the key lookup of an engine-built evaluator), a template whose own
672
+ * site verifies it plans its sites as key taps and takes their radiances from the lookup.
673
+ */
674
+ export function createNativeEnvironmentLeanExecutor(frame, access) {
614
675
  const templates = [];
615
676
  const candidates = new Map();
616
677
  const memo = new WeakMap();
@@ -651,7 +712,7 @@ export function createNativeEnvironmentLeanExecutor(frame) {
651
712
  return templates.length < MAXIMUM_TEMPLATES ? "candidate" : undefined;
652
713
  if (t.disabled || !t.consts || t.executionVerified === false)
653
714
  return undefined;
654
- const leanPlan = geometry(t, t.consts, frame, environment, index);
715
+ const leanPlan = geometry(t, t.consts, frame, environment, index, t.keyMode === true ? access : undefined);
655
716
  if (leanPlan)
656
717
  nativeEnvironmentLeanStatistics.plannedSites++;
657
718
  return leanPlan;
@@ -688,19 +749,81 @@ export function createNativeEnvironmentLeanExecutor(frame) {
688
749
  return;
689
750
  t.execution = c;
690
751
  t.executionVerified = true;
752
+ // #394: key taps need no pupil and no lens mapping (the evaluator then reads one plane point per tap).
753
+ if (access !== undefined && t.consts.lensMapping === undefined && t.environment.pupil === undefined) {
754
+ t.radiance = [];
755
+ t.keyMode = keyModeReproduced(t, t.consts, access, geometry(t, t.consts, frame, t.environment, index, access), result);
756
+ if (!t.keyMode)
757
+ delete t.radiance;
758
+ }
691
759
  }
692
760
  catch {
693
761
  // Unverified: sites of this template run the full path.
694
762
  }
695
763
  },
696
- execute(plan, evaluate, aperture, fullPlan, internalEvaluator = false) {
764
+ execute(plan, evaluate, aperture, fullPlan, internalEvaluator = false, countEvaluations) {
697
765
  const t = plan.template, c = t.execution, k = t.consts;
698
766
  if (t.executionVerified !== true || !c || !k)
699
767
  return undefined;
700
768
  const environment = plan.owned, optics = environment.optics;
701
769
  const S = k.spectralCount, P = k.spatialCount, kernels = plan.kernels;
770
+ if (plan.keyTaps === true) {
771
+ // #394: every tap's radiance from the evaluator's key lookup, with tapIrradiance's arithmetic. A tap that would
772
+ // need the full irradiance function returns undefined before anything is counted: the caller runs the site in full.
773
+ if (t.keyMode !== true || !access || !countEvaluations)
774
+ return undefined;
775
+ const values = [], times = [], instantTaps = [];
776
+ for (const instant of plan.instants) {
777
+ const v = new Float64Array(S * P), tapValues = [];
778
+ if (kernels === undefined) {
779
+ // One tap per group.
780
+ for (let gi = 0; gi < S * P; gi++) {
781
+ const tap = instant.taps[gi], s = Math.floor(gi / P);
782
+ const irradiance = keyRadiance(t, k, access, tap.key, s) * (c.acceptance[s] * tap.fieldFactor);
783
+ if (!tap.fieldValid || !Number.isFinite(irradiance) || irradiance < 0)
784
+ return undefined;
785
+ let integrated = 0;
786
+ integrated += irradiance * 1;
787
+ v[gi] = integrated;
788
+ }
789
+ values.push(v);
790
+ times.push(instant.time);
791
+ continue;
792
+ }
793
+ for (let gi = 0; gi < S * P; gi++) {
794
+ const first = k.tapOffsets[gi], s = Math.floor(gi / P), groupValues = new Float64Array(k.tapOffsets[gi + 1] - first);
795
+ for (let pi = 0; pi < groupValues.length; pi++) {
796
+ const tap = instant.taps[first + pi];
797
+ const irradiance = keyRadiance(t, k, access, tap.key, s) * (c.acceptance[s] * tap.fieldFactor);
798
+ if (!tap.fieldValid || !Number.isFinite(irradiance) || irradiance < 0)
799
+ return undefined;
800
+ let integrated = 0;
801
+ integrated += irradiance * 1;
802
+ groupValues[pi] = integrated;
803
+ }
804
+ tapValues.push(groupValues);
805
+ v[gi] = kernels === undefined ? groupValues[0] : psfSum(kernels[gi], groupValues);
806
+ }
807
+ values.push(v);
808
+ instantTaps.push(tapValues);
809
+ times.push(instant.time);
810
+ }
811
+ // The evaluations the full path would count (and its abort check), then the PSF checks after each instant's taps.
812
+ countEvaluations(plan.count);
813
+ let full;
814
+ if (kernels !== undefined)
815
+ for (let ti = 0; ti < values.length; ti++) {
816
+ if (!psfChecksPass(k, c, instantTaps[ti], values[ti]))
817
+ values[ti] = fullPsfInstant(full ??= fullPlan(), ti, instantTaps[ti]);
818
+ }
819
+ nativeEnvironmentLeanStatistics.executedSites++;
820
+ nativeEnvironmentLeanStatistics.keySites++;
821
+ return photo(k, c, values, environment, plan.site, plan.start, plan.end, plan.duration, times) ??
822
+ createSensorEqeTemporalPhotoSignal(fullPhotoInput(k, values, environment, times)).value.photoSignal;
823
+ }
702
824
  /** One tap: its callbacks in order, each result validated, the pupil sum, then the distortion area factor once. */
703
- const tapIrradiance = (tap, s) => {
825
+ const tapIrradiance = (leanTap, s) => {
826
+ const tap = leanTap;
704
827
  let integrated = 0;
705
828
  const support = tap.apertureRays ?? [undefined];
706
829
  for (const ray of support) {
@@ -888,6 +1011,42 @@ function chargeTotal(photoE, darkE) {
888
1011
  }
889
1012
  return sum;
890
1013
  }
1014
+ /**
1015
+ * #394: a sensor template from one fully parsed and computed site (`calculateNativeRawSite`), as the lean sensor stage builds
1016
+ * its templates; undefined unless the lean arithmetic reproduces that site's dark and total charge, both charge realizations,
1017
+ * its codes and its flags bit for bit. Its `compute` then gives `calculateNativeRawSite`'s outputs for any site of the same
1018
+ * readout inputs (same CFA channel, dark current, capacity, readout regime and fixed-pattern profile).
1019
+ */
1020
+ export function createVerifiedNativeSiteReadout(parsed, result, whiteCode, photo, x, y, index, seed) {
1021
+ try {
1022
+ const readout = resolveSensorReadoutRegime({ profile: parsed.readoutProfile, regimeId: parsed.regimeId });
1023
+ const t = { darkRate: parsed.charge.darkCharge.darkCurrentElectronsPerSecond,
1024
+ capacity: result.capacity.value.physicalChargeCapacityElectrons, readout, pattern: parsed.fixedPatternNoise, whiteCode };
1025
+ const darkE = t.darkRate * photo.localExposureDurationSeconds, total = chargeTotal(photo.expectedGeneratedElectronCount, darkE);
1026
+ const r = result.readout.value, realization = result.realization.value, out = leanSensorSite(t, { photo, darkE, total, x, y }, index, seed);
1027
+ if (!out || !Object.is(darkE, parsed.charge.darkCharge.expectedDarkElectronCount) || !Object.is(total, result.accumulatedCharge.value.totalExpectedStoredElectronCount) ||
1028
+ !Object.is(out.photoRealized, realization.photoRealizedElectronCount) || !Object.is(out.darkRealized, realization.darkRealizedElectronCount) ||
1029
+ realization.additionalComponents.length !== 0 || out.rawCode !== r.rawCode || out.blackLevelCode !== r.blackLevelCode || out.digitalSaturationCode !== r.digitalSaturationCode ||
1030
+ out.flags !== (Number(r.physicalScalarSaturationApplied) | Number(r.preAdcSaturationApplied) << 1 | Number(r.digitalSaturationApplied) << 2))
1031
+ return undefined;
1032
+ return {
1033
+ compute(photoE, duration, sx, sy, siteIndex, siteSeed) {
1034
+ // The parse-phase predicates of the full path (claim()): finite non-negative dark and total charge.
1035
+ const siteDark = t.darkRate * duration;
1036
+ if (!Number.isFinite(photoE) || photoE < 0 || !Number.isFinite(siteDark) || siteDark < 0)
1037
+ return undefined;
1038
+ const siteTotal = chargeTotal(photoE, siteDark);
1039
+ if (!Number.isFinite(siteTotal) || siteTotal < 0)
1040
+ return undefined;
1041
+ const o = leanSensorSite(t, { photo: { expectedGeneratedElectronCount: photoE }, darkE: siteDark, total: siteTotal, x: sx, y: sy }, siteIndex, siteSeed);
1042
+ return o && { rawCode: o.rawCode, blackLevelCode: o.blackLevelCode, digitalSaturationCode: o.digitalSaturationCode, flags: o.flags, whiteCode: o.whiteCode };
1043
+ }
1044
+ };
1045
+ }
1046
+ catch {
1047
+ return undefined;
1048
+ }
1049
+ }
891
1050
  export function createNativeEnvironmentSensorLean(frame, producerFrame, nativeWindow) {
892
1051
  const enabled = !leanDisabled && nativeWindow !== undefined && frame.identityBinding && frame.repeatWidth > 0 && frame.repeatHeight > 0;
893
1052
  const templates = [], handles = new WeakMap(), memo = new WeakMap();