@photivra/engine 2.8.1 → 2.10.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 (91) hide show
  1. package/CHANGELOG.md +144 -1
  2. package/CITATION.cff +1 -1
  3. package/README.md +8 -6
  4. package/SECURITY.md +1 -1
  5. package/THIRD_PARTY.md +2 -0
  6. package/dist/capture/export-jpeg.d.ts +19 -0
  7. package/dist/capture/export-jpeg.js +190 -108
  8. package/dist/capture/export-jpeg.js.map +1 -1
  9. package/dist/capture/native-environment-lean.js +106 -12
  10. package/dist/capture/native-environment-lean.js.map +1 -1
  11. package/dist/capture/native-environment-tabulated.d.ts +37 -0
  12. package/dist/capture/native-environment-tabulated.js +42 -6
  13. package/dist/capture/native-environment-tabulated.js.map +1 -1
  14. package/dist/core/portable-math.d.ts +13 -0
  15. package/dist/core/portable-math.js +43 -0
  16. package/dist/core/portable-math.js.map +1 -1
  17. package/dist/core/version.d.ts +1 -1
  18. package/dist/core/version.js +1 -1
  19. package/dist/core/version.js.map +1 -1
  20. package/dist/facade/channel-response.d.ts +15 -0
  21. package/dist/facade/channel-response.js +33 -0
  22. package/dist/facade/channel-response.js.map +1 -0
  23. package/dist/facade/diffraction-provider.js +50 -15
  24. package/dist/facade/diffraction-provider.js.map +1 -1
  25. package/dist/facade/display-view.d.ts +75 -0
  26. package/dist/facade/display-view.js +471 -109
  27. package/dist/facade/display-view.js.map +1 -1
  28. package/dist/facade/expected-signal-setup.js +2 -1
  29. package/dist/facade/expected-signal-setup.js.map +1 -1
  30. package/dist/facade/expected-signal.d.ts +36 -4
  31. package/dist/facade/expected-signal.js +331 -43
  32. package/dist/facade/expected-signal.js.map +1 -1
  33. package/dist/facade/scene-radiance.d.ts +4 -1
  34. package/dist/facade/scene-radiance.js +4 -4
  35. package/dist/facade/scene-radiance.js.map +1 -1
  36. package/dist/facade/simulate-capture.d.ts +77 -5
  37. package/dist/facade/simulate-capture.js +232 -40
  38. package/dist/facade/simulate-capture.js.map +1 -1
  39. package/dist/facade/simulate-display-view.d.ts +25 -2
  40. package/dist/facade/simulate-display-view.js +55 -21
  41. package/dist/facade/simulate-display-view.js.map +1 -1
  42. package/dist/gpu/image-kernel-execution.d.ts +5 -0
  43. package/dist/gpu/image-kernel-execution.js +2 -2
  44. package/dist/gpu/image-kernel-execution.js.map +1 -1
  45. package/dist/index.d.ts +5 -4
  46. package/dist/index.js +3 -3
  47. package/dist/index.js.map +1 -1
  48. package/dist/optics/diffraction-psf-profile.d.ts +19 -17
  49. package/dist/optics/diffraction-psf-profile.js +49 -18
  50. package/dist/optics/diffraction-psf-profile.js.map +1 -1
  51. package/dist/optics/irradiance-convolution.d.ts +11 -0
  52. package/dist/optics/irradiance-convolution.js +64 -24
  53. package/dist/optics/irradiance-convolution.js.map +1 -1
  54. package/dist/optics/scene-to-sensor-irradiance.d.ts +13 -0
  55. package/dist/optics/scene-to-sensor-irradiance.js +58 -24
  56. package/dist/optics/scene-to-sensor-irradiance.js.map +1 -1
  57. package/dist/optics/sensor-aperture-rays.js +2 -1
  58. package/dist/optics/sensor-aperture-rays.js.map +1 -1
  59. package/dist/optics/sensor-environment-query.js +4 -3
  60. package/dist/optics/sensor-environment-query.js.map +1 -1
  61. package/dist/optics/site-diffraction-kernel.d.ts +16 -0
  62. package/dist/optics/site-diffraction-kernel.js +60 -8
  63. package/dist/optics/site-diffraction-kernel.js.map +1 -1
  64. package/dist/output/print-jpeg.d.ts +28 -1
  65. package/dist/output/print-jpeg.js +79 -7
  66. package/dist/output/print-jpeg.js.map +1 -1
  67. package/dist/processing/demosaic.js +114 -37
  68. package/dist/processing/demosaic.js.map +1 -1
  69. package/dist/schema/scene-radiance.d.ts +8 -0
  70. package/dist/schema/scene-radiance.js +28 -17
  71. package/dist/schema/scene-radiance.js.map +1 -1
  72. package/dist/sensor/environment-photo-signal.d.ts +21 -0
  73. package/dist/sensor/environment-photo-signal.js +68 -2
  74. package/dist/sensor/environment-photo-signal.js.map +1 -1
  75. package/dist/sensor/stochastic-seed.d.ts +12 -0
  76. package/dist/sensor/stochastic-seed.js +25 -0
  77. package/dist/sensor/stochastic-seed.js.map +1 -1
  78. package/docs/API_REFERENCE.md +23 -1
  79. package/docs/QUICK_START.md +77 -8
  80. package/docs/USAGE.md +30 -0
  81. package/docs/api/capture-export-jpeg.md +10 -0
  82. package/docs/api/core-version.md +1 -1
  83. package/docs/api/exports.json +77 -2
  84. package/docs/api/facade-display-view.md +50 -0
  85. package/docs/api/facade-scene-radiance.md +2 -1
  86. package/docs/api/facade-simulate-capture.md +88 -8
  87. package/docs/api/facade-simulate-display-view.md +23 -1
  88. package/docs/api/gpu-image-kernel-execution.md +5 -0
  89. package/docs/api/optics-diffraction-psf-profile.md +1 -1
  90. package/docs/api/output-print-jpeg.md +41 -0
  91. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  import { InvalidConfigurationError } from "../core/configuration-error.js";
3
+ import { hypot3 } from "../core/portable-math.js";
3
4
  import { apertureRadianceQuery } from "../sensor/environment-photo-signal.js";
4
5
  import { createSensorEqeTemporalPhotoSignal } from "../sensor/temporal-photo-signal.js";
5
6
  import { isDeeplyFrozen } from "../core/internal-radiance-evaluator.js";
@@ -21,7 +22,7 @@ import { composeNativeEnvironmentSensorSite } from "./native-environment-optical
21
22
  import { calculateNativeRawSite, parseNativeRawProducerSite } from "./sensor-raw-producer.js";
22
23
  import { calculateSiteEffectiveSaturationCode } from "./raw-white-level.js";
23
24
  import { adcTransfer, resolveSensorReadoutRegime, samplePoisson } from "../sensor/raw-readout.js";
24
- import { createDeterministicUniformStream, deriveNativeSiteStreamSeedUint32, sampleDeterministicStandardNormal, NATIVE_SITE_CHARGE_STREAM_ID, NATIVE_SITE_READ_NOISE_STREAM_ID } from "../sensor/stochastic-seed.js";
25
+ import { createResettableUniformStream, deriveNativeSiteStreamSeedUint32, sampleDeterministicStandardNormal, NATIVE_SITE_CHARGE_STREAM_ID, NATIVE_SITE_READ_NOISE_STREAM_ID } from "../sensor/stochastic-seed.js";
25
26
  import { evaluateParsedFixedPatternSite } from "../sensor/fixed-pattern-noise.js";
26
27
  /** Most distinct templates kept for one capture event. Any further distinct sites just run the full path. */
27
28
  const MAXIMUM_TEMPLATES = 64;
@@ -273,7 +274,7 @@ function geometry(t, k, frame, environment, index, access) {
273
274
  if (!Number.isFinite(reference.z) || reference.z <= 0)
274
275
  return undefined;
275
276
  const rx = pd * (reference.x / reference.z), ry = pd * (reference.y / reference.z);
276
- const length = Math.hypot(rx, ry, pd);
277
+ const length = hypot3(rx, ry, pd);
277
278
  const ray = { x: rx / length, y: ry / length, z: pd / length };
278
279
  // Build the parseSceneRadianceEvaluationRequest output directly. Every other field is a template constant already
279
280
  // parsed for the template site. The sample ID is a public opaque ID (trimming cannot change it) and the time is
@@ -281,7 +282,7 @@ function geometry(t, k, frame, environment, index, access) {
281
282
  const direction = { x: -ray.x, y: -ray.y, z: -ray.z };
282
283
  if (!Number.isFinite(direction.x) || !Number.isFinite(direction.y) || !Number.isFinite(direction.z))
283
284
  return undefined;
284
- const norm = Math.hypot(direction.x, direction.y, direction.z);
285
+ const norm = hypot3(direction.x, direction.y, direction.z);
285
286
  if (!Number.isFinite(norm) || Math.abs(norm - 1) > 1e-9)
286
287
  return undefined;
287
288
  // #394: the key the evaluator would read for this request (undefined where it would throw: the full path decides).
@@ -969,6 +970,12 @@ function sensorMatches(site, photo, x, y, t, memo) {
969
970
  photo.temporalSamples.length === t.photo.temporalSamples.length;
970
971
  }
971
972
  /** The sensor stage of calculateNativeRawSite, in the same operation order. Returns undefined when one of its checks would throw. */
973
+ /**
974
+ * #418: the charge and read-noise streams of the site leanSensorSite is computing, reseeded for each site. leanSensorSite is
975
+ * synchronous and never re-entered, so one pair serves every call; reseeding sets exactly the state a new
976
+ * createDeterministicUniformStream would start from, so every draw is unchanged.
977
+ */
978
+ const chargeStream = createResettableUniformStream(), readNoiseStream = createResettableUniformStream();
972
979
  function leanSensorSite(t, h, index, seed) {
973
980
  const ratio = h.total / t.capacity, headroom = t.capacity - h.total;
974
981
  if (!Number.isFinite(ratio) || !Number.isFinite(headroom))
@@ -979,19 +986,23 @@ function leanSensorSite(t, h, index, seed) {
979
986
  const darkMean = pattern === null ? h.darkE : h.darkE * pattern.darkCurrentFactor;
980
987
  if (!Number.isFinite(photoMean) || photoMean < 0 || !Number.isFinite(darkMean) || darkMean < 0)
981
988
  return undefined;
982
- const random = createDeterministicUniformStream(deriveNativeSiteStreamSeedUint32(seed, index, NATIVE_SITE_CHARGE_STREAM_ID));
989
+ const random = chargeStream;
990
+ random.reset(deriveNativeSiteStreamSeedUint32(seed, index, NATIVE_SITE_CHARGE_STREAM_ID));
983
991
  const photoRealized = samplePoisson(photoMean, random), darkRealized = samplePoisson(darkMean, random);
984
992
  const realized = photoRealized + darkRealized + 0;
985
993
  if (!Number.isFinite(realized) || realized < 0)
986
994
  return undefined;
987
- const readRandom = createDeterministicUniformStream(deriveNativeSiteStreamSeedUint32(seed, index, NATIVE_SITE_READ_NOISE_STREAM_ID));
995
+ const readRandom = readNoiseStream;
996
+ readRandom.reset(deriveNativeSiteStreamSeedUint32(seed, index, NATIVE_SITE_READ_NOISE_STREAM_ID));
988
997
  const regime = t.readout.regime, capacity = t.capacity;
989
998
  const afterPhysical = Math.min(realized, capacity), physicalSaturation = afterPhysical < realized;
990
- const sampled = regime.readNoiseComponents.map(component => component.rmsElectrons.value === 0 ? 0 :
991
- sampleDeterministicStandardNormal(readRandom) * component.rmsElectrons.value);
999
+ // The components' draws and their sum in one loop: the same draws, added to 0 in the same order as before (#418).
1000
+ const components = regime.readNoiseComponents;
992
1001
  let totalReadNoise = 0;
993
- for (const value of sampled)
994
- totalReadNoise = totalReadNoise + value;
1002
+ for (let k = 0; k < components.length; k++) {
1003
+ const rms = components[k].rmsElectrons.value;
1004
+ totalReadNoise = totalReadNoise + (rms === 0 ? 0 : sampleDeterministicStandardNormal(readRandom) * rms);
1005
+ }
995
1006
  const afterReadNoise = afterPhysical + totalReadNoise + (pattern === null ? 0 : pattern.readoutOffsetElectrons);
996
1007
  const threshold = regime.preAdcSaturationElectronEquivalent.value;
997
1008
  const afterPreAdc = Math.min(afterReadNoise, threshold), preAdcSaturation = afterReadNoise > threshold;
@@ -1001,6 +1012,84 @@ function leanSensorSite(t, h, index, seed) {
1001
1012
  flags: Number(physicalSaturation) | Number(preAdcSaturation) << 1 | Number(quantized >= digitalSaturationCode) << 2,
1002
1013
  whiteCode: t.whiteCode, photoRealized, darkRealized };
1003
1014
  }
1015
+ function flatSensorStage(t) {
1016
+ const regime = t.readout.regime;
1017
+ return { darkRate: t.darkRate, capacity: t.capacity, pattern: t.pattern, whiteCode: t.whiteCode, regime, adcMaximumCode: t.readout.adcMaximumCode,
1018
+ inlDefined: regime.adc.integralNonLinearity !== undefined, gain: regime.systemConversionGainElectronsPerCode.value, black: regime.adc.blackLevelCode,
1019
+ digitalGain: regime.adc.digitalGain, digitalSaturationCode: regime.adc.digitalSaturationCode, threshold: regime.preAdcSaturationElectronEquivalent.value,
1020
+ rms: Float64Array.from(regime.readNoiseComponents, (component) => component.rmsElectrons.value), darkMean: NaN, darkThreshold: NaN,
1021
+ out: { rawCode: 0, blackLevelCode: 0, digitalSaturationCode: 0, flags: 0, whiteCode: 0 } };
1022
+ }
1023
+ /** Writes the site's outputs into `f.out`; false where leanSensorSite returns undefined. */
1024
+ function flatSensorSite(f, photoE, darkE, total, x, y, index, seed) {
1025
+ const ratio = total / f.capacity, headroom = f.capacity - total;
1026
+ if (!Number.isFinite(ratio) || !Number.isFinite(headroom))
1027
+ return false;
1028
+ const pattern = f.pattern === undefined ? null : evaluateParsedFixedPatternSite(f.pattern, { x, y });
1029
+ const photoMean = pattern === null ? photoE : photoE * pattern.photoResponseGainFactor;
1030
+ const darkMean = pattern === null ? darkE : darkE * pattern.darkCurrentFactor;
1031
+ if (!Number.isFinite(photoMean) || photoMean < 0 || !Number.isFinite(darkMean) || darkMean < 0)
1032
+ return false;
1033
+ const random = chargeStream;
1034
+ random.reset(deriveNativeSiteStreamSeedUint32(seed, index, NATIVE_SITE_CHARGE_STREAM_ID));
1035
+ const photoRealized = samplePoisson(photoMean, random);
1036
+ let darkRealized;
1037
+ if (darkMean > 0 && darkMean < 30) {
1038
+ // samplePoisson's branch for 0 < mean < 30 (Knuth's product of uniforms) with its threshold Math.exp(−mean) kept while the
1039
+ // mean repeats: the same draws from the same stream. (createPoissonSampler would also do this, but it brings the
1040
+ // portable-math functions into capture-only bundles.)
1041
+ if (darkMean !== f.darkMean) {
1042
+ f.darkMean = darkMean;
1043
+ f.darkThreshold = Math.exp(-darkMean);
1044
+ }
1045
+ const threshold = f.darkThreshold;
1046
+ let product = 1, count = 0;
1047
+ do {
1048
+ count += 1;
1049
+ product *= random.next();
1050
+ } while (product > threshold);
1051
+ darkRealized = count - 1;
1052
+ }
1053
+ else
1054
+ darkRealized = samplePoisson(darkMean, random);
1055
+ const realized = photoRealized + darkRealized + 0;
1056
+ if (!Number.isFinite(realized) || realized < 0)
1057
+ return false;
1058
+ const readRandom = readNoiseStream;
1059
+ readRandom.reset(deriveNativeSiteStreamSeedUint32(seed, index, NATIVE_SITE_READ_NOISE_STREAM_ID));
1060
+ const capacity = f.capacity;
1061
+ const afterPhysical = Math.min(realized, capacity), physicalSaturation = afterPhysical < realized;
1062
+ const rmsAll = f.rms;
1063
+ let totalReadNoise = 0;
1064
+ for (let k = 0; k < rmsAll.length; k++) {
1065
+ const rms = rmsAll[k];
1066
+ totalReadNoise = totalReadNoise + (rms === 0 ? 0 : sampleDeterministicStandardNormal(readRandom) * rms);
1067
+ }
1068
+ const afterReadNoise = afterPhysical + totalReadNoise + (pattern === null ? 0 : pattern.readoutOffsetElectrons);
1069
+ const threshold = f.threshold;
1070
+ const afterPreAdc = Math.min(afterReadNoise, threshold), preAdcSaturation = afterReadNoise > threshold;
1071
+ let quantized;
1072
+ if (f.inlDefined)
1073
+ quantized = adcTransfer(f.regime, f.adcMaximumCode, afterPreAdc).quantized;
1074
+ else {
1075
+ // adcTransfer without integral non-linearity: ideal = e/G + black, plus the zero INL offset, rounded half up (Math.round + 0),
1076
+ // then with a digital gain the ADC clamp and the gain's own rounding about the black level.
1077
+ const ideal = afterPreAdc / f.gain + f.black, quantizedAdc = Math.round(ideal + 0) + 0, digitalGain = f.digitalGain;
1078
+ if (digitalGain === undefined)
1079
+ quantized = quantizedAdc;
1080
+ else {
1081
+ const adcCode = Math.max(0, Math.min(quantizedAdc, f.adcMaximumCode));
1082
+ quantized = Math.round((adcCode - f.black) * digitalGain + f.black) + 0;
1083
+ }
1084
+ }
1085
+ const digitalSaturationCode = f.digitalSaturationCode, out = f.out;
1086
+ out.rawCode = Math.max(0, Math.min(quantized, digitalSaturationCode));
1087
+ out.blackLevelCode = f.black;
1088
+ out.digitalSaturationCode = digitalSaturationCode;
1089
+ out.flags = Number(physicalSaturation) | Number(preAdcSaturation) << 1 | Number(quantized >= digitalSaturationCode) << 2;
1090
+ out.whiteCode = f.whiteCode;
1091
+ return true;
1092
+ }
1004
1093
  /** The Kahan (compensated) total that composeSensorAccumulatedCharge computes, for photo and dark charge only. */
1005
1094
  function chargeTotal(photoE, darkE) {
1006
1095
  let sum = 0, correction = 0;
@@ -1029,17 +1118,22 @@ export function createVerifiedNativeSiteReadout(parsed, result, whiteCode, photo
1029
1118
  realization.additionalComponents.length !== 0 || out.rawCode !== r.rawCode || out.blackLevelCode !== r.blackLevelCode || out.digitalSaturationCode !== r.digitalSaturationCode ||
1030
1119
  out.flags !== (Number(r.physicalScalarSaturationApplied) | Number(r.preAdcSaturationApplied) << 1 | Number(r.digitalSaturationApplied) << 2))
1031
1120
  return undefined;
1121
+ // #449: the flat stage must reproduce the verification site too.
1122
+ const flat = flatSensorStage(t), fo = flat.out;
1123
+ if (!flatSensorSite(flat, photo.expectedGeneratedElectronCount, darkE, total, x, y, index, seed) || fo.rawCode !== out.rawCode || fo.blackLevelCode !== out.blackLevelCode ||
1124
+ fo.digitalSaturationCode !== out.digitalSaturationCode || fo.flags !== out.flags || fo.whiteCode !== out.whiteCode)
1125
+ return undefined;
1032
1126
  return {
1033
1127
  compute(photoE, duration, sx, sy, siteIndex, siteSeed) {
1034
1128
  // The parse-phase predicates of the full path (claim()): finite non-negative dark and total charge.
1035
- const siteDark = t.darkRate * duration;
1129
+ const siteDark = flat.darkRate * duration;
1036
1130
  if (!Number.isFinite(photoE) || photoE < 0 || !Number.isFinite(siteDark) || siteDark < 0)
1037
1131
  return undefined;
1038
1132
  const siteTotal = chargeTotal(photoE, siteDark);
1039
1133
  if (!Number.isFinite(siteTotal) || siteTotal < 0)
1040
1134
  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 };
1135
+ // One record for every site (#449): the caller copies the five RAW fields before its next call.
1136
+ return flatSensorSite(flat, photoE, siteDark, siteTotal, sx, sy, siteIndex, siteSeed) ? fo : undefined;
1043
1137
  }
1044
1138
  };
1045
1139
  }