@photivra/engine 2.20.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 (174) hide show
  1. package/CHANGELOG.md +178 -1
  2. package/CITATION.cff +1 -1
  3. package/README.md +7 -6
  4. package/SECURITY.md +1 -1
  5. package/THIRD_PARTY.md +7 -0
  6. package/dist/capture/dng-lens-correction-opcodes.d.ts +2 -2
  7. package/dist/capture/dng-lens-correction-opcodes.js +2 -2
  8. package/dist/capture/dng-lens-correction-opcodes.js.map +1 -1
  9. package/dist/capture/exposure-build-up-stage.d.ts +16 -0
  10. package/dist/capture/exposure-build-up-stage.js +4 -0
  11. package/dist/capture/exposure-build-up-stage.js.map +1 -0
  12. package/dist/capture/native-environment-lean.d.ts +2 -1
  13. package/dist/capture/native-environment-lean.js +41 -73
  14. package/dist/capture/native-environment-lean.js.map +1 -1
  15. package/dist/capture/native-environment-tabulated.js +4 -3
  16. package/dist/capture/native-environment-tabulated.js.map +1 -1
  17. package/dist/capture/native-raw.d.ts +3 -2
  18. package/dist/capture/native-raw.js +5 -4
  19. package/dist/capture/native-raw.js.map +1 -1
  20. package/dist/capture/sensor-raw-producer.d.ts +4 -1
  21. package/dist/capture/sensor-raw-producer.js +12 -3
  22. package/dist/capture/sensor-raw-producer.js.map +1 -1
  23. package/dist/core/version.d.ts +1 -1
  24. package/dist/core/version.js +1 -1
  25. package/dist/core/version.js.map +1 -1
  26. package/dist/equipment/generic-lens-catalogue.d.ts +3 -2
  27. package/dist/equipment/generic-lens-catalogue.js +18 -5
  28. package/dist/equipment/generic-lens-catalogue.js.map +1 -1
  29. package/dist/equipment/generic-tier-assets.d.ts +32 -0
  30. package/dist/equipment/generic-tier-assets.js +16 -11
  31. package/dist/equipment/generic-tier-assets.js.map +1 -1
  32. package/dist/equipment/sensor-profile-body.d.ts +78 -0
  33. package/dist/equipment/sensor-profile-body.js +77 -0
  34. package/dist/equipment/sensor-profile-body.js.map +1 -0
  35. package/dist/equipment/sensor-profiles.d.ts +8 -0
  36. package/dist/equipment/sensor-profiles.js +82 -0
  37. package/dist/equipment/sensor-profiles.js.map +1 -0
  38. package/dist/facade/catalogue-lens-signal.d.ts +2 -2
  39. package/dist/facade/catalogue-lens-signal.js +58 -71
  40. package/dist/facade/catalogue-lens-signal.js.map +1 -1
  41. package/dist/facade/chart-radiance-batch.js +2 -1
  42. package/dist/facade/chart-radiance-batch.js.map +1 -1
  43. package/dist/facade/defocus-image-space-provider.d.ts +7 -0
  44. package/dist/facade/defocus-image-space-provider.js +64 -76
  45. package/dist/facade/defocus-image-space-provider.js.map +1 -1
  46. package/dist/facade/display-view-gpu-session.js +2 -0
  47. package/dist/facade/display-view-gpu-session.js.map +1 -1
  48. package/dist/facade/display-view-gpu.d.ts +6 -1
  49. package/dist/facade/display-view-gpu.js.map +1 -1
  50. package/dist/facade/display-view-lens-signal.js +25 -23
  51. package/dist/facade/display-view-lens-signal.js.map +1 -1
  52. package/dist/facade/display-view.js +5 -23
  53. package/dist/facade/display-view.js.map +1 -1
  54. package/dist/facade/expected-signal-setup.d.ts +3 -1
  55. package/dist/facade/expected-signal-setup.js +1 -1
  56. package/dist/facade/expected-signal-setup.js.map +1 -1
  57. package/dist/facade/expected-signal.d.ts +47 -14
  58. package/dist/facade/expected-signal.js +117 -112
  59. package/dist/facade/expected-signal.js.map +1 -1
  60. package/dist/facade/exposure-build-up.d.ts +70 -0
  61. package/dist/facade/exposure-build-up.js +164 -0
  62. package/dist/facade/exposure-build-up.js.map +1 -0
  63. package/dist/facade/fast-methods.js +9 -1
  64. package/dist/facade/fast-methods.js.map +1 -1
  65. package/dist/facade/generic-lens-capture.d.ts +2 -2
  66. package/dist/facade/generic-lens-capture.js +2 -2
  67. package/dist/facade/generic-lens-capture.js.map +1 -1
  68. package/dist/facade/image-plane-enable.d.ts +7 -0
  69. package/dist/facade/image-plane-enable.js +17 -0
  70. package/dist/facade/image-plane-enable.js.map +1 -0
  71. package/dist/facade/image-plane-lattice-data.d.ts +6 -0
  72. package/dist/facade/image-plane-lattice-data.js +10 -0
  73. package/dist/facade/image-plane-lattice-data.js.map +1 -0
  74. package/dist/facade/image-plane-support.d.ts +33 -0
  75. package/dist/facade/image-plane-support.js +11 -0
  76. package/dist/facade/image-plane-support.js.map +1 -0
  77. package/dist/facade/image-plane.d.ts +36 -0
  78. package/dist/facade/image-plane.js +353 -0
  79. package/dist/facade/image-plane.js.map +1 -0
  80. package/dist/facade/mixed-illumination.d.ts +108 -0
  81. package/dist/facade/mixed-illumination.js +174 -0
  82. package/dist/facade/mixed-illumination.js.map +1 -0
  83. package/dist/facade/representative-light-sources.d.ts +72 -0
  84. package/dist/facade/representative-light-sources.js +124 -0
  85. package/dist/facade/representative-light-sources.js.map +1 -0
  86. package/dist/facade/scene-radiance.d.ts +129 -4
  87. package/dist/facade/scene-radiance.js +48 -13
  88. package/dist/facade/scene-radiance.js.map +1 -1
  89. package/dist/facade/simulate-capture.d.ts +39 -10
  90. package/dist/facade/simulate-capture.js +252 -92
  91. package/dist/facade/simulate-capture.js.map +1 -1
  92. package/dist/facade/simulate-display-view.js +14 -6
  93. package/dist/facade/simulate-display-view.js.map +1 -1
  94. package/dist/gpu/chart-composite-gpu.d.ts +13 -1
  95. package/dist/gpu/chart-composite-gpu.js +48 -9
  96. package/dist/gpu/chart-composite-gpu.js.map +1 -1
  97. package/dist/gpu/chart-radiance-gpu.js +4 -30
  98. package/dist/gpu/chart-radiance-gpu.js.map +1 -1
  99. package/dist/gpu/display-view-composite-material-module.d.ts +10 -4
  100. package/dist/gpu/display-view-composite-material-module.js +39 -13
  101. package/dist/gpu/display-view-composite-material-module.js.map +1 -1
  102. package/dist/gpu/display-view-image-material-module.d.ts +86 -0
  103. package/dist/gpu/display-view-image-material-module.js +339 -0
  104. package/dist/gpu/display-view-image-material-module.js.map +1 -0
  105. package/dist/gpu/display-view-image-quality.d.ts +45 -0
  106. package/dist/gpu/display-view-image-quality.js +380 -0
  107. package/dist/gpu/display-view-image-quality.js.map +1 -0
  108. package/dist/gpu/display-view-quality-gpu.d.ts +67 -1
  109. package/dist/gpu/display-view-quality-gpu.js +136 -205
  110. package/dist/gpu/display-view-quality-gpu.js.map +1 -1
  111. package/dist/gpu/display-view-speed-gpu.d.ts +51 -3
  112. package/dist/gpu/display-view-speed-gpu.js +131 -93
  113. package/dist/gpu/display-view-speed-gpu.js.map +1 -1
  114. package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +29 -15
  115. package/dist/gpu/wgsl-display-view-quality-kernel.js +58 -36
  116. package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
  117. package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +21 -6
  118. package/dist/gpu/wgsl-display-view-speed-kernel.js +46 -11
  119. package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
  120. package/dist/gpu/wgsl-exact-sum.d.ts +20 -0
  121. package/dist/gpu/wgsl-exact-sum.js +42 -0
  122. package/dist/gpu/wgsl-exact-sum.js.map +1 -0
  123. package/dist/index.d.ts +11 -3
  124. package/dist/index.js +7 -1
  125. package/dist/index.js.map +1 -1
  126. package/dist/motion/subject-motion.d.ts +66 -0
  127. package/dist/motion/subject-motion.js +52 -0
  128. package/dist/motion/subject-motion.js.map +1 -0
  129. package/dist/optics/generic-lens-lateral-color.d.ts +83 -0
  130. package/dist/optics/generic-lens-lateral-color.js +77 -0
  131. package/dist/optics/generic-lens-lateral-color.js.map +1 -0
  132. package/dist/optics/generic-lens-psf.js +54 -46
  133. package/dist/optics/generic-lens-psf.js.map +1 -1
  134. package/dist/optics/lens-psf-profile.d.ts +7 -0
  135. package/dist/optics/lens-psf-profile.js +2 -1
  136. package/dist/optics/lens-psf-profile.js.map +1 -1
  137. package/dist/output/developed-lens-corrections.d.ts +19 -5
  138. package/dist/output/developed-lens-corrections.js +28 -9
  139. package/dist/output/developed-lens-corrections.js.map +1 -1
  140. package/dist/output/generic-lens-corrections.d.ts +41 -5
  141. package/dist/output/generic-lens-corrections.js +52 -8
  142. package/dist/output/generic-lens-corrections.js.map +1 -1
  143. package/dist/output/print-plan.d.ts +41 -5
  144. package/dist/output/print-plan.js +59 -14
  145. package/dist/output/print-plan.js.map +1 -1
  146. package/dist/sensor/binomial-sample.d.ts +3 -0
  147. package/dist/sensor/binomial-sample.js +86 -0
  148. package/dist/sensor/binomial-sample.js.map +1 -0
  149. package/dist/sensor/sensor-characteristics.d.ts +10 -0
  150. package/dist/sensor/sensor-characteristics.js +20 -9
  151. package/dist/sensor/sensor-characteristics.js.map +1 -1
  152. package/docs/AGENT_GUIDE.md +1 -0
  153. package/docs/API_REFERENCE.md +98 -1
  154. package/docs/QUICK_START.md +125 -0
  155. package/docs/USAGE.md +92 -1
  156. package/docs/api/core-version.md +1 -1
  157. package/docs/api/equipment-generic-lens-catalogue.md +10 -5
  158. package/docs/api/equipment-sensor-profile-body.md +64 -0
  159. package/docs/api/equipment-sensor-profiles.md +86 -0
  160. package/docs/api/exports.json +207 -2
  161. package/docs/api/facade-exposure-build-up.md +105 -0
  162. package/docs/api/facade-fast-methods.md +9 -1
  163. package/docs/api/facade-image-plane-enable.md +16 -0
  164. package/docs/api/facade-mixed-illumination.md +67 -0
  165. package/docs/api/facade-representative-light-sources.md +131 -0
  166. package/docs/api/facade-scene-radiance.md +54 -3
  167. package/docs/api/facade-simulate-capture.md +26 -6
  168. package/docs/api/motion-subject-motion.md +23 -0
  169. package/docs/api/optics-generic-lens-lateral-color.md +79 -0
  170. package/docs/api/output-developed-lens-corrections.md +10 -1
  171. package/docs/api/output-generic-lens-corrections.md +40 -4
  172. package/docs/api/output-print-plan.md +35 -1
  173. package/llms.txt +2 -0
  174. package/package.json +1 -1
@@ -965,58 +965,21 @@ function sensorMatches(site, photo, x, y, t, memo) {
965
965
  return sameRecordExcept(photo, t.photo, PHOTO_SITE_FIELDS, memo) && siteSlot(photo.site, x, y) &&
966
966
  photo.temporalSamples.length === t.photo.temporalSamples.length;
967
967
  }
968
- /** The sensor stage of calculateNativeRawSite, in the same operation order. Returns undefined when one of its checks would throw. */
969
968
  /**
970
- * #418: the charge and read-noise streams of the site leanSensorSite is computing, reseeded for each site. leanSensorSite is
969
+ * #418: the charge and read-noise streams of the site flatSensorSite is computing, reseeded for each site. flatSensorSite is
971
970
  * synchronous and never re-entered, so one pair serves every call; reseeding sets exactly the state a new
972
971
  * createDeterministicUniformStream would start from, so every draw is unchanged.
973
972
  */
974
973
  const chargeStream = createResettableUniformStream(), readNoiseStream = createResettableUniformStream();
975
- function leanSensorSite(t, h, index, seed) {
976
- const ratio = h.total / t.capacity, headroom = t.capacity - h.total;
977
- if (!Number.isFinite(ratio) || !Number.isFinite(headroom))
978
- return undefined;
979
- const pattern = t.pattern === undefined ? null : evaluateParsedFixedPatternSite(t.pattern, { x: h.x, y: h.y });
980
- const photoE = h.photo.expectedGeneratedElectronCount;
981
- const photoMean = pattern === null ? photoE : photoE * pattern.photoResponseGainFactor;
982
- const darkMean = pattern === null ? h.darkE : h.darkE * pattern.darkCurrentFactor;
983
- if (!Number.isFinite(photoMean) || photoMean < 0 || !Number.isFinite(darkMean) || darkMean < 0)
984
- return undefined;
985
- const random = chargeStream;
986
- random.reset(deriveNativeSiteStreamSeedUint32(seed, index, NATIVE_SITE_CHARGE_STREAM_ID));
987
- const photoRealized = samplePoisson(photoMean, random), darkRealized = samplePoisson(darkMean, random);
988
- const realized = photoRealized + darkRealized + 0;
989
- if (!Number.isFinite(realized) || realized < 0)
990
- return undefined;
991
- const readRandom = readNoiseStream;
992
- readRandom.reset(deriveNativeSiteStreamSeedUint32(seed, index, NATIVE_SITE_READ_NOISE_STREAM_ID));
993
- const regime = t.readout.regime, capacity = t.capacity;
994
- const afterPhysical = Math.min(realized, capacity), physicalSaturation = afterPhysical < realized;
995
- // The components' draws and their sum in one loop: the same draws, added to 0 in the same order as before (#418).
996
- const components = regime.readNoiseComponents;
997
- let totalReadNoise = 0;
998
- for (let k = 0; k < components.length; k++) {
999
- const rms = components[k].rmsElectrons.value;
1000
- totalReadNoise = totalReadNoise + (rms === 0 ? 0 : sampleDeterministicStandardNormal(readRandom) * rms);
1001
- }
1002
- const afterReadNoise = afterPhysical + totalReadNoise + (pattern === null ? 0 : pattern.readoutOffsetElectrons);
1003
- const threshold = regime.preAdcSaturationElectronEquivalent.value;
1004
- const afterPreAdc = Math.min(afterReadNoise, threshold), preAdcSaturation = afterReadNoise > threshold;
1005
- const quantized = adcTransfer(regime, t.readout.adcMaximumCode, afterPreAdc).quantized;
1006
- const digitalSaturationCode = regime.adc.digitalSaturationCode;
1007
- return { rawCode: Math.max(0, Math.min(quantized, digitalSaturationCode)), blackLevelCode: regime.adc.blackLevelCode, digitalSaturationCode,
1008
- flags: Number(physicalSaturation) | Number(preAdcSaturation) << 1 | Number(quantized >= digitalSaturationCode) << 2,
1009
- whiteCode: t.whiteCode, photoRealized, darkRealized };
1010
- }
1011
- function flatSensorStage(t) {
974
+ function flatSensorStage(t, b) {
1012
975
  const regime = t.readout.regime;
1013
976
  return { darkRate: t.darkRate, capacity: t.capacity, pattern: t.pattern, whiteCode: t.whiteCode, regime, adcMaximumCode: t.readout.adcMaximumCode,
1014
977
  inlDefined: regime.adc.integralNonLinearity !== undefined, gain: regime.systemConversionGainElectronsPerCode.value, black: regime.adc.blackLevelCode,
1015
978
  digitalGain: regime.adc.digitalGain, digitalSaturationCode: regime.adc.digitalSaturationCode, threshold: regime.preAdcSaturationElectronEquivalent.value,
1016
979
  rms: Float64Array.from(regime.readNoiseComponents, (component) => component.rmsElectrons.value), darkMean: NaN, darkThreshold: NaN,
1017
- out: { rawCode: 0, blackLevelCode: 0, digitalSaturationCode: 0, flags: 0, whiteCode: 0 } };
980
+ out: { rawCode: 0, blackLevelCode: 0, digitalSaturationCode: 0, flags: 0, whiteCode: 0, photoRealized: 0, darkRealized: 0 }, b };
1018
981
  }
1019
- /** Writes the site's outputs into `f.out`; false where leanSensorSite returns undefined. */
982
+ /** Writes the site's outputs into `f.out`; false where a check of calculateNativeRawSite would throw. */
1020
983
  function flatSensorSite(f, photoE, darkE, total, x, y, index, seed) {
1021
984
  const ratio = total / f.capacity, headroom = f.capacity - total;
1022
985
  if (!Number.isFinite(ratio) || !Number.isFinite(headroom))
@@ -1028,8 +991,7 @@ function flatSensorSite(f, photoE, darkE, total, x, y, index, seed) {
1028
991
  return false;
1029
992
  const random = chargeStream;
1030
993
  random.reset(deriveNativeSiteStreamSeedUint32(seed, index, NATIVE_SITE_CHARGE_STREAM_ID));
1031
- const photoRealized = samplePoisson(photoMean, random);
1032
- let darkRealized;
994
+ let photoRealized = samplePoisson(photoMean, random), darkRealized;
1033
995
  if (darkMean > 0 && darkMean < 30) {
1034
996
  // samplePoisson's branch for 0 < mean < 30 (Knuth's product of uniforms) with its threshold Math.exp(−mean) kept while the
1035
997
  // mean repeats: the same draws from the same stream. (createPoissonSampler would also do this, but it brings the
@@ -1048,11 +1010,17 @@ function flatSensorSite(f, photoE, darkE, total, x, y, index, seed) {
1048
1010
  }
1049
1011
  else
1050
1012
  darkRealized = samplePoisson(darkMean, random);
1013
+ const b = f.b;
1014
+ if (b) {
1015
+ b.split(index, photoMean, darkMean, photoRealized, darkRealized);
1016
+ photoRealized = b.photo;
1017
+ darkRealized = b.dark;
1018
+ }
1051
1019
  const realized = photoRealized + darkRealized + 0;
1052
1020
  if (!Number.isFinite(realized) || realized < 0)
1053
1021
  return false;
1054
1022
  const readRandom = readNoiseStream;
1055
- readRandom.reset(deriveNativeSiteStreamSeedUint32(seed, index, NATIVE_SITE_READ_NOISE_STREAM_ID));
1023
+ readRandom.reset(deriveNativeSiteStreamSeedUint32(seed, index, b ? b.read : NATIVE_SITE_READ_NOISE_STREAM_ID));
1056
1024
  const capacity = f.capacity;
1057
1025
  const afterPhysical = Math.min(realized, capacity), physicalSaturation = afterPhysical < realized;
1058
1026
  const rmsAll = f.rms;
@@ -1084,6 +1052,8 @@ function flatSensorSite(f, photoE, darkE, total, x, y, index, seed) {
1084
1052
  out.digitalSaturationCode = digitalSaturationCode;
1085
1053
  out.flags = Number(physicalSaturation) | Number(preAdcSaturation) << 1 | Number(quantized >= digitalSaturationCode) << 2;
1086
1054
  out.whiteCode = f.whiteCode;
1055
+ out.photoRealized = photoRealized;
1056
+ out.darkRealized = darkRealized;
1087
1057
  return true;
1088
1058
  }
1089
1059
  /** The Kahan (compensated) total that composeSensorAccumulatedCharge computes, for photo and dark charge only. */
@@ -1096,28 +1066,32 @@ function chargeTotal(photoE, darkE) {
1096
1066
  }
1097
1067
  return sum;
1098
1068
  }
1069
+ /**
1070
+ * True when the flat stage `f` reproduces the fully computed site `result` bit for bit: its dark and total charge, both charge
1071
+ * realizations, its codes and its flags.
1072
+ */
1073
+ function reproducesSite(f, parsed, result, photo, x, y, index, seed) {
1074
+ const darkE = f.darkRate * photo.localExposureDurationSeconds, total = chargeTotal(photo.expectedGeneratedElectronCount, darkE);
1075
+ const r = result.readout.value, realization = result.realization.value, out = f.out;
1076
+ return flatSensorSite(f, photo.expectedGeneratedElectronCount, darkE, total, x, y, index, seed) && Object.is(darkE, parsed.charge.darkCharge.expectedDarkElectronCount) &&
1077
+ Object.is(total, result.accumulatedCharge.value.totalExpectedStoredElectronCount) &&
1078
+ Object.is(out.photoRealized, realization.photoRealizedElectronCount) && Object.is(out.darkRealized, realization.darkRealizedElectronCount) &&
1079
+ realization.additionalComponents.length === 0 && out.rawCode === r.rawCode && out.blackLevelCode === r.blackLevelCode && out.digitalSaturationCode === r.digitalSaturationCode &&
1080
+ out.flags === (Number(r.physicalScalarSaturationApplied) | Number(r.preAdcSaturationApplied) << 1 | Number(r.digitalSaturationApplied) << 2);
1081
+ }
1099
1082
  /**
1100
1083
  * #394: a sensor template from one fully parsed and computed site (`calculateNativeRawSite`), as the lean sensor stage builds
1101
1084
  * its templates; undefined unless the lean arithmetic reproduces that site's dark and total charge, both charge realizations,
1102
1085
  * its codes and its flags bit for bit. Its `compute` then gives `calculateNativeRawSite`'s outputs for any site of the same
1103
1086
  * readout inputs (same CFA channel, dark current, capacity, readout regime and fixed-pattern profile).
1104
1087
  */
1105
- export function createVerifiedNativeSiteReadout(parsed, result, whiteCode, photo, x, y, index, seed) {
1088
+ export function createVerifiedNativeSiteReadout(parsed, result, whiteCode, photo, x, y, index, seed, b) {
1106
1089
  try {
1107
1090
  const readout = resolveSensorReadoutRegime({ profile: parsed.readoutProfile, regimeId: parsed.regimeId });
1108
1091
  const t = { darkRate: parsed.charge.darkCharge.darkCurrentElectronsPerSecond,
1109
1092
  capacity: result.capacity.value.physicalChargeCapacityElectrons, readout, pattern: parsed.fixedPatternNoise, whiteCode };
1110
- const darkE = t.darkRate * photo.localExposureDurationSeconds, total = chargeTotal(photo.expectedGeneratedElectronCount, darkE);
1111
- const r = result.readout.value, realization = result.realization.value, out = leanSensorSite(t, { photo, darkE, total, x, y }, index, seed);
1112
- if (!out || !Object.is(darkE, parsed.charge.darkCharge.expectedDarkElectronCount) || !Object.is(total, result.accumulatedCharge.value.totalExpectedStoredElectronCount) ||
1113
- !Object.is(out.photoRealized, realization.photoRealizedElectronCount) || !Object.is(out.darkRealized, realization.darkRealizedElectronCount) ||
1114
- realization.additionalComponents.length !== 0 || out.rawCode !== r.rawCode || out.blackLevelCode !== r.blackLevelCode || out.digitalSaturationCode !== r.digitalSaturationCode ||
1115
- out.flags !== (Number(r.physicalScalarSaturationApplied) | Number(r.preAdcSaturationApplied) << 1 | Number(r.digitalSaturationApplied) << 2))
1116
- return undefined;
1117
- // #449: the flat stage must reproduce the verification site too.
1118
- const flat = flatSensorStage(t), fo = flat.out;
1119
- if (!flatSensorSite(flat, photo.expectedGeneratedElectronCount, darkE, total, x, y, index, seed) || fo.rawCode !== out.rawCode || fo.blackLevelCode !== out.blackLevelCode ||
1120
- fo.digitalSaturationCode !== out.digitalSaturationCode || fo.flags !== out.flags || fo.whiteCode !== out.whiteCode)
1093
+ const flat = flatSensorStage(t, b), fo = flat.out;
1094
+ if (!reproducesSite(flat, parsed, result, photo, x, y, index, seed))
1121
1095
  return undefined;
1122
1096
  return {
1123
1097
  compute(photoE, duration, sx, sy, siteIndex, siteSeed) {
@@ -1189,20 +1163,21 @@ export function createNativeEnvironmentSensorLean(frame, producerFrame, nativeWi
1189
1163
  claim(site) {
1190
1164
  return typeof site === "object" && site !== null && handles.has(site) ? site : undefined;
1191
1165
  },
1192
- compute(handle, index, seed, timing) {
1193
- const h = handles.get(handle), out = leanSensorSite(h.template, h, index, seed);
1194
- if (out)
1166
+ compute(handle, index, seed, timing, b) {
1167
+ // The template's flat stage (made in observe(), with the same build-up stage); the caller copies the outputs at once.
1168
+ const h = handles.get(handle), f = h.template.flat;
1169
+ if (flatSensorSite(f, h.photo.expectedGeneratedElectronCount, h.darkE, h.total, h.x, h.y, index, seed)) {
1195
1170
  nativeEnvironmentLeanStatistics.sensorSites++;
1196
- if (out)
1197
- return { rawCode: out.rawCode, blackLevelCode: out.blackLevelCode, digitalSaturationCode: out.digitalSaturationCode, flags: out.flags, whiteCode: out.whiteCode };
1171
+ return f.out;
1172
+ }
1198
1173
  // A compute-phase check failed: the full path computes (and throws) exactly as it would have.
1199
1174
  const parsed = parseNativeRawProducerSite(composeNativeEnvironmentSensorSite(h.site, h.photo), index, producerFrame, timing);
1200
- const r = calculateNativeRawSite(parsed, seed, index).readout.value;
1175
+ const r = calculateNativeRawSite(parsed, seed, index, b).readout.value;
1201
1176
  return { rawCode: r.rawCode, blackLevelCode: r.blackLevelCode, digitalSaturationCode: r.digitalSaturationCode,
1202
1177
  flags: Number(r.physicalScalarSaturationApplied) | Number(r.preAdcSaturationApplied) << 1 | Number(r.digitalSaturationApplied) << 2,
1203
1178
  whiteCode: calculateSiteEffectiveSaturationCode(r, parsed.readoutProfile, parsed.regimeId) };
1204
1179
  },
1205
- observe(index, parsed, result, whiteCode, seed) {
1180
+ observe(index, parsed, result, whiteCode, seed, b) {
1206
1181
  const entry = pending.get(index);
1207
1182
  pending.delete(index);
1208
1183
  if (!entry || templates.length >= MAXIMUM_TEMPLATES)
@@ -1215,18 +1190,11 @@ export function createNativeEnvironmentSensorLean(frame, producerFrame, nativeWi
1215
1190
  const t = { site, photo, phase, x, y, verified: false, darkRate: parsed.charge.darkCharge.darkCurrentElectronsPerSecond,
1216
1191
  capacity: result.capacity.value.physicalChargeCapacityElectrons, readout, pattern: parsed.fixedPatternNoise, whiteCode };
1217
1192
  templates.push(t);
1218
- // Self-check: the template matches itself and the lean arithmetic reproduces its full computation bit for bit.
1193
+ // Self-check: the template matches itself and its flat stage reproduces its full computation bit for bit.
1219
1194
  if (!sensorMatches(site, photo, x, y, t, new WeakMap()))
1220
1195
  return;
1221
- const darkE = t.darkRate * photo.localExposureDurationSeconds, total = chargeTotal(photo.expectedGeneratedElectronCount, darkE);
1222
- const r = result.readout.value, realization = result.realization.value;
1223
- const out = leanSensorSite(t, { photo, darkE, total, x, y }, index, seed);
1224
- if (!out || !Object.is(darkE, parsed.charge.darkCharge.expectedDarkElectronCount) ||
1225
- !Object.is(total, result.accumulatedCharge.value.totalExpectedStoredElectronCount) ||
1226
- !Object.is(out.photoRealized, realization.photoRealizedElectronCount) || !Object.is(out.darkRealized, realization.darkRealizedElectronCount) ||
1227
- realization.additionalComponents.length !== 0 || out.rawCode !== r.rawCode || out.blackLevelCode !== r.blackLevelCode ||
1228
- out.digitalSaturationCode !== r.digitalSaturationCode || out.flags !== (Number(r.physicalScalarSaturationApplied) |
1229
- Number(r.preAdcSaturationApplied) << 1 | Number(r.digitalSaturationApplied) << 2))
1196
+ t.flat = flatSensorStage(t, b);
1197
+ if (!reproducesSite(t.flat, parsed, result, photo, x, y, index, seed))
1230
1198
  return;
1231
1199
  t.verified = true;
1232
1200
  }