@photivra/engine 2.13.0 → 2.14.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 (125) hide show
  1. package/CHANGELOG.md +68 -1
  2. package/CITATION.cff +1 -1
  3. package/README.md +7 -7
  4. package/SECURITY.md +1 -1
  5. package/THIRD_PARTY.md +2 -0
  6. package/dist/camera/lens-projection.js +2 -1
  7. package/dist/camera/lens-projection.js.map +1 -1
  8. package/dist/capture/dng-import.js +1 -1
  9. package/dist/capture/dng-import.js.map +1 -1
  10. package/dist/capture/host-yield-cadence.js +1 -1
  11. package/dist/capture/host-yield-cadence.js.map +1 -1
  12. package/dist/capture/native-environment-optical-stage.js +1 -1
  13. package/dist/capture/native-environment-optical-stage.js.map +1 -1
  14. package/dist/capture/native-raw.js +1 -1
  15. package/dist/capture/native-raw.js.map +1 -1
  16. package/dist/capture/packed-sensor-raw.js +3 -3
  17. package/dist/capture/packed-sensor-raw.js.map +1 -1
  18. package/dist/capture/sensor-raw-producer.js +1 -1
  19. package/dist/capture/sensor-raw-producer.js.map +1 -1
  20. package/dist/color/capture-color.js +3 -3
  21. package/dist/color/capture-color.js.map +1 -1
  22. package/dist/color/chromatic-adaptation.js +2 -2
  23. package/dist/color/chromatic-adaptation.js.map +1 -1
  24. package/dist/color/color-matching-functions.js +1 -1
  25. package/dist/color/color-matching-functions.js.map +1 -1
  26. package/dist/color/colorimetry-math.js +2 -2
  27. package/dist/color/colorimetry-math.js.map +1 -1
  28. package/dist/color/illuminants.js +3 -3
  29. package/dist/color/illuminants.js.map +1 -1
  30. package/dist/color/rgb-color-spaces.js +1 -1
  31. package/dist/color/rgb-color-spaces.js.map +1 -1
  32. package/dist/color/sensor-color-development.js +1 -1
  33. package/dist/color/sensor-color-development.js.map +1 -1
  34. package/dist/core/record-validation.js +3 -3
  35. package/dist/core/record-validation.js.map +1 -1
  36. package/dist/core/version.d.ts +1 -1
  37. package/dist/core/version.js +1 -1
  38. package/dist/core/version.js.map +1 -1
  39. package/dist/equipment/generic-prime-lenses.js +3 -3
  40. package/dist/equipment/generic-prime-lenses.js.map +1 -1
  41. package/dist/equipment/generic-tier-assets.d.ts +23 -1
  42. package/dist/equipment/generic-tier-assets.js +58 -30
  43. package/dist/equipment/generic-tier-assets.js.map +1 -1
  44. package/dist/equipment/generic-tier-presets.d.ts +6 -3
  45. package/dist/equipment/generic-tier-presets.js +26 -13
  46. package/dist/equipment/generic-tier-presets.js.map +1 -1
  47. package/dist/exposure/auto-iso-slowest-shutter.js +2 -1
  48. package/dist/exposure/auto-iso-slowest-shutter.js.map +1 -1
  49. package/dist/exposure/flash-ttl.js +1 -1
  50. package/dist/exposure/flash-ttl.js.map +1 -1
  51. package/dist/exposure/flash.js +1 -1
  52. package/dist/exposure/flash.js.map +1 -1
  53. package/dist/facade/chart-material-layers.js +2 -2
  54. package/dist/facade/chart-material-layers.js.map +1 -1
  55. package/dist/facade/chart-radiance-batch.js +1 -1
  56. package/dist/facade/chart-radiance-batch.js.map +1 -1
  57. package/dist/facade/chart-scenes.d.ts +45 -8
  58. package/dist/facade/chart-scenes.js +101 -44
  59. package/dist/facade/chart-scenes.js.map +1 -1
  60. package/dist/facade/color-patches.d.ts +129 -0
  61. package/dist/facade/color-patches.js +209 -0
  62. package/dist/facade/color-patches.js.map +1 -0
  63. package/dist/facade/display-view-gpu.js +1 -1
  64. package/dist/facade/display-view-gpu.js.map +1 -1
  65. package/dist/facade/scene-radiance.js +2 -2
  66. package/dist/facade/scene-radiance.js.map +1 -1
  67. package/dist/facade/simulate-capture.d.ts +47 -3
  68. package/dist/facade/simulate-capture.js +71 -22
  69. package/dist/facade/simulate-capture.js.map +1 -1
  70. package/dist/facade/simulate-display-view.js +2 -2
  71. package/dist/facade/simulate-display-view.js.map +1 -1
  72. package/dist/gpu/chart-composite-gpu.js +2 -2
  73. package/dist/gpu/chart-composite-gpu.js.map +1 -1
  74. package/dist/gpu/display-view-composite-material-module.js +1 -1
  75. package/dist/gpu/display-view-composite-material-module.js.map +1 -1
  76. package/dist/gpu/image-kernel-execution.js +2 -2
  77. package/dist/gpu/image-kernel-execution.js.map +1 -1
  78. package/dist/gpu/temporal-sum-gpu.js +23 -23
  79. package/dist/gpu/webgpu-runtime.js +1 -1
  80. package/dist/gpu/webgpu-runtime.js.map +1 -1
  81. package/dist/gpu/wgsl-chart-composite-kernel.js +318 -318
  82. package/dist/gpu/wgsl-chart-composite-kernel.js.map +1 -1
  83. package/dist/gpu/wgsl-chart-kernel.js +117 -117
  84. package/dist/gpu/wgsl-chart-kernel.js.map +1 -1
  85. package/dist/gpu/wgsl-display-view-quality-kernel.js +505 -505
  86. package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
  87. package/dist/gpu/wgsl-display-view-speed-kernel.js +248 -248
  88. package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
  89. package/dist/gpu/wgsl-kernels.js +203 -203
  90. package/dist/gpu/wgsl-kernels.js.map +1 -1
  91. package/dist/gpu/wgsl-sensor-kernel.js +302 -302
  92. package/dist/gpu/wgsl-sensor-kernel.js.map +1 -1
  93. package/dist/index.d.ts +5 -3
  94. package/dist/index.js +4 -2
  95. package/dist/index.js.map +1 -1
  96. package/dist/motion/temporal-exposure-sum.js +1 -1
  97. package/dist/motion/temporal-exposure-sum.js.map +1 -1
  98. package/dist/optics/diffraction-psf-profile.js +4 -4
  99. package/dist/optics/diffraction-psf-profile.js.map +1 -1
  100. package/dist/optics/front-of-lens-filter.js +1 -1
  101. package/dist/optics/front-of-lens-filter.js.map +1 -1
  102. package/dist/optics/irradiance-convolution.js +1 -1
  103. package/dist/optics/irradiance-convolution.js.map +1 -1
  104. package/dist/output/native-capture-sdr.js +1 -1
  105. package/dist/output/native-capture-sdr.js.map +1 -1
  106. package/dist/output/png-encoder.js +2 -2
  107. package/dist/output/png-encoder.js.map +1 -1
  108. package/dist/output/print-jpeg.js +1 -1
  109. package/dist/output/print-jpeg.js.map +1 -1
  110. package/dist/sensor/fixed-pattern-noise.js +1 -1
  111. package/dist/sensor/fixed-pattern-noise.js.map +1 -1
  112. package/dist/sensor/readout-exposure-linkage.js +1 -1
  113. package/dist/sensor/readout-exposure-linkage.js.map +1 -1
  114. package/docs/API_REFERENCE.md +29 -1
  115. package/docs/QUICK_START.md +40 -5
  116. package/docs/USAGE.md +35 -1
  117. package/docs/api/core-version.md +1 -1
  118. package/docs/api/equipment-generic-tier-assets.md +41 -1
  119. package/docs/api/equipment-generic-tier-presets.md +11 -3
  120. package/docs/api/exports.json +107 -2
  121. package/docs/api/facade-chart-scenes.md +36 -5
  122. package/docs/api/facade-color-patches.md +150 -0
  123. package/docs/api/facade-simulate-capture.md +52 -2
  124. package/llms.txt +1 -0
  125. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"fixed-pattern-noise.js","sourceRoot":"","sources":["../../src/sensor/fixed-pattern-noise.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Fixed-pattern noise (FPN): the part of sensor noise that stays the same from shot to shot for one\n * physical sensor. It covers photo-response non-uniformity (PRNU), dark-signal non-uniformity (DSNU),\n * hot pixels and row and column readout offsets, as defined in EMVA Standard 1288 Release 4.0 (spatial\n * non-uniformity of the photo response and of the dark signal).\n *\n * The pattern is deterministic. It is seeded from an explicit sensor-instance seed and absolute\n * colour-sampling site coordinates, never from the per-capture noise seed, so the same sensor shows\n * the same pattern in every capture.\n *\n * Application points in the readout chain (see raw-readout.ts):\n * - PRNU multiplies the expected photo charge before photo shot-noise sampling;\n * - DSNU and hot pixels multiply the population-mean dark-current expectation before dark shot-noise\n * sampling, so their excess charge scales with exposure duration and with the declared\n * temperature model of the dark-current profile;\n * - row and column offsets are input-referred electron offsets added with read noise, before the\n * pre-ADC threshold.\n *\n * The dark-current model itself stays a population mean; this module is a separate spatial model that\n * must be attached explicitly. Values are declared by the caller (or are synthetic preset values).\n * Nothing here is a measured calibration.\n * @see docs/MOTION_AND_SIGNAL.md for equations, coordinate/unit conventions, blockers and support\n * limits.\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { parseEvidenceList, type EvidenceBackedFact, type EvidenceProvenance } from \"../core/evidence-provenance.js\";\nimport { requireAllowlistedRecord, requirePublicOpaqueId, requireInputKeys } from \"../core/record-validation.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport {\n createDeterministicUniformStream,\n deriveSensorFixedPatternSeedUint32,\n sampleDeterministicStandardNormal,\n SENSOR_FIXED_PATTERN_COLUMN_STREAM_ID,\n SENSOR_FIXED_PATTERN_DSNU_STREAM_ID,\n SENSOR_FIXED_PATTERN_HOT_PIXEL_STREAM_ID,\n SENSOR_FIXED_PATTERN_PRNU_STREAM_ID,\n SENSOR_FIXED_PATTERN_ROW_STREAM_ID\n} from \"./stochastic-seed.js\";\n\nexport const SENSOR_FIXED_PATTERN_NOISE_PROFILE_SCHEMA_VERSION = \"0.1.0\" as const;\n/** Versioned identity of the pattern-generation equations and seed schedule. */\nexport const SENSOR_FIXED_PATTERN_NOISE_MODEL = Object.freeze({ id: \"photivra-sensor-fixed-pattern\", version: \"1.0.0\" } as const);\n\n/**\n * Explicit fixed-pattern parameters for one sensor instance. Every component is required; a zero\n * standard deviation, fraction or RMS disables that component.\n */\nexport interface SensorFixedPatternNoiseProfile {\n schemaVersion: typeof SENSOR_FIXED_PATTERN_NOISE_PROFILE_SCHEMA_VERSION;\n profileId: string;\n /** The colour-sampling grid whose absolute site coordinates index the pattern. */\n colorSamplingProfileId: string;\n scientificStatus: \"approximation\";\n /** Identifies the physical sensor. Independent of every per-capture noise seed. */\n sensorInstanceSeedUint32: number;\n /** EMVA 1288 PRNU: spatial standard deviation of the photo response divided by its mean. */\n photoResponseNonUniformity: { relativeStandardDeviation: EvidenceBackedFact<number> };\n /** Spatial standard deviation of the dark-current rate divided by the population-mean rate. */\n darkSignalNonUniformity: { relativeStandardDeviation: EvidenceBackedFact<number> };\n /** Fraction of sites whose dark-current rate is the population mean times the multiplier. */\n hotPixels: { siteFraction: EvidenceBackedFact<number>; darkCurrentMultiplier: EvidenceBackedFact<number> };\n /** Fixed readout offset per row, in input-referred electrons, normally distributed with zero mean. */\n rowOffset: { rmsElectrons: EvidenceBackedFact<number> };\n /** Fixed readout offset per column, in input-referred electrons, normally distributed with zero mean. */\n columnOffset: { rmsElectrons: EvidenceBackedFact<number> };\n patternDistribution: \"normal-factors-truncated-at-zero\";\n evidence: readonly EvidenceProvenance[];\n limitations: readonly string[];\n}\n\n/** The deterministic pattern values of one colour-sampling site. */\nexport interface SensorFixedPatternSiteValues {\n profileId: string;\n modelId: typeof SENSOR_FIXED_PATTERN_NOISE_MODEL.id;\n modelVersion: typeof SENSOR_FIXED_PATTERN_NOISE_MODEL.version;\n site: { x: number; y: number };\n /** Multiplies the expected photo charge (1 = no PRNU); never negative. */\n photoResponseGainFactor: number;\n /** Multiplies the population-mean dark expectation (1 = no DSNU); never negative. */\n darkCurrentFactor: number;\n hotPixel: boolean;\n rowOffsetElectrons: number;\n columnOffsetElectrons: number;\n /** rowOffsetElectrons + columnOffsetElectrons, added at readout. */\n readoutOffsetElectrons: number;\n}\n\nconst FACT_KEYS = [\"value\", \"evidence\"];\n\nfunction fact(value: unknown, path: string, maximum: number, minimum = 0): EvidenceBackedFact<number> {\n const record = requireAllowlistedRecord(value, FACT_KEYS, path + \" must be an evidence-backed fact.\");\n const v = record.value;\n if (typeof v !== \"number\" || !Number.isFinite(v) || v < minimum || v > maximum) {\n throw new InvalidConfigurationError(path + \".value must be finite from \" + minimum + \" through \" + maximum + \".\");\n }\n return { value: v, evidence: parseEvidenceList(record.evidence, path + \".evidence\") };\n}\n\nfunction limitations(value: unknown): readonly string[] {\n if (!Array.isArray(value) || value.some((entry) => typeof entry !== \"string\" || entry.trim().length === 0)) {\n throw new InvalidConfigurationError(\"sensorFixedPatternNoise.limitations must be an array of non-empty strings.\");\n }\n if (new Set(value).size !== value.length) {\n throw new InvalidConfigurationError(\"sensorFixedPatternNoise.limitations must not contain duplicates.\");\n }\n return value.map((entry: string) => entry.trim());\n}\n\n/**\n * Validates an untrusted fixed-pattern noise profile. Unknown keys, out-of-range parameters and\n * missing evidence are rejected. The PRNU and DSNU relative standard deviations are limited to 0.5\n * and 5, and the hot-pixel multiplier to 1–1e6.\n * @param value - Untrusted data. TypeScript types alone are not validation.\n * @returns The validated profile.\n * @see docs/MOTION_AND_SIGNAL.md for the model and its sources.\n */\nexport function parseSensorFixedPatternNoiseProfile(value: unknown): SensorFixedPatternNoiseProfile {\n const r = requireAllowlistedRecord(value, [\"schemaVersion\", \"profileId\", \"colorSamplingProfileId\", \"scientificStatus\",\n \"sensorInstanceSeedUint32\", \"photoResponseNonUniformity\", \"darkSignalNonUniformity\", \"hotPixels\", \"rowOffset\", \"columnOffset\",\n \"patternDistribution\", \"evidence\", \"limitations\"], \"Invalid sensor fixed-pattern noise profile fields.\");\n if (r.schemaVersion !== SENSOR_FIXED_PATTERN_NOISE_PROFILE_SCHEMA_VERSION) {\n throw new InvalidConfigurationError('sensorFixedPatternNoise.schemaVersion must be \"' + SENSOR_FIXED_PATTERN_NOISE_PROFILE_SCHEMA_VERSION + '\".');\n }\n if (r.scientificStatus !== \"approximation\") {\n throw new InvalidConfigurationError('sensorFixedPatternNoise.scientificStatus must be \"approximation\".');\n }\n if (r.patternDistribution !== \"normal-factors-truncated-at-zero\") {\n throw new InvalidConfigurationError('sensorFixedPatternNoise.patternDistribution must be \"normal-factors-truncated-at-zero\".');\n }\n const seed = r.sensorInstanceSeedUint32;\n if (typeof seed !== \"number\" || !Number.isSafeInteger(seed) || seed < 0 || seed > 0xffff_ffff) {\n throw new InvalidConfigurationError(\"sensorFixedPatternNoise.sensorInstanceSeedUint32 must be an unsigned 32-bit integer.\");\n }\n const prnu = requireAllowlistedRecord(r.photoResponseNonUniformity, [\"relativeStandardDeviation\"], \"Invalid PRNU fields.\");\n const dsnu = requireAllowlistedRecord(r.darkSignalNonUniformity, [\"relativeStandardDeviation\"], \"Invalid DSNU fields.\");\n const hot = requireAllowlistedRecord(r.hotPixels, [\"siteFraction\", \"darkCurrentMultiplier\"], \"Invalid hot-pixel fields.\");\n const row = requireAllowlistedRecord(r.rowOffset, [\"rmsElectrons\"], \"Invalid row-offset fields.\");\n const column = requireAllowlistedRecord(r.columnOffset, [\"rmsElectrons\"], \"Invalid column-offset fields.\");\n return {\n schemaVersion: SENSOR_FIXED_PATTERN_NOISE_PROFILE_SCHEMA_VERSION,\n profileId: requirePublicOpaqueId(r.profileId, \"sensorFixedPatternNoise.profileId must be a public opaque identifier.\"),\n colorSamplingProfileId: requirePublicOpaqueId(r.colorSamplingProfileId, \"sensorFixedPatternNoise.colorSamplingProfileId must be a public opaque identifier.\"),\n scientificStatus: \"approximation\",\n sensorInstanceSeedUint32: seed,\n photoResponseNonUniformity: { relativeStandardDeviation: fact(prnu.relativeStandardDeviation, \"photoResponseNonUniformity.relativeStandardDeviation\", 0.5) },\n darkSignalNonUniformity: { relativeStandardDeviation: fact(dsnu.relativeStandardDeviation, \"darkSignalNonUniformity.relativeStandardDeviation\", 5) },\n hotPixels: { siteFraction: fact(hot.siteFraction, \"hotPixels.siteFraction\", 1),\n darkCurrentMultiplier: fact(hot.darkCurrentMultiplier, \"hotPixels.darkCurrentMultiplier\", 1e6, 1) },\n rowOffset: { rmsElectrons: fact(row.rmsElectrons, \"rowOffset.rmsElectrons\", 1e6) },\n columnOffset: { rmsElectrons: fact(column.rmsElectrons, \"columnOffset.rmsElectrons\", 1e6) },\n patternDistribution: \"normal-factors-truncated-at-zero\",\n evidence: parseEvidenceList(r.evidence, \"sensorFixedPatternNoise.evidence\"),\n limitations: limitations(r.limitations)\n };\n}\n\nfunction normal(seed: number, x: number, y: number, stream: number): number {\n return sampleDeterministicStandardNormal(createDeterministicUniformStream(deriveSensorFixedPatternSeedUint32(seed, x, y, stream)));\n}\n\n/**\n * Computes the fixed-pattern values of one absolute colour-sampling site.\n *\n * - PRNU gain g = max(0, 1 + σ_PRNU·z₁);\n * - hot pixel when u < f_hot, then dark factor d = m_hot; otherwise d = max(0, 1 + σ_DSNU·z₂);\n * - row offset σ_row·z(row y), column offset σ_col·z(column x), in input-referred electrons.\n *\n * Each z is a standard normal value and u a uniform value, each from its own stream hashed with\n * MurmurHash3 from (sensorInstanceSeed, x, y, stream). Row values use x = 0 and column values y = 0,\n * so a whole row or column shares one value. Cutting negative factors off at zero shifts the mean\n * factor by less than 1e-6 for σ ≤ 0.2.\n * @param input - A parsed or untrusted profile plus a nonnegative integer site.\n * @returns The site's fixed-pattern values.\n */\nexport function calculateSensorFixedPatternSite(input: {\n profile: SensorFixedPatternNoiseProfile; site: { x: number; y: number };\n}): SensorFixedPatternSiteValues {\n requireInputKeys(input, \"calculateSensorFixedPatternSite\", [\"profile\", \"site\"]);\n const profile = parseSensorFixedPatternNoiseProfile(input.profile);\n return evaluateParsedFixedPatternSite(profile, input.site);\n}\n\n/** Internal: the PRNU gain, dark-current factor and hot-pixel state of one site (no readout offsets). */\nexport function evaluateFixedPatternSiteFactors(profile: SensorFixedPatternNoiseProfile, x: number, y: number):\n { photoResponseGainFactor: number; darkCurrentFactor: number; hotPixel: boolean } {\n const seed = profile.sensorInstanceSeedUint32;\n const prnuSd = profile.photoResponseNonUniformity.relativeStandardDeviation.value;\n const dsnuSd = profile.darkSignalNonUniformity.relativeStandardDeviation.value;\n const hotFraction = profile.hotPixels.siteFraction.value;\n const photoResponseGainFactor = prnuSd === 0 ? 1 : Math.max(0, 1 + prnuSd * normal(seed, x, y, SENSOR_FIXED_PATTERN_PRNU_STREAM_ID));\n const hotPixel = hotFraction > 0 &&\n createDeterministicUniformStream(deriveSensorFixedPatternSeedUint32(seed, x, y, SENSOR_FIXED_PATTERN_HOT_PIXEL_STREAM_ID)).next() < hotFraction;\n const darkCurrentFactor = hotPixel ? profile.hotPixels.darkCurrentMultiplier.value :\n dsnuSd === 0 ? 1 : Math.max(0, 1 + dsnuSd * normal(seed, x, y, SENSOR_FIXED_PATTERN_DSNU_STREAM_ID));\n return { photoResponseGainFactor, darkCurrentFactor, hotPixel };\n}\n\n/** Internal: the fixed readout offset of row `y`, in input-referred electrons. */\nexport function evaluateFixedPatternRowOffset(profile: SensorFixedPatternNoiseProfile, y: number): number {\n const rms = profile.rowOffset.rmsElectrons.value;\n return rms === 0 ? 0 : rms * normal(profile.sensorInstanceSeedUint32, 0, y, SENSOR_FIXED_PATTERN_ROW_STREAM_ID);\n}\n\n/** Internal: the fixed readout offset of column `x`, in input-referred electrons. */\nexport function evaluateFixedPatternColumnOffset(profile: SensorFixedPatternNoiseProfile, x: number): number {\n const rms = profile.columnOffset.rmsElectrons.value;\n return rms === 0 ? 0 : rms * normal(profile.sensorInstanceSeedUint32, x, 0, SENSOR_FIXED_PATTERN_COLUMN_STREAM_ID);\n}\n\n/** Internal: the same calculation for an already parsed profile. */\nexport function evaluateParsedFixedPatternSite(profile: SensorFixedPatternNoiseProfile,\n site: { x: number; y: number }): SensorFixedPatternSiteValues {\n const { x, y } = site ?? {};\n if (!Number.isSafeInteger(x) || !Number.isSafeInteger(y) || x < 0 || y < 0 || x > 0xffff_ffff || y > 0xffff_ffff) {\n throw new InvalidScientificInputError(\"Fixed-pattern site coordinates must be nonnegative uint32 integers.\");\n }\n const { photoResponseGainFactor, darkCurrentFactor, hotPixel } = evaluateFixedPatternSiteFactors(profile, x, y);\n const rowOffsetElectrons = evaluateFixedPatternRowOffset(profile, y);\n const columnOffsetElectrons = evaluateFixedPatternColumnOffset(profile, x);\n return {\n profileId: profile.profileId,\n modelId: SENSOR_FIXED_PATTERN_NOISE_MODEL.id,\n modelVersion: SENSOR_FIXED_PATTERN_NOISE_MODEL.version,\n site: { x, y },\n photoResponseGainFactor,\n darkCurrentFactor,\n hotPixel,\n rowOffsetElectrons,\n columnOffsetElectrons,\n readoutOffsetElectrons: rowOffsetElectrons + columnOffsetElectrons\n };\n}\n"]}
1
+ {"version":3,"file":"fixed-pattern-noise.js","sourceRoot":"","sources":["../../src/sensor/fixed-pattern-noise.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Fixed-pattern noise (FPN): the part of sensor noise that stays the same from shot to shot for one\n * physical sensor. It covers photo-response non-uniformity (PRNU), dark-signal non-uniformity (DSNU),\n * hot pixels and row and column readout offsets, as defined in EMVA Standard 1288 Release 4.0 (spatial\n * non-uniformity of the photo response and of the dark signal).\n *\n * The pattern is deterministic. It is seeded from an explicit sensor-instance seed and absolute\n * colour-sampling site coordinates, never from the per-capture noise seed, so the same sensor shows\n * the same pattern in every capture.\n *\n * Application points in the readout chain (see raw-readout.ts):\n * - PRNU multiplies the expected photo charge before photo shot-noise sampling;\n * - DSNU and hot pixels multiply the population-mean dark-current expectation before dark shot-noise\n * sampling, so their excess charge scales with exposure duration and with the declared\n * temperature model of the dark-current profile;\n * - row and column offsets are input-referred electron offsets added with read noise, before the\n * pre-ADC threshold.\n *\n * The dark-current model itself stays a population mean; this module is a separate spatial model that\n * must be attached explicitly. Values are declared by the caller (or are synthetic preset values).\n * Nothing here is a measured calibration.\n * @see docs/MOTION_AND_SIGNAL.md for equations, coordinate/unit conventions, blockers and support\n * limits.\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { parseEvidenceList, type EvidenceBackedFact, type EvidenceProvenance } from \"../core/evidence-provenance.js\";\nimport { requireAllowlistedRecord, requirePublicOpaqueId, requireInputKeys } from \"../core/record-validation.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport {\n createDeterministicUniformStream,\n deriveSensorFixedPatternSeedUint32,\n sampleDeterministicStandardNormal,\n SENSOR_FIXED_PATTERN_COLUMN_STREAM_ID,\n SENSOR_FIXED_PATTERN_DSNU_STREAM_ID,\n SENSOR_FIXED_PATTERN_HOT_PIXEL_STREAM_ID,\n SENSOR_FIXED_PATTERN_PRNU_STREAM_ID,\n SENSOR_FIXED_PATTERN_ROW_STREAM_ID\n} from \"./stochastic-seed.js\";\n\nexport const SENSOR_FIXED_PATTERN_NOISE_PROFILE_SCHEMA_VERSION = \"0.1.0\" as const;\n/** Versioned identity of the pattern-generation equations and seed schedule. */\nexport const SENSOR_FIXED_PATTERN_NOISE_MODEL = /* @__PURE__ */ Object.freeze({ id: \"photivra-sensor-fixed-pattern\", version: \"1.0.0\" } as const);\n\n/**\n * Explicit fixed-pattern parameters for one sensor instance. Every component is required; a zero\n * standard deviation, fraction or RMS disables that component.\n */\nexport interface SensorFixedPatternNoiseProfile {\n schemaVersion: typeof SENSOR_FIXED_PATTERN_NOISE_PROFILE_SCHEMA_VERSION;\n profileId: string;\n /** The colour-sampling grid whose absolute site coordinates index the pattern. */\n colorSamplingProfileId: string;\n scientificStatus: \"approximation\";\n /** Identifies the physical sensor. Independent of every per-capture noise seed. */\n sensorInstanceSeedUint32: number;\n /** EMVA 1288 PRNU: spatial standard deviation of the photo response divided by its mean. */\n photoResponseNonUniformity: { relativeStandardDeviation: EvidenceBackedFact<number> };\n /** Spatial standard deviation of the dark-current rate divided by the population-mean rate. */\n darkSignalNonUniformity: { relativeStandardDeviation: EvidenceBackedFact<number> };\n /** Fraction of sites whose dark-current rate is the population mean times the multiplier. */\n hotPixels: { siteFraction: EvidenceBackedFact<number>; darkCurrentMultiplier: EvidenceBackedFact<number> };\n /** Fixed readout offset per row, in input-referred electrons, normally distributed with zero mean. */\n rowOffset: { rmsElectrons: EvidenceBackedFact<number> };\n /** Fixed readout offset per column, in input-referred electrons, normally distributed with zero mean. */\n columnOffset: { rmsElectrons: EvidenceBackedFact<number> };\n patternDistribution: \"normal-factors-truncated-at-zero\";\n evidence: readonly EvidenceProvenance[];\n limitations: readonly string[];\n}\n\n/** The deterministic pattern values of one colour-sampling site. */\nexport interface SensorFixedPatternSiteValues {\n profileId: string;\n modelId: typeof SENSOR_FIXED_PATTERN_NOISE_MODEL.id;\n modelVersion: typeof SENSOR_FIXED_PATTERN_NOISE_MODEL.version;\n site: { x: number; y: number };\n /** Multiplies the expected photo charge (1 = no PRNU); never negative. */\n photoResponseGainFactor: number;\n /** Multiplies the population-mean dark expectation (1 = no DSNU); never negative. */\n darkCurrentFactor: number;\n hotPixel: boolean;\n rowOffsetElectrons: number;\n columnOffsetElectrons: number;\n /** rowOffsetElectrons + columnOffsetElectrons, added at readout. */\n readoutOffsetElectrons: number;\n}\n\nconst FACT_KEYS = [\"value\", \"evidence\"];\n\nfunction fact(value: unknown, path: string, maximum: number, minimum = 0): EvidenceBackedFact<number> {\n const record = requireAllowlistedRecord(value, FACT_KEYS, path + \" must be an evidence-backed fact.\");\n const v = record.value;\n if (typeof v !== \"number\" || !Number.isFinite(v) || v < minimum || v > maximum) {\n throw new InvalidConfigurationError(path + \".value must be finite from \" + minimum + \" through \" + maximum + \".\");\n }\n return { value: v, evidence: parseEvidenceList(record.evidence, path + \".evidence\") };\n}\n\nfunction limitations(value: unknown): readonly string[] {\n if (!Array.isArray(value) || value.some((entry) => typeof entry !== \"string\" || entry.trim().length === 0)) {\n throw new InvalidConfigurationError(\"sensorFixedPatternNoise.limitations must be an array of non-empty strings.\");\n }\n if (new Set(value).size !== value.length) {\n throw new InvalidConfigurationError(\"sensorFixedPatternNoise.limitations must not contain duplicates.\");\n }\n return value.map((entry: string) => entry.trim());\n}\n\n/**\n * Validates an untrusted fixed-pattern noise profile. Unknown keys, out-of-range parameters and\n * missing evidence are rejected. The PRNU and DSNU relative standard deviations are limited to 0.5\n * and 5, and the hot-pixel multiplier to 1–1e6.\n * @param value - Untrusted data. TypeScript types alone are not validation.\n * @returns The validated profile.\n * @see docs/MOTION_AND_SIGNAL.md for the model and its sources.\n */\nexport function parseSensorFixedPatternNoiseProfile(value: unknown): SensorFixedPatternNoiseProfile {\n const r = requireAllowlistedRecord(value, [\"schemaVersion\", \"profileId\", \"colorSamplingProfileId\", \"scientificStatus\",\n \"sensorInstanceSeedUint32\", \"photoResponseNonUniformity\", \"darkSignalNonUniformity\", \"hotPixels\", \"rowOffset\", \"columnOffset\",\n \"patternDistribution\", \"evidence\", \"limitations\"], \"Invalid sensor fixed-pattern noise profile fields.\");\n if (r.schemaVersion !== SENSOR_FIXED_PATTERN_NOISE_PROFILE_SCHEMA_VERSION) {\n throw new InvalidConfigurationError('sensorFixedPatternNoise.schemaVersion must be \"' + SENSOR_FIXED_PATTERN_NOISE_PROFILE_SCHEMA_VERSION + '\".');\n }\n if (r.scientificStatus !== \"approximation\") {\n throw new InvalidConfigurationError('sensorFixedPatternNoise.scientificStatus must be \"approximation\".');\n }\n if (r.patternDistribution !== \"normal-factors-truncated-at-zero\") {\n throw new InvalidConfigurationError('sensorFixedPatternNoise.patternDistribution must be \"normal-factors-truncated-at-zero\".');\n }\n const seed = r.sensorInstanceSeedUint32;\n if (typeof seed !== \"number\" || !Number.isSafeInteger(seed) || seed < 0 || seed > 0xffff_ffff) {\n throw new InvalidConfigurationError(\"sensorFixedPatternNoise.sensorInstanceSeedUint32 must be an unsigned 32-bit integer.\");\n }\n const prnu = requireAllowlistedRecord(r.photoResponseNonUniformity, [\"relativeStandardDeviation\"], \"Invalid PRNU fields.\");\n const dsnu = requireAllowlistedRecord(r.darkSignalNonUniformity, [\"relativeStandardDeviation\"], \"Invalid DSNU fields.\");\n const hot = requireAllowlistedRecord(r.hotPixels, [\"siteFraction\", \"darkCurrentMultiplier\"], \"Invalid hot-pixel fields.\");\n const row = requireAllowlistedRecord(r.rowOffset, [\"rmsElectrons\"], \"Invalid row-offset fields.\");\n const column = requireAllowlistedRecord(r.columnOffset, [\"rmsElectrons\"], \"Invalid column-offset fields.\");\n return {\n schemaVersion: SENSOR_FIXED_PATTERN_NOISE_PROFILE_SCHEMA_VERSION,\n profileId: requirePublicOpaqueId(r.profileId, \"sensorFixedPatternNoise.profileId must be a public opaque identifier.\"),\n colorSamplingProfileId: requirePublicOpaqueId(r.colorSamplingProfileId, \"sensorFixedPatternNoise.colorSamplingProfileId must be a public opaque identifier.\"),\n scientificStatus: \"approximation\",\n sensorInstanceSeedUint32: seed,\n photoResponseNonUniformity: { relativeStandardDeviation: fact(prnu.relativeStandardDeviation, \"photoResponseNonUniformity.relativeStandardDeviation\", 0.5) },\n darkSignalNonUniformity: { relativeStandardDeviation: fact(dsnu.relativeStandardDeviation, \"darkSignalNonUniformity.relativeStandardDeviation\", 5) },\n hotPixels: { siteFraction: fact(hot.siteFraction, \"hotPixels.siteFraction\", 1),\n darkCurrentMultiplier: fact(hot.darkCurrentMultiplier, \"hotPixels.darkCurrentMultiplier\", 1e6, 1) },\n rowOffset: { rmsElectrons: fact(row.rmsElectrons, \"rowOffset.rmsElectrons\", 1e6) },\n columnOffset: { rmsElectrons: fact(column.rmsElectrons, \"columnOffset.rmsElectrons\", 1e6) },\n patternDistribution: \"normal-factors-truncated-at-zero\",\n evidence: parseEvidenceList(r.evidence, \"sensorFixedPatternNoise.evidence\"),\n limitations: limitations(r.limitations)\n };\n}\n\nfunction normal(seed: number, x: number, y: number, stream: number): number {\n return sampleDeterministicStandardNormal(createDeterministicUniformStream(deriveSensorFixedPatternSeedUint32(seed, x, y, stream)));\n}\n\n/**\n * Computes the fixed-pattern values of one absolute colour-sampling site.\n *\n * - PRNU gain g = max(0, 1 + σ_PRNU·z₁);\n * - hot pixel when u < f_hot, then dark factor d = m_hot; otherwise d = max(0, 1 + σ_DSNU·z₂);\n * - row offset σ_row·z(row y), column offset σ_col·z(column x), in input-referred electrons.\n *\n * Each z is a standard normal value and u a uniform value, each from its own stream hashed with\n * MurmurHash3 from (sensorInstanceSeed, x, y, stream). Row values use x = 0 and column values y = 0,\n * so a whole row or column shares one value. Cutting negative factors off at zero shifts the mean\n * factor by less than 1e-6 for σ ≤ 0.2.\n * @param input - A parsed or untrusted profile plus a nonnegative integer site.\n * @returns The site's fixed-pattern values.\n */\nexport function calculateSensorFixedPatternSite(input: {\n profile: SensorFixedPatternNoiseProfile; site: { x: number; y: number };\n}): SensorFixedPatternSiteValues {\n requireInputKeys(input, \"calculateSensorFixedPatternSite\", [\"profile\", \"site\"]);\n const profile = parseSensorFixedPatternNoiseProfile(input.profile);\n return evaluateParsedFixedPatternSite(profile, input.site);\n}\n\n/** Internal: the PRNU gain, dark-current factor and hot-pixel state of one site (no readout offsets). */\nexport function evaluateFixedPatternSiteFactors(profile: SensorFixedPatternNoiseProfile, x: number, y: number):\n { photoResponseGainFactor: number; darkCurrentFactor: number; hotPixel: boolean } {\n const seed = profile.sensorInstanceSeedUint32;\n const prnuSd = profile.photoResponseNonUniformity.relativeStandardDeviation.value;\n const dsnuSd = profile.darkSignalNonUniformity.relativeStandardDeviation.value;\n const hotFraction = profile.hotPixels.siteFraction.value;\n const photoResponseGainFactor = prnuSd === 0 ? 1 : Math.max(0, 1 + prnuSd * normal(seed, x, y, SENSOR_FIXED_PATTERN_PRNU_STREAM_ID));\n const hotPixel = hotFraction > 0 &&\n createDeterministicUniformStream(deriveSensorFixedPatternSeedUint32(seed, x, y, SENSOR_FIXED_PATTERN_HOT_PIXEL_STREAM_ID)).next() < hotFraction;\n const darkCurrentFactor = hotPixel ? profile.hotPixels.darkCurrentMultiplier.value :\n dsnuSd === 0 ? 1 : Math.max(0, 1 + dsnuSd * normal(seed, x, y, SENSOR_FIXED_PATTERN_DSNU_STREAM_ID));\n return { photoResponseGainFactor, darkCurrentFactor, hotPixel };\n}\n\n/** Internal: the fixed readout offset of row `y`, in input-referred electrons. */\nexport function evaluateFixedPatternRowOffset(profile: SensorFixedPatternNoiseProfile, y: number): number {\n const rms = profile.rowOffset.rmsElectrons.value;\n return rms === 0 ? 0 : rms * normal(profile.sensorInstanceSeedUint32, 0, y, SENSOR_FIXED_PATTERN_ROW_STREAM_ID);\n}\n\n/** Internal: the fixed readout offset of column `x`, in input-referred electrons. */\nexport function evaluateFixedPatternColumnOffset(profile: SensorFixedPatternNoiseProfile, x: number): number {\n const rms = profile.columnOffset.rmsElectrons.value;\n return rms === 0 ? 0 : rms * normal(profile.sensorInstanceSeedUint32, x, 0, SENSOR_FIXED_PATTERN_COLUMN_STREAM_ID);\n}\n\n/** Internal: the same calculation for an already parsed profile. */\nexport function evaluateParsedFixedPatternSite(profile: SensorFixedPatternNoiseProfile,\n site: { x: number; y: number }): SensorFixedPatternSiteValues {\n const { x, y } = site ?? {};\n if (!Number.isSafeInteger(x) || !Number.isSafeInteger(y) || x < 0 || y < 0 || x > 0xffff_ffff || y > 0xffff_ffff) {\n throw new InvalidScientificInputError(\"Fixed-pattern site coordinates must be nonnegative uint32 integers.\");\n }\n const { photoResponseGainFactor, darkCurrentFactor, hotPixel } = evaluateFixedPatternSiteFactors(profile, x, y);\n const rowOffsetElectrons = evaluateFixedPatternRowOffset(profile, y);\n const columnOffsetElectrons = evaluateFixedPatternColumnOffset(profile, x);\n return {\n profileId: profile.profileId,\n modelId: SENSOR_FIXED_PATTERN_NOISE_MODEL.id,\n modelVersion: SENSOR_FIXED_PATTERN_NOISE_MODEL.version,\n site: { x, y },\n photoResponseGainFactor,\n darkCurrentFactor,\n hotPixel,\n rowOffsetElectrons,\n columnOffsetElectrons,\n readoutOffsetElectrons: rowOffsetElectrons + columnOffsetElectrons\n };\n}\n"]}
@@ -223,7 +223,7 @@ export function assessReadoutExposureTimingLinkage(input) {
223
223
  ]);
224
224
  }
225
225
  /** Model identity of the electronic rolling-shutter (line reset → line read) exposure schedule. */
226
- export const ELECTRONIC_ROLLING_SHUTTER_MODEL = Object.freeze({
226
+ export const ELECTRONIC_ROLLING_SHUTTER_MODEL = /* @__PURE__ */ Object.freeze({
227
227
  id: "electronic-rolling-shutter-line-reset-read",
228
228
  version: "1.0.0"
229
229
  });
@@ -1 +1 @@
1
- {"version":3,"file":"readout-exposure-linkage.js","sourceRoot":"","sources":["../../src/sensor/readout-exposure-linkage.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Links rolling sensor readout to electronic shutter scans, and derives the exposure schedule of a\n * fully electronic rolling shutter.\n *\n * A declared link says that the readout scan and one or more exposure-boundary scans move across the\n * sensor in the same order (or in reverse). That is deliberately weaker than time synchronization: it\n * does not say when readout happens relative to the start or end of exposure. The total data-readout\n * time is kept for diagnostics but never used to check the link. The readout skew and the boundary\n * sweep times may differ; their ratio is reported without implying a shared clock.\n * @see docs/MOTION_AND_SIGNAL.md for equations, coordinate/unit conventions, blockers and support\n * limits.\n */\n\nimport {\n approximationResult,\n type CalculationProvenance,\n type CalculationResult\n} from \"../core/calculation-result.js\";\nimport {\n parseEvidenceList,\n type EvidenceProvenance\n} from \"../core/evidence-provenance.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport type { RasterPoint, RasterRect } from \"../output/capture-geometry.js\";\nimport {\n calculateCaptureExposureWindows,\n parseBoundarySchedule,\n parseSecondsFact,\n type CalculateCaptureExposureWindowsInput,\n type ExposureBoundarySchedule,\n type UniformLinearExposureBoundarySchedule,\n type SourcedCaptureTimingSeconds\n} from \"./exposure-window.js\";\nimport {\n calculateSensorReadoutTiming,\n type CaptureShutterMechanism,\n type NativeSensorReadoutScanDirection,\n type RollingSensorReadoutTimingDeclaration,\n type SensorReadoutTimingDeclaration,\n type SourcedSensorTimingSeconds\n} from \"./readout-timing.js\";\nimport type { NativeImageRaster } from \"./sensor-geometry.js\";\nimport { requireInputKeys, rejectUnknownFields } from \"../core/record-validation.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\n\nexport type ReadoutExposureBoundaryId = \"opening\" | \"closing\";\n\nexport type ReadoutExposureSpatialPhaseOrientation =\n | \"same\"\n | \"reversed\";\n\nexport interface ReadoutExposureBoundarySpatialLink {\n boundary: ReadoutExposureBoundaryId;\n /**\n * How the normalized position of the rolling readout scan relates to that\n * of the selected electronic exposure-boundary scan.\n *\n * \"same\": boundary normalized phase = readout normalized phase.\n * \"reversed\": boundary normalized phase = 1 - readout normalized phase.\n *\n * This does not establish absolute temporal synchronization.\n */\n phaseOrientation: ReadoutExposureSpatialPhaseOrientation;\n /**\n * Evidence for the link itself.\n *\n * The readout direction and timing, and the exposure-boundary direction and\n * timing, keep their own separate evidence.\n */\n evidence: readonly EvidenceProvenance[];\n}\n\nexport type ReadoutExposureTimingLinkageDeclaration =\n | {\n /**\n * No link between readout and exposure is stated for this capture.\n * This is not evidence that the physical processes are independent.\n */\n kind: \"unlinked\";\n links?: never;\n }\n | {\n /**\n * One or more exposure boundaries are explicitly linked to the rolling\n * readout by their normalized position across the native sensor.\n */\n kind: \"spatial-phase-linked\";\n links: readonly ReadoutExposureBoundarySpatialLink[];\n };\n\nexport interface AssessReadoutExposureTimingLinkageInput {\n nativeRaster: NativeImageRaster;\n activeCaptureRect?: RasterRect;\n shutterMechanism: CaptureShutterMechanism;\n readout: SensorReadoutTimingDeclaration;\n nominalExposureDurationSeconds: SourcedCaptureTimingSeconds;\n opening: ExposureBoundarySchedule;\n closing: ExposureBoundarySchedule;\n linkage: ReadoutExposureTimingLinkageDeclaration;\n samplePointsNative?: readonly RasterPoint[];\n}\n\nexport interface ReadoutExposureBoundarySpatialLinkAssessment {\n boundary: ReadoutExposureBoundaryId;\n boundaryActuator: \"electronic\";\n phaseOrientation: ReadoutExposureSpatialPhaseOrientation;\n normalizedPhaseRelationship:\n | \"boundary-phase-equals-readout-phase\"\n | \"boundary-phase-equals-one-minus-readout-phase\";\n relationshipEvidence: readonly EvidenceProvenance[];\n readoutDirectionNative: NativeSensorReadoutScanDirection;\n boundaryDirectionNative: NativeSensorReadoutScanDirection;\n /**\n * Rolling readout skew: time from the first to the last scan position.\n */\n readoutSpatialSamplingSkewSeconds: SourcedSensorTimingSeconds;\n /**\n * Time for the exposure boundary to sweep from first to last position.\n */\n boundaryTraversalDurationSeconds: SourcedCaptureTimingSeconds;\n /**\n * A descriptive ratio only. A value of 1 does not prove time\n * synchronization or a common clock origin.\n */\n boundaryTraversalToReadoutSpatialSkewRatio: number;\n absoluteTemporalAlignment: \"not-established\";\n}\n\nexport interface ReadoutExposureTimingLinkageAssessment {\n linkageKind: ReadoutExposureTimingLinkageDeclaration[\"kind\"];\n relationshipMeaning:\n | \"no-readout-exposure-relationship-asserted\"\n | \"normalized-spatial-phase-relationship-asserted\";\n readoutMode: \"rolling\" | \"global\";\n shutterMechanism: CaptureShutterMechanism;\n activeCaptureRect: RasterRect;\n absoluteTemporalAlignment: \"not-established\";\n /**\n * Data-readout time for the capture. Kept for diagnostics, but never used to\n * check an exposure-boundary link.\n */\n captureReadoutDurationSeconds: SourcedSensorTimingSeconds;\n links: readonly ReadoutExposureBoundarySpatialLinkAssessment[];\n componentProvenance: {\n sensorReadout: CalculationProvenance;\n exposureWindows: CalculationProvenance;\n };\n}\n\ntype UnknownRecord = Record<string, unknown>;\n\nconst BOUNDARIES = new Set<ReadoutExposureBoundaryId>([\n \"opening\",\n \"closing\"\n]);\n\nconst PHASE_ORIENTATIONS =\n new Set<ReadoutExposureSpatialPhaseOrientation>([\n \"same\",\n \"reversed\"\n ]);\n\nfunction requireRecord(\n value: unknown,\n path: string,\n allowedKeys: readonly string[] | null\n): UnknownRecord {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new InvalidConfigurationError(path + \" must be an object.\");\n }\n if (allowedKeys !== null) rejectUnknownFields(value, allowedKeys, path);\n return value as UnknownRecord;\n}\n\nfunction parseSpatialLink(\n value: unknown,\n path: string\n): ReadoutExposureBoundarySpatialLink {\n const record = requireRecord(value, path, [\"boundary\", \"phaseOrientation\", \"evidence\"]);\n\n if (\n typeof record.boundary !== \"string\" ||\n !BOUNDARIES.has(record.boundary as ReadoutExposureBoundaryId)\n ) {\n throw new InvalidConfigurationError(path + \".boundary is invalid.\");\n }\n\n if (\n typeof record.phaseOrientation !== \"string\" ||\n !PHASE_ORIENTATIONS.has(\n record.phaseOrientation as ReadoutExposureSpatialPhaseOrientation\n )\n ) {\n throw new InvalidConfigurationError(\n path + \".phaseOrientation is invalid.\"\n );\n }\n\n return {\n boundary: record.boundary as ReadoutExposureBoundaryId,\n phaseOrientation:\n record.phaseOrientation as ReadoutExposureSpatialPhaseOrientation,\n evidence: parseEvidenceList(record.evidence, path + \".evidence\")\n };\n}\n\nfunction parseLinkageDeclaration(\n value: unknown\n): ReadoutExposureTimingLinkageDeclaration {\n const record = requireRecord(value, \"linkage\", [\"kind\", \"links\"]);\n\n if (record.kind === \"unlinked\") {\n if (record.links !== undefined) {\n throw new InvalidScientificInputError(\n \"linkage.links must be omitted when linkage.kind is unlinked.\"\n );\n }\n return { kind: \"unlinked\" };\n }\n\n if (record.kind !== \"spatial-phase-linked\") {\n throw new InvalidConfigurationError(\"linkage.kind is invalid.\");\n }\n\n if (!Array.isArray(record.links) || record.links.length === 0) {\n throw new InvalidConfigurationError(\n \"linkage.links must be a non-empty array for spatial-phase-linked linkage.\"\n );\n }\n\n const links = record.links.map((entry, index) =>\n parseSpatialLink(entry, \"linkage.links[\" + index + \"]\")\n );\n\n const seen = new Set<ReadoutExposureBoundaryId>();\n for (const link of links) {\n if (seen.has(link.boundary)) {\n throw new InvalidScientificInputError(\n \"linkage.links must not contain duplicate boundary links.\"\n );\n }\n seen.add(link.boundary);\n }\n\n return {\n kind: \"spatial-phase-linked\",\n links\n };\n}\n\nfunction oppositeDirection(\n direction: NativeSensorReadoutScanDirection\n): NativeSensorReadoutScanDirection {\n switch (direction) {\n case \"top-to-bottom\":\n return \"bottom-to-top\";\n case \"bottom-to-top\":\n return \"top-to-bottom\";\n case \"left-to-right\":\n return \"right-to-left\";\n case \"right-to-left\":\n return \"left-to-right\";\n }\n}\n\nfunction expectedBoundaryDirection(\n readoutDirection: NativeSensorReadoutScanDirection,\n phaseOrientation: ReadoutExposureSpatialPhaseOrientation\n): NativeSensorReadoutScanDirection {\n return phaseOrientation === \"same\"\n ? readoutDirection\n : oppositeDirection(readoutDirection);\n}\n\n/**\n * Checks an explicitly declared link between the rolling readout scan and one\n * or more electronic exposure-boundary scans.\n *\n * This is deliberately weaker than time synchronization. It can show that two\n * schedules sweep the native sensor in the same order (or in reverse), but not\n * when readout happens relative to the start or end of exposure.\n *\n * The total data-readout time is kept for diagnostics and never used to check\n * the link. The readout skew and the boundary sweep time may differ; their\n * ratio is reported without implying a shared clock.\n */\nexport function assessReadoutExposureTimingLinkage(\n input: AssessReadoutExposureTimingLinkageInput\n): CalculationResult<ReadoutExposureTimingLinkageAssessment> {\n requireInputKeys(input, \"assessReadoutExposureTimingLinkage\", [\"nativeRaster\", \"activeCaptureRect\", \"shutterMechanism\", \"readout\", \"nominalExposureDurationSeconds\", \"opening\", \"closing\", \"linkage\", \"samplePointsNative\"]);\n const linkage = parseLinkageDeclaration(input.linkage);\n\n const sensorReadout = calculateSensorReadoutTiming({\n nativeRaster: input.nativeRaster,\n ...(input.activeCaptureRect === undefined\n ? {}\n : { activeCaptureRect: input.activeCaptureRect }),\n shutterMechanism: input.shutterMechanism,\n readout: input.readout,\n ...(input.samplePointsNative === undefined\n ? {}\n : { samplePointsNative: input.samplePointsNative })\n });\n\n const exposureWindows = calculateCaptureExposureWindows({\n nativeRaster: input.nativeRaster,\n ...(input.activeCaptureRect === undefined\n ? {}\n : { activeCaptureRect: input.activeCaptureRect }),\n shutterMechanism: input.shutterMechanism,\n nominalExposureDurationSeconds:\n input.nominalExposureDurationSeconds,\n opening: input.opening,\n closing: input.closing,\n ...(input.samplePointsNative === undefined\n ? {}\n : { samplePointsNative: input.samplePointsNative })\n });\n\n if (linkage.kind === \"unlinked\") {\n return approximationResult(\n {\n linkageKind: \"unlinked\",\n relationshipMeaning:\n \"no-readout-exposure-relationship-asserted\",\n readoutMode: sensorReadout.value.readoutMode,\n shutterMechanism: input.shutterMechanism,\n activeCaptureRect: {\n ...sensorReadout.value.activeCaptureRect\n },\n absoluteTemporalAlignment: \"not-established\",\n captureReadoutDurationSeconds:\n sensorReadout.value.captureReadoutDurationSeconds,\n links: [],\n componentProvenance: {\n sensorReadout: sensorReadout.provenance,\n exposureWindows: exposureWindows.provenance\n }\n },\n \"readout-exposure-spatial-linkage\",\n \"1.0.0\",\n [\n \"Unlinked means Photivra asserts no readout-to-exposure relationship for this capture; it does not prove the physical processes are independent.\",\n \"Sensor data-readout timing and exposure-window timing retain independent provenance and semantics.\",\n \"Absolute temporal alignment between data readout and exposure boundaries is not established.\"\n ]\n );\n }\n\n const readoutScan = sensorReadout.value.scan;\n if (readoutScan === null) {\n throw new InvalidScientificInputError(\n \"spatial-phase-linked linkage requires rolling sensor readout with a spatial scan.\"\n );\n }\n\n const assessments =\n linkage.links.map<ReadoutExposureBoundarySpatialLinkAssessment>(\n (link) => {\n const boundary =\n link.boundary === \"opening\"\n ? exposureWindows.value.opening\n : exposureWindows.value.closing;\n\n if (boundary.actuator !== \"electronic\") {\n throw new InvalidScientificInputError(\n \"A readout spatial-phase link may target only an electronic exposure boundary.\"\n );\n }\n\n if (\n boundary.schedule.kind !==\n \"uniform-linear-native-scan\"\n ) {\n throw new InvalidScientificInputError(\n \"A readout spatial-phase link requires the selected exposure boundary to use a uniform-linear native scan.\"\n );\n }\n\n const readoutDirection =\n readoutScan.directionNative.value;\n const boundaryDirection =\n boundary.schedule.directionNative.value;\n const expectedDirection = expectedBoundaryDirection(\n readoutDirection,\n link.phaseOrientation\n );\n\n if (boundaryDirection !== expectedDirection) {\n throw new InvalidScientificInputError(\n \"Declared readout/exposure spatial phase orientation contradicts the readout and exposure-boundary scan directions.\"\n );\n }\n\n const readoutSpatialSamplingSkewSeconds =\n readoutScan.spatialSamplingSkewSeconds;\n const boundaryTraversalDurationSeconds =\n boundary.schedule.traversalDurationSeconds;\n\n return {\n boundary: link.boundary,\n boundaryActuator: \"electronic\",\n phaseOrientation: link.phaseOrientation,\n normalizedPhaseRelationship:\n link.phaseOrientation === \"same\"\n ? \"boundary-phase-equals-readout-phase\"\n : \"boundary-phase-equals-one-minus-readout-phase\",\n relationshipEvidence: link.evidence,\n readoutDirectionNative: readoutDirection,\n boundaryDirectionNative: boundaryDirection,\n readoutSpatialSamplingSkewSeconds,\n boundaryTraversalDurationSeconds,\n boundaryTraversalToReadoutSpatialSkewRatio:\n boundaryTraversalDurationSeconds.value /\n readoutSpatialSamplingSkewSeconds.value,\n absoluteTemporalAlignment: \"not-established\"\n };\n }\n );\n\n return approximationResult(\n {\n linkageKind: \"spatial-phase-linked\",\n relationshipMeaning:\n \"normalized-spatial-phase-relationship-asserted\",\n readoutMode: sensorReadout.value.readoutMode,\n shutterMechanism: input.shutterMechanism,\n activeCaptureRect: {\n ...sensorReadout.value.activeCaptureRect\n },\n absoluteTemporalAlignment: \"not-established\",\n captureReadoutDurationSeconds:\n sensorReadout.value.captureReadoutDurationSeconds,\n links: assessments,\n componentProvenance: {\n sensorReadout: sensorReadout.provenance,\n exposureWindows: exposureWindows.provenance\n }\n },\n \"readout-exposure-spatial-linkage\",\n \"1.0.0\",\n [\n \"The linkage establishes normalized native-sensor spatial phase/order only, not absolute temporal synchronization.\",\n \"Every asserted boundary link carries evidence separate from the evidence for the underlying readout and exposure timing facts.\",\n \"Only electronic exposure boundaries may be linked to rolling sensor readout spatial phase.\",\n \"Same phase orientation requires matching native scan direction; reversed phase orientation requires the exact opposite direction on the same native axis.\",\n \"Rolling readout spatial skew and exposure-boundary traversal duration may differ; their ratio is descriptive and does not establish a common clock origin.\",\n \"Capture data-readout duration is not used to validate exposure-boundary timing linkage.\",\n \"Active capture, native coordinate semantics and shutter mechanism are shared because both component schedules are resolved from the same input.\",\n \"Sensor readout and exposure-window component provenance remain visible; linkage does not upgrade either component to calibrated status.\",\n \"No camera-specific timing relationship is inferred from shutter mechanism, sensor architecture metadata, output resolution or adjacent specifications.\"\n ]\n );\n}\n\n/** Model identity of the electronic rolling-shutter (line reset → line read) exposure schedule. */\nexport const ELECTRONIC_ROLLING_SHUTTER_MODEL = Object.freeze({\n id: \"electronic-rolling-shutter-line-reset-read\",\n version: \"1.0.0\"\n} as const);\n\nexport interface ResolveElectronicRollingShutterExposureScheduleInput {\n /** Rolling readout declaration whose scan direction and spatial skew drive both exposure boundaries. */\n readout: RollingSensorReadoutTimingDeclaration;\n /** Per-line integration time between line reset and line read. */\n nominalExposureDurationSeconds: SourcedCaptureTimingSeconds;\n /**\n * Evidence that this capture uses a fully electronic, line-by-line (rolling) shutter whose reset\n * scan runs at the readout line rate. This explicit link is required before sensor readout timing\n * may set the exposure-window timing.\n */\n linkageEvidence: readonly EvidenceProvenance[];\n}\n\n/**\n * Exposure-window schedule of an electronic rolling shutter. Its `shutterMechanism`,\n * `nominalExposureDurationSeconds`, `opening` and `closing` fields form a complete exposure schedule, so\n * they can be passed as a native RAW / environment `exposureWindow` or spread into a production\n * `temporalCapture.exposureWindowInput` (see {@link createRollingShutterCaptureExposureSchedule}).\n */\nexport interface ElectronicRollingShutterExposureSchedule {\n modelId: typeof ELECTRONIC_ROLLING_SHUTTER_MODEL.id;\n modelVersion: typeof ELECTRONIC_ROLLING_SHUTTER_MODEL.version;\n shutterMechanism: \"electronic\";\n nominalExposureDurationSeconds: SourcedCaptureTimingSeconds;\n /** Line-reset scan: a native point at normalized scan position s starts integrating at s·τ. */\n opening: UniformLinearExposureBoundarySchedule;\n /** Line-read scan: the same point stops integrating at T + s·τ. */\n closing: UniformLinearExposureBoundarySchedule;\n /** Explicit linkage declaration accepted by {@link assessReadoutExposureTimingLinkage}. */\n linkage: Extract<ReadoutExposureTimingLinkageDeclaration, { kind: \"spatial-phase-linked\" }>;\n /** τ: offset between the first and last exposure starts (equal to the readout skew). */\n lineScanSkewSeconds: number;\n /** Every point integrates for exactly the nominal exposure duration T. */\n localExposureDurationSeconds: number;\n limitations: readonly string[];\n}\n\n/** The exposure-schedule fields of a derived rolling-shutter schedule, without model metadata. */\nexport function createRollingShutterCaptureExposureSchedule(\n schedule: ElectronicRollingShutterExposureSchedule\n): Pick<CalculateCaptureExposureWindowsInput, \"shutterMechanism\" | \"nominalExposureDurationSeconds\" | \"opening\" | \"closing\"> {\n // 2.0.0: the whole schedule is checked, so an unknown field at any level is rejected.\n const record = requireRecord(schedule, \"rollingShutterSchedule\", [\"modelId\", \"modelVersion\", \"shutterMechanism\",\n \"nominalExposureDurationSeconds\", \"opening\", \"closing\", \"linkage\", \"lineScanSkewSeconds\", \"localExposureDurationSeconds\",\n \"limitations\"]);\n parseSecondsFact(record.nominalExposureDurationSeconds, \"rollingShutterSchedule.nominalExposureDurationSeconds\");\n parseBoundarySchedule(record.opening, \"rollingShutterSchedule.opening\");\n parseBoundarySchedule(record.closing, \"rollingShutterSchedule.closing\");\n if (record.linkage !== undefined) parseLinkageDeclaration(record.linkage);\n return structuredClone({ shutterMechanism: schedule.shutterMechanism,\n nominalExposureDurationSeconds: schedule.nominalExposureDurationSeconds,\n opening: schedule.opening, closing: schedule.closing });\n}\n\n/**\n * Derives the exposure-window schedule of a fully electronic rolling shutter from its rolling readout\n * declaration.\n *\n * In a CMOS rolling shutter, each line is reset, collects light for the exposure time T and is then\n * read. The reset pointer runs T ahead of the read pointer, and both move at the line rate (El Gamal &\n * Eltoukhy 2005; Liang, Chang & Chen 2008). With the uniform linear scan used throughout this engine, a\n * native point at normalized scan position s ∈ [0, 1] (along the declared scan direction, over the\n * active capture) collects light over [s·τ, s·τ + T], where τ is the declared first-to-last readout\n * skew. So both boundaries become uniform linear scans in the readout direction that take τ to sweep.\n *\n * When this schedule feeds the per-site exposure windows, everything that samples motion, moving\n * sources or time-varying light at local exposure times (environment RAW, rotation quadrature,\n * temporal illumination integration) produces rolling-shutter skew, wobble, partial exposure and\n * flicker banding.\n *\n * Approximation: constant line rate, no line-reset jitter, no hybrid of global reset with a mechanical\n * closing shutter, and the active-capture skew is declared by the caller (never scaled from a\n * full-frame value).\n * @see docs/MOTION_AND_SIGNAL.md#electronic-rolling-shutter-image-formation\n */\nexport function resolveElectronicRollingShutterExposureSchedule(\n input: ResolveElectronicRollingShutterExposureScheduleInput\n): ElectronicRollingShutterExposureSchedule {\n requireInputKeys(input, \"resolveElectronicRollingShutterExposureSchedule\", [\"readout\", \"nominalExposureDurationSeconds\", \"linkageEvidence\"]);\n // The input keys are checked above; calculateSensorReadoutTiming checks the readout's own fields.\n const record = requireRecord(input, \"rollingShutter\", null);\n const readout = requireRecord(record.readout, \"rollingShutter.readout\", null);\n if (readout.readoutMode !== \"rolling\") {\n throw new InvalidScientificInputError(\n \"An electronic rolling-shutter exposure schedule requires a rolling sensor readout declaration.\"\n );\n }\n const linkageEvidence = parseEvidenceList(input.linkageEvidence, \"rollingShutter.linkageEvidence\");\n const rolling = input.readout;\n // Check the declaration and the exposure fact with the existing readout and exposure-window calculators.\n const probeRaster = { pixelWidth: 2, pixelHeight: 2 };\n calculateSensorReadoutTiming({ nativeRaster: probeRaster, shutterMechanism: \"electronic\", readout: rolling });\n const opening: UniformLinearExposureBoundarySchedule = {\n kind: \"uniform-linear-native-scan\",\n directionNative: { value: rolling.scanDirectionNative.value, evidence: [...rolling.scanDirectionNative.evidence] },\n traversalDurationSeconds: { value: rolling.spatialSamplingSkewSeconds.value, unit: \"s\",\n evidence: [...rolling.spatialSamplingSkewSeconds.evidence] }\n };\n const closing: UniformLinearExposureBoundarySchedule = structuredClone(opening);\n const nominal = calculateCaptureExposureWindows({ nativeRaster: probeRaster, shutterMechanism: \"electronic\",\n nominalExposureDurationSeconds: input.nominalExposureDurationSeconds, opening, closing }).value.nominalExposureDurationSeconds;\n return {\n modelId: ELECTRONIC_ROLLING_SHUTTER_MODEL.id,\n modelVersion: ELECTRONIC_ROLLING_SHUTTER_MODEL.version,\n shutterMechanism: \"electronic\",\n nominalExposureDurationSeconds: { value: nominal.value, unit: \"s\", evidence: [...nominal.evidence] },\n opening,\n closing,\n linkage: { kind: \"spatial-phase-linked\", links: [\n { boundary: \"opening\", phaseOrientation: \"same\", evidence: linkageEvidence },\n { boundary: \"closing\", phaseOrientation: \"same\", evidence: linkageEvidence }] },\n lineScanSkewSeconds: rolling.spatialSamplingSkewSeconds.value,\n localExposureDurationSeconds: nominal.value,\n limitations: [\n \"Uniform-linear line timing: each native point integrates over [s·τ, s·τ + T] with s its normalized scan position.\",\n \"Line-reset jitter, non-uniform line timing, global-reset release and mechanical-close hybrids are not modeled.\",\n \"The skew τ is the caller-declared active-capture spatial sampling skew; it is never scaled from a full-frame value.\"\n ]\n };\n}\n\n/**\n * True when an exposure schedule is exactly the electronic rolling-shutter schedule derived from this\n * readout declaration: an electronic mechanism, with both boundaries scanning in the readout direction\n * and taking the readout skew to sweep.\n */\nexport function isElectronicRollingShutterExposureSchedule(\n schedule: Pick<CalculateCaptureExposureWindowsInput, \"shutterMechanism\" | \"opening\" | \"closing\">,\n readout: SensorReadoutTimingDeclaration\n): boolean {\n if (readout.readoutMode !== \"rolling\" || schedule.shutterMechanism !== \"electronic\") return false;\n return [schedule.opening, schedule.closing].every((boundary) => boundary.kind === \"uniform-linear-native-scan\" &&\n boundary.directionNative.value === readout.scanDirectionNative.value &&\n boundary.traversalDurationSeconds.value === readout.spatialSamplingSkewSeconds.value);\n}\n"]}
1
+ {"version":3,"file":"readout-exposure-linkage.js","sourceRoot":"","sources":["../../src/sensor/readout-exposure-linkage.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC;;;;;;;;;;;GAWG;AAEH,OAAO,EACL,mBAAmB,EAGpB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,iBAAiB,EAElB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,2BAA2B,EAAE,MAAM,uBAAuB,CAAC;AAEpE,OAAO,EACL,+BAA+B,EAC/B,qBAAqB,EACrB,gBAAgB,EAKjB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,4BAA4B,EAM7B,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACrF,OAAO,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AA4G3E,MAAM,UAAU,GAAG,IAAI,GAAG,CAA4B;IACpD,SAAS;IACT,SAAS;CACV,CAAC,CAAC;AAEH,MAAM,kBAAkB,GACtB,IAAI,GAAG,CAAyC;IAC9C,MAAM;IACN,UAAU;CACX,CAAC,CAAC;AAEL,SAAS,aAAa,CACpB,KAAc,EACd,IAAY,EACZ,WAAqC;IAErC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACxE,MAAM,IAAI,yBAAyB,CAAC,IAAI,GAAG,qBAAqB,CAAC,CAAC;IACpE,CAAC;IACD,IAAI,WAAW,KAAK,IAAI;QAAE,mBAAmB,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,CAAC,CAAC;IACxE,OAAO,KAAsB,CAAC;AAChC,CAAC;AAED,SAAS,gBAAgB,CACvB,KAAc,EACd,IAAY;IAEZ,MAAM,MAAM,GAAG,aAAa,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,UAAU,EAAE,kBAAkB,EAAE,UAAU,CAAC,CAAC,CAAC;IAExF,IACE,OAAO,MAAM,CAAC,QAAQ,KAAK,QAAQ;QACnC,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,QAAqC,CAAC,EAC7D,CAAC;QACD,MAAM,IAAI,yBAAyB,CAAC,IAAI,GAAG,uBAAuB,CAAC,CAAC;IACtE,CAAC;IAED,IACE,OAAO,MAAM,CAAC,gBAAgB,KAAK,QAAQ;QAC3C,CAAC,kBAAkB,CAAC,GAAG,CACrB,MAAM,CAAC,gBAA0D,CAClE,EACD,CAAC;QACD,MAAM,IAAI,yBAAyB,CACjC,IAAI,GAAG,+BAA+B,CACvC,CAAC;IACJ,CAAC;IAED,OAAO;QACL,QAAQ,EAAE,MAAM,CAAC,QAAqC;QACtD,gBAAgB,EACd,MAAM,CAAC,gBAA0D;QACnE,QAAQ,EAAE,iBAAiB,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,GAAG,WAAW,CAAC;KACjE,CAAC;AACJ,CAAC;AAED,SAAS,uBAAuB,CAC9B,KAAc;IAEd,MAAM,MAAM,GAAG,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAElE,IAAI,MAAM,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QAC/B,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAC/B,MAAM,IAAI,2BAA2B,CACnC,8DAA8D,CAC/D,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;IAC9B,CAAC;IAED,IAAI,MAAM,CAAC,IAAI,KAAK,sBAAsB,EAAE,CAAC;QAC3C,MAAM,IAAI,yBAAyB,CAAC,0BAA0B,CAAC,CAAC;IAClE,CAAC;IAED,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9D,MAAM,IAAI,yBAAyB,CACjC,2EAA2E,CAC5E,CAAC;IACJ,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAC9C,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,GAAG,KAAK,GAAG,GAAG,CAAC,CACxD,CAAC;IAEF,MAAM,IAAI,GAAG,IAAI,GAAG,EAA6B,CAAC;IAClD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC5B,MAAM,IAAI,2BAA2B,CACnC,0DAA0D,CAC3D,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC1B,CAAC;IAED,OAAO;QACL,IAAI,EAAE,sBAAsB;QAC5B,KAAK;KACN,CAAC;AACJ,CAAC;AAED,SAAS,iBAAiB,CACxB,SAA2C;IAE3C,QAAQ,SAAS,EAAE,CAAC;QAClB,KAAK,eAAe;YAClB,OAAO,eAAe,CAAC;QACzB,KAAK,eAAe;YAClB,OAAO,eAAe,CAAC;QACzB,KAAK,eAAe;YAClB,OAAO,eAAe,CAAC;QACzB,KAAK,eAAe;YAClB,OAAO,eAAe,CAAC;IAC3B,CAAC;AACH,CAAC;AAED,SAAS,yBAAyB,CAChC,gBAAkD,EAClD,gBAAwD;IAExD,OAAO,gBAAgB,KAAK,MAAM;QAChC,CAAC,CAAC,gBAAgB;QAClB,CAAC,CAAC,iBAAiB,CAAC,gBAAgB,CAAC,CAAC;AAC1C,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,kCAAkC,CAChD,KAA8C;IAE9C,gBAAgB,CAAC,KAAK,EAAE,oCAAoC,EAAE,CAAC,cAAc,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,SAAS,EAAE,gCAAgC,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,oBAAoB,CAAC,CAAC,CAAC;IAC7N,MAAM,OAAO,GAAG,uBAAuB,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAEvD,MAAM,aAAa,GAAG,4BAA4B,CAAC;QACjD,YAAY,EAAE,KAAK,CAAC,YAAY;QAChC,GAAG,CAAC,KAAK,CAAC,iBAAiB,KAAK,SAAS;YACvC,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC,EAAE,iBAAiB,EAAE,KAAK,CAAC,iBAAiB,EAAE,CAAC;QACnD,gBAAgB,EAAE,KAAK,CAAC,gBAAgB;QACxC,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,GAAG,CAAC,KAAK,CAAC,kBAAkB,KAAK,SAAS;YACxC,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC,EAAE,kBAAkB,EAAE,KAAK,CAAC,kBAAkB,EAAE,CAAC;KACtD,CAAC,CAAC;IAEH,MAAM,eAAe,GAAG,+BAA+B,CAAC;QACtD,YAAY,EAAE,KAAK,CAAC,YAAY;QAChC,GAAG,CAAC,KAAK,CAAC,iBAAiB,KAAK,SAAS;YACvC,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC,EAAE,iBAAiB,EAAE,KAAK,CAAC,iBAAiB,EAAE,CAAC;QACnD,gBAAgB,EAAE,KAAK,CAAC,gBAAgB;QACxC,8BAA8B,EAC5B,KAAK,CAAC,8BAA8B;QACtC,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,GAAG,CAAC,KAAK,CAAC,kBAAkB,KAAK,SAAS;YACxC,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC,EAAE,kBAAkB,EAAE,KAAK,CAAC,kBAAkB,EAAE,CAAC;KACtD,CAAC,CAAC;IAEH,IAAI,OAAO,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QAChC,OAAO,mBAAmB,CACxB;YACE,WAAW,EAAE,UAAU;YACvB,mBAAmB,EACjB,2CAA2C;YAC7C,WAAW,EAAE,aAAa,CAAC,KAAK,CAAC,WAAW;YAC5C,gBAAgB,EAAE,KAAK,CAAC,gBAAgB;YACxC,iBAAiB,EAAE;gBACjB,GAAG,aAAa,CAAC,KAAK,CAAC,iBAAiB;aACzC;YACD,yBAAyB,EAAE,iBAAiB;YAC5C,6BAA6B,EAC3B,aAAa,CAAC,KAAK,CAAC,6BAA6B;YACnD,KAAK,EAAE,EAAE;YACT,mBAAmB,EAAE;gBACnB,aAAa,EAAE,aAAa,CAAC,UAAU;gBACvC,eAAe,EAAE,eAAe,CAAC,UAAU;aAC5C;SACF,EACD,kCAAkC,EAClC,OAAO,EACP;YACE,iJAAiJ;YACjJ,oGAAoG;YACpG,8FAA8F;SAC/F,CACF,CAAC;IACJ,CAAC;IAED,MAAM,WAAW,GAAG,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC;IAC7C,IAAI,WAAW,KAAK,IAAI,EAAE,CAAC;QACzB,MAAM,IAAI,2BAA2B,CACnC,mFAAmF,CACpF,CAAC;IACJ,CAAC;IAED,MAAM,WAAW,GACf,OAAO,CAAC,KAAK,CAAC,GAAG,CACf,CAAC,IAAI,EAAE,EAAE;QACP,MAAM,QAAQ,GACZ,IAAI,CAAC,QAAQ,KAAK,SAAS;YACzB,CAAC,CAAC,eAAe,CAAC,KAAK,CAAC,OAAO;YAC/B,CAAC,CAAC,eAAe,CAAC,KAAK,CAAC,OAAO,CAAC;QAEpC,IAAI,QAAQ,CAAC,QAAQ,KAAK,YAAY,EAAE,CAAC;YACvC,MAAM,IAAI,2BAA2B,CACnC,+EAA+E,CAChF,CAAC;QACJ,CAAC;QAED,IACE,QAAQ,CAAC,QAAQ,CAAC,IAAI;YACtB,4BAA4B,EAC5B,CAAC;YACD,MAAM,IAAI,2BAA2B,CACnC,2GAA2G,CAC5G,CAAC;QACJ,CAAC;QAED,MAAM,gBAAgB,GACpB,WAAW,CAAC,eAAe,CAAC,KAAK,CAAC;QACpC,MAAM,iBAAiB,GACrB,QAAQ,CAAC,QAAQ,CAAC,eAAe,CAAC,KAAK,CAAC;QAC1C,MAAM,iBAAiB,GAAG,yBAAyB,CACjD,gBAAgB,EAChB,IAAI,CAAC,gBAAgB,CACtB,CAAC;QAEF,IAAI,iBAAiB,KAAK,iBAAiB,EAAE,CAAC;YAC5C,MAAM,IAAI,2BAA2B,CACnC,oHAAoH,CACrH,CAAC;QACJ,CAAC;QAED,MAAM,iCAAiC,GACrC,WAAW,CAAC,0BAA0B,CAAC;QACzC,MAAM,gCAAgC,GACpC,QAAQ,CAAC,QAAQ,CAAC,wBAAwB,CAAC;QAE7C,OAAO;YACL,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,gBAAgB,EAAE,YAAY;YAC9B,gBAAgB,EAAE,IAAI,CAAC,gBAAgB;YACvC,2BAA2B,EACzB,IAAI,CAAC,gBAAgB,KAAK,MAAM;gBAC9B,CAAC,CAAC,qCAAqC;gBACvC,CAAC,CAAC,+CAA+C;YACrD,oBAAoB,EAAE,IAAI,CAAC,QAAQ;YACnC,sBAAsB,EAAE,gBAAgB;YACxC,uBAAuB,EAAE,iBAAiB;YAC1C,iCAAiC;YACjC,gCAAgC;YAChC,0CAA0C,EACxC,gCAAgC,CAAC,KAAK;gBACtC,iCAAiC,CAAC,KAAK;YACzC,yBAAyB,EAAE,iBAAiB;SAC7C,CAAC;IACJ,CAAC,CACF,CAAC;IAEJ,OAAO,mBAAmB,CACxB;QACE,WAAW,EAAE,sBAAsB;QACnC,mBAAmB,EACjB,gDAAgD;QAClD,WAAW,EAAE,aAAa,CAAC,KAAK,CAAC,WAAW;QAC5C,gBAAgB,EAAE,KAAK,CAAC,gBAAgB;QACxC,iBAAiB,EAAE;YACjB,GAAG,aAAa,CAAC,KAAK,CAAC,iBAAiB;SACzC;QACD,yBAAyB,EAAE,iBAAiB;QAC5C,6BAA6B,EAC3B,aAAa,CAAC,KAAK,CAAC,6BAA6B;QACnD,KAAK,EAAE,WAAW;QAClB,mBAAmB,EAAE;YACnB,aAAa,EAAE,aAAa,CAAC,UAAU;YACvC,eAAe,EAAE,eAAe,CAAC,UAAU;SAC5C;KACF,EACD,kCAAkC,EAClC,OAAO,EACP;QACE,mHAAmH;QACnH,gIAAgI;QAChI,4FAA4F;QAC5F,2JAA2J;QAC3J,4JAA4J;QAC5J,yFAAyF;QACzF,iJAAiJ;QACjJ,yIAAyI;QACzI,wJAAwJ;KACzJ,CACF,CAAC;AACJ,CAAC;AAED,mGAAmG;AACnG,MAAM,CAAC,MAAM,gCAAgC,GAAG,eAAe,CAAC,MAAM,CAAC,MAAM,CAAC;IAC5E,EAAE,EAAE,4CAA4C;IAChD,OAAO,EAAE,OAAO;CACR,CAAC,CAAC;AAuCZ,kGAAkG;AAClG,MAAM,UAAU,2CAA2C,CACzD,QAAkD;IAElD,sFAAsF;IACtF,MAAM,MAAM,GAAG,aAAa,CAAC,QAAQ,EAAE,wBAAwB,EAAE,CAAC,SAAS,EAAE,cAAc,EAAE,kBAAkB;QAC7G,gCAAgC,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,qBAAqB,EAAE,8BAA8B;QACxH,aAAa,CAAC,CAAC,CAAC;IAClB,gBAAgB,CAAC,MAAM,CAAC,8BAA8B,EAAE,uDAAuD,CAAC,CAAC;IACjH,qBAAqB,CAAC,MAAM,CAAC,OAAO,EAAE,gCAAgC,CAAC,CAAC;IACxE,qBAAqB,CAAC,MAAM,CAAC,OAAO,EAAE,gCAAgC,CAAC,CAAC;IACxE,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS;QAAE,uBAAuB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC1E,OAAO,eAAe,CAAC,EAAE,gBAAgB,EAAE,QAAQ,CAAC,gBAAgB;QAClE,8BAA8B,EAAE,QAAQ,CAAC,8BAA8B;QACvE,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;AAC5D,CAAC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,+CAA+C,CAC7D,KAA2D;IAE3D,gBAAgB,CAAC,KAAK,EAAE,iDAAiD,EAAE,CAAC,SAAS,EAAE,gCAAgC,EAAE,iBAAiB,CAAC,CAAC,CAAC;IAC7I,kGAAkG;IAClG,MAAM,MAAM,GAAG,aAAa,CAAC,KAAK,EAAE,gBAAgB,EAAE,IAAI,CAAC,CAAC;IAC5D,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,OAAO,EAAE,wBAAwB,EAAE,IAAI,CAAC,CAAC;IAC9E,IAAI,OAAO,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;QACtC,MAAM,IAAI,2BAA2B,CACnC,gGAAgG,CACjG,CAAC;IACJ,CAAC;IACD,MAAM,eAAe,GAAG,iBAAiB,CAAC,KAAK,CAAC,eAAe,EAAE,gCAAgC,CAAC,CAAC;IACnG,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;IAC9B,yGAAyG;IACzG,MAAM,WAAW,GAAG,EAAE,UAAU,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC;IACtD,4BAA4B,CAAC,EAAE,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;IAC9G,MAAM,OAAO,GAA0C;QACrD,IAAI,EAAE,4BAA4B;QAClC,eAAe,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,mBAAmB,CAAC,KAAK,EAAE,QAAQ,EAAE,CAAC,GAAG,OAAO,CAAC,mBAAmB,CAAC,QAAQ,CAAC,EAAE;QAClH,wBAAwB,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,0BAA0B,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG;YACpF,QAAQ,EAAE,CAAC,GAAG,OAAO,CAAC,0BAA0B,CAAC,QAAQ,CAAC,EAAE;KAC/D,CAAC;IACF,MAAM,OAAO,GAA0C,eAAe,CAAC,OAAO,CAAC,CAAC;IAChF,MAAM,OAAO,GAAG,+BAA+B,CAAC,EAAE,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,YAAY;QACzG,8BAA8B,EAAE,KAAK,CAAC,8BAA8B,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,8BAA8B,CAAC;IACjI,OAAO;QACL,OAAO,EAAE,gCAAgC,CAAC,EAAE;QAC5C,YAAY,EAAE,gCAAgC,CAAC,OAAO;QACtD,gBAAgB,EAAE,YAAY;QAC9B,8BAA8B,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,EAAE;QACpG,OAAO;QACP,OAAO;QACP,OAAO,EAAE,EAAE,IAAI,EAAE,sBAAsB,EAAE,KAAK,EAAE;gBAC9C,EAAE,QAAQ,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,EAAE;gBAC5E,EAAE,QAAQ,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,EAAE;aAAC,EAAE;QACjF,mBAAmB,EAAE,OAAO,CAAC,0BAA0B,CAAC,KAAK;QAC7D,4BAA4B,EAAE,OAAO,CAAC,KAAK;QAC3C,WAAW,EAAE;YACX,mHAAmH;YACnH,gHAAgH;YAChH,qHAAqH;SACtH;KACF,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,0CAA0C,CACxD,QAAgG,EAChG,OAAuC;IAEvC,IAAI,OAAO,CAAC,WAAW,KAAK,SAAS,IAAI,QAAQ,CAAC,gBAAgB,KAAK,YAAY;QAAE,OAAO,KAAK,CAAC;IAClG,OAAO,CAAC,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,KAAK,4BAA4B;QAC5G,QAAQ,CAAC,eAAe,CAAC,KAAK,KAAK,OAAO,CAAC,mBAAmB,CAAC,KAAK;QACpE,QAAQ,CAAC,wBAAwB,CAAC,KAAK,KAAK,OAAO,CAAC,0BAA0B,CAAC,KAAK,CAAC,CAAC;AAC1F,CAAC","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Links rolling sensor readout to electronic shutter scans, and derives the exposure schedule of a\n * fully electronic rolling shutter.\n *\n * A declared link says that the readout scan and one or more exposure-boundary scans move across the\n * sensor in the same order (or in reverse). That is deliberately weaker than time synchronization: it\n * does not say when readout happens relative to the start or end of exposure. The total data-readout\n * time is kept for diagnostics but never used to check the link. The readout skew and the boundary\n * sweep times may differ; their ratio is reported without implying a shared clock.\n * @see docs/MOTION_AND_SIGNAL.md for equations, coordinate/unit conventions, blockers and support\n * limits.\n */\n\nimport {\n approximationResult,\n type CalculationProvenance,\n type CalculationResult\n} from \"../core/calculation-result.js\";\nimport {\n parseEvidenceList,\n type EvidenceProvenance\n} from \"../core/evidence-provenance.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport type { RasterPoint, RasterRect } from \"../output/capture-geometry.js\";\nimport {\n calculateCaptureExposureWindows,\n parseBoundarySchedule,\n parseSecondsFact,\n type CalculateCaptureExposureWindowsInput,\n type ExposureBoundarySchedule,\n type UniformLinearExposureBoundarySchedule,\n type SourcedCaptureTimingSeconds\n} from \"./exposure-window.js\";\nimport {\n calculateSensorReadoutTiming,\n type CaptureShutterMechanism,\n type NativeSensorReadoutScanDirection,\n type RollingSensorReadoutTimingDeclaration,\n type SensorReadoutTimingDeclaration,\n type SourcedSensorTimingSeconds\n} from \"./readout-timing.js\";\nimport type { NativeImageRaster } from \"./sensor-geometry.js\";\nimport { requireInputKeys, rejectUnknownFields } from \"../core/record-validation.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\n\nexport type ReadoutExposureBoundaryId = \"opening\" | \"closing\";\n\nexport type ReadoutExposureSpatialPhaseOrientation =\n | \"same\"\n | \"reversed\";\n\nexport interface ReadoutExposureBoundarySpatialLink {\n boundary: ReadoutExposureBoundaryId;\n /**\n * How the normalized position of the rolling readout scan relates to that\n * of the selected electronic exposure-boundary scan.\n *\n * \"same\": boundary normalized phase = readout normalized phase.\n * \"reversed\": boundary normalized phase = 1 - readout normalized phase.\n *\n * This does not establish absolute temporal synchronization.\n */\n phaseOrientation: ReadoutExposureSpatialPhaseOrientation;\n /**\n * Evidence for the link itself.\n *\n * The readout direction and timing, and the exposure-boundary direction and\n * timing, keep their own separate evidence.\n */\n evidence: readonly EvidenceProvenance[];\n}\n\nexport type ReadoutExposureTimingLinkageDeclaration =\n | {\n /**\n * No link between readout and exposure is stated for this capture.\n * This is not evidence that the physical processes are independent.\n */\n kind: \"unlinked\";\n links?: never;\n }\n | {\n /**\n * One or more exposure boundaries are explicitly linked to the rolling\n * readout by their normalized position across the native sensor.\n */\n kind: \"spatial-phase-linked\";\n links: readonly ReadoutExposureBoundarySpatialLink[];\n };\n\nexport interface AssessReadoutExposureTimingLinkageInput {\n nativeRaster: NativeImageRaster;\n activeCaptureRect?: RasterRect;\n shutterMechanism: CaptureShutterMechanism;\n readout: SensorReadoutTimingDeclaration;\n nominalExposureDurationSeconds: SourcedCaptureTimingSeconds;\n opening: ExposureBoundarySchedule;\n closing: ExposureBoundarySchedule;\n linkage: ReadoutExposureTimingLinkageDeclaration;\n samplePointsNative?: readonly RasterPoint[];\n}\n\nexport interface ReadoutExposureBoundarySpatialLinkAssessment {\n boundary: ReadoutExposureBoundaryId;\n boundaryActuator: \"electronic\";\n phaseOrientation: ReadoutExposureSpatialPhaseOrientation;\n normalizedPhaseRelationship:\n | \"boundary-phase-equals-readout-phase\"\n | \"boundary-phase-equals-one-minus-readout-phase\";\n relationshipEvidence: readonly EvidenceProvenance[];\n readoutDirectionNative: NativeSensorReadoutScanDirection;\n boundaryDirectionNative: NativeSensorReadoutScanDirection;\n /**\n * Rolling readout skew: time from the first to the last scan position.\n */\n readoutSpatialSamplingSkewSeconds: SourcedSensorTimingSeconds;\n /**\n * Time for the exposure boundary to sweep from first to last position.\n */\n boundaryTraversalDurationSeconds: SourcedCaptureTimingSeconds;\n /**\n * A descriptive ratio only. A value of 1 does not prove time\n * synchronization or a common clock origin.\n */\n boundaryTraversalToReadoutSpatialSkewRatio: number;\n absoluteTemporalAlignment: \"not-established\";\n}\n\nexport interface ReadoutExposureTimingLinkageAssessment {\n linkageKind: ReadoutExposureTimingLinkageDeclaration[\"kind\"];\n relationshipMeaning:\n | \"no-readout-exposure-relationship-asserted\"\n | \"normalized-spatial-phase-relationship-asserted\";\n readoutMode: \"rolling\" | \"global\";\n shutterMechanism: CaptureShutterMechanism;\n activeCaptureRect: RasterRect;\n absoluteTemporalAlignment: \"not-established\";\n /**\n * Data-readout time for the capture. Kept for diagnostics, but never used to\n * check an exposure-boundary link.\n */\n captureReadoutDurationSeconds: SourcedSensorTimingSeconds;\n links: readonly ReadoutExposureBoundarySpatialLinkAssessment[];\n componentProvenance: {\n sensorReadout: CalculationProvenance;\n exposureWindows: CalculationProvenance;\n };\n}\n\ntype UnknownRecord = Record<string, unknown>;\n\nconst BOUNDARIES = new Set<ReadoutExposureBoundaryId>([\n \"opening\",\n \"closing\"\n]);\n\nconst PHASE_ORIENTATIONS =\n new Set<ReadoutExposureSpatialPhaseOrientation>([\n \"same\",\n \"reversed\"\n ]);\n\nfunction requireRecord(\n value: unknown,\n path: string,\n allowedKeys: readonly string[] | null\n): UnknownRecord {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new InvalidConfigurationError(path + \" must be an object.\");\n }\n if (allowedKeys !== null) rejectUnknownFields(value, allowedKeys, path);\n return value as UnknownRecord;\n}\n\nfunction parseSpatialLink(\n value: unknown,\n path: string\n): ReadoutExposureBoundarySpatialLink {\n const record = requireRecord(value, path, [\"boundary\", \"phaseOrientation\", \"evidence\"]);\n\n if (\n typeof record.boundary !== \"string\" ||\n !BOUNDARIES.has(record.boundary as ReadoutExposureBoundaryId)\n ) {\n throw new InvalidConfigurationError(path + \".boundary is invalid.\");\n }\n\n if (\n typeof record.phaseOrientation !== \"string\" ||\n !PHASE_ORIENTATIONS.has(\n record.phaseOrientation as ReadoutExposureSpatialPhaseOrientation\n )\n ) {\n throw new InvalidConfigurationError(\n path + \".phaseOrientation is invalid.\"\n );\n }\n\n return {\n boundary: record.boundary as ReadoutExposureBoundaryId,\n phaseOrientation:\n record.phaseOrientation as ReadoutExposureSpatialPhaseOrientation,\n evidence: parseEvidenceList(record.evidence, path + \".evidence\")\n };\n}\n\nfunction parseLinkageDeclaration(\n value: unknown\n): ReadoutExposureTimingLinkageDeclaration {\n const record = requireRecord(value, \"linkage\", [\"kind\", \"links\"]);\n\n if (record.kind === \"unlinked\") {\n if (record.links !== undefined) {\n throw new InvalidScientificInputError(\n \"linkage.links must be omitted when linkage.kind is unlinked.\"\n );\n }\n return { kind: \"unlinked\" };\n }\n\n if (record.kind !== \"spatial-phase-linked\") {\n throw new InvalidConfigurationError(\"linkage.kind is invalid.\");\n }\n\n if (!Array.isArray(record.links) || record.links.length === 0) {\n throw new InvalidConfigurationError(\n \"linkage.links must be a non-empty array for spatial-phase-linked linkage.\"\n );\n }\n\n const links = record.links.map((entry, index) =>\n parseSpatialLink(entry, \"linkage.links[\" + index + \"]\")\n );\n\n const seen = new Set<ReadoutExposureBoundaryId>();\n for (const link of links) {\n if (seen.has(link.boundary)) {\n throw new InvalidScientificInputError(\n \"linkage.links must not contain duplicate boundary links.\"\n );\n }\n seen.add(link.boundary);\n }\n\n return {\n kind: \"spatial-phase-linked\",\n links\n };\n}\n\nfunction oppositeDirection(\n direction: NativeSensorReadoutScanDirection\n): NativeSensorReadoutScanDirection {\n switch (direction) {\n case \"top-to-bottom\":\n return \"bottom-to-top\";\n case \"bottom-to-top\":\n return \"top-to-bottom\";\n case \"left-to-right\":\n return \"right-to-left\";\n case \"right-to-left\":\n return \"left-to-right\";\n }\n}\n\nfunction expectedBoundaryDirection(\n readoutDirection: NativeSensorReadoutScanDirection,\n phaseOrientation: ReadoutExposureSpatialPhaseOrientation\n): NativeSensorReadoutScanDirection {\n return phaseOrientation === \"same\"\n ? readoutDirection\n : oppositeDirection(readoutDirection);\n}\n\n/**\n * Checks an explicitly declared link between the rolling readout scan and one\n * or more electronic exposure-boundary scans.\n *\n * This is deliberately weaker than time synchronization. It can show that two\n * schedules sweep the native sensor in the same order (or in reverse), but not\n * when readout happens relative to the start or end of exposure.\n *\n * The total data-readout time is kept for diagnostics and never used to check\n * the link. The readout skew and the boundary sweep time may differ; their\n * ratio is reported without implying a shared clock.\n */\nexport function assessReadoutExposureTimingLinkage(\n input: AssessReadoutExposureTimingLinkageInput\n): CalculationResult<ReadoutExposureTimingLinkageAssessment> {\n requireInputKeys(input, \"assessReadoutExposureTimingLinkage\", [\"nativeRaster\", \"activeCaptureRect\", \"shutterMechanism\", \"readout\", \"nominalExposureDurationSeconds\", \"opening\", \"closing\", \"linkage\", \"samplePointsNative\"]);\n const linkage = parseLinkageDeclaration(input.linkage);\n\n const sensorReadout = calculateSensorReadoutTiming({\n nativeRaster: input.nativeRaster,\n ...(input.activeCaptureRect === undefined\n ? {}\n : { activeCaptureRect: input.activeCaptureRect }),\n shutterMechanism: input.shutterMechanism,\n readout: input.readout,\n ...(input.samplePointsNative === undefined\n ? {}\n : { samplePointsNative: input.samplePointsNative })\n });\n\n const exposureWindows = calculateCaptureExposureWindows({\n nativeRaster: input.nativeRaster,\n ...(input.activeCaptureRect === undefined\n ? {}\n : { activeCaptureRect: input.activeCaptureRect }),\n shutterMechanism: input.shutterMechanism,\n nominalExposureDurationSeconds:\n input.nominalExposureDurationSeconds,\n opening: input.opening,\n closing: input.closing,\n ...(input.samplePointsNative === undefined\n ? {}\n : { samplePointsNative: input.samplePointsNative })\n });\n\n if (linkage.kind === \"unlinked\") {\n return approximationResult(\n {\n linkageKind: \"unlinked\",\n relationshipMeaning:\n \"no-readout-exposure-relationship-asserted\",\n readoutMode: sensorReadout.value.readoutMode,\n shutterMechanism: input.shutterMechanism,\n activeCaptureRect: {\n ...sensorReadout.value.activeCaptureRect\n },\n absoluteTemporalAlignment: \"not-established\",\n captureReadoutDurationSeconds:\n sensorReadout.value.captureReadoutDurationSeconds,\n links: [],\n componentProvenance: {\n sensorReadout: sensorReadout.provenance,\n exposureWindows: exposureWindows.provenance\n }\n },\n \"readout-exposure-spatial-linkage\",\n \"1.0.0\",\n [\n \"Unlinked means Photivra asserts no readout-to-exposure relationship for this capture; it does not prove the physical processes are independent.\",\n \"Sensor data-readout timing and exposure-window timing retain independent provenance and semantics.\",\n \"Absolute temporal alignment between data readout and exposure boundaries is not established.\"\n ]\n );\n }\n\n const readoutScan = sensorReadout.value.scan;\n if (readoutScan === null) {\n throw new InvalidScientificInputError(\n \"spatial-phase-linked linkage requires rolling sensor readout with a spatial scan.\"\n );\n }\n\n const assessments =\n linkage.links.map<ReadoutExposureBoundarySpatialLinkAssessment>(\n (link) => {\n const boundary =\n link.boundary === \"opening\"\n ? exposureWindows.value.opening\n : exposureWindows.value.closing;\n\n if (boundary.actuator !== \"electronic\") {\n throw new InvalidScientificInputError(\n \"A readout spatial-phase link may target only an electronic exposure boundary.\"\n );\n }\n\n if (\n boundary.schedule.kind !==\n \"uniform-linear-native-scan\"\n ) {\n throw new InvalidScientificInputError(\n \"A readout spatial-phase link requires the selected exposure boundary to use a uniform-linear native scan.\"\n );\n }\n\n const readoutDirection =\n readoutScan.directionNative.value;\n const boundaryDirection =\n boundary.schedule.directionNative.value;\n const expectedDirection = expectedBoundaryDirection(\n readoutDirection,\n link.phaseOrientation\n );\n\n if (boundaryDirection !== expectedDirection) {\n throw new InvalidScientificInputError(\n \"Declared readout/exposure spatial phase orientation contradicts the readout and exposure-boundary scan directions.\"\n );\n }\n\n const readoutSpatialSamplingSkewSeconds =\n readoutScan.spatialSamplingSkewSeconds;\n const boundaryTraversalDurationSeconds =\n boundary.schedule.traversalDurationSeconds;\n\n return {\n boundary: link.boundary,\n boundaryActuator: \"electronic\",\n phaseOrientation: link.phaseOrientation,\n normalizedPhaseRelationship:\n link.phaseOrientation === \"same\"\n ? \"boundary-phase-equals-readout-phase\"\n : \"boundary-phase-equals-one-minus-readout-phase\",\n relationshipEvidence: link.evidence,\n readoutDirectionNative: readoutDirection,\n boundaryDirectionNative: boundaryDirection,\n readoutSpatialSamplingSkewSeconds,\n boundaryTraversalDurationSeconds,\n boundaryTraversalToReadoutSpatialSkewRatio:\n boundaryTraversalDurationSeconds.value /\n readoutSpatialSamplingSkewSeconds.value,\n absoluteTemporalAlignment: \"not-established\"\n };\n }\n );\n\n return approximationResult(\n {\n linkageKind: \"spatial-phase-linked\",\n relationshipMeaning:\n \"normalized-spatial-phase-relationship-asserted\",\n readoutMode: sensorReadout.value.readoutMode,\n shutterMechanism: input.shutterMechanism,\n activeCaptureRect: {\n ...sensorReadout.value.activeCaptureRect\n },\n absoluteTemporalAlignment: \"not-established\",\n captureReadoutDurationSeconds:\n sensorReadout.value.captureReadoutDurationSeconds,\n links: assessments,\n componentProvenance: {\n sensorReadout: sensorReadout.provenance,\n exposureWindows: exposureWindows.provenance\n }\n },\n \"readout-exposure-spatial-linkage\",\n \"1.0.0\",\n [\n \"The linkage establishes normalized native-sensor spatial phase/order only, not absolute temporal synchronization.\",\n \"Every asserted boundary link carries evidence separate from the evidence for the underlying readout and exposure timing facts.\",\n \"Only electronic exposure boundaries may be linked to rolling sensor readout spatial phase.\",\n \"Same phase orientation requires matching native scan direction; reversed phase orientation requires the exact opposite direction on the same native axis.\",\n \"Rolling readout spatial skew and exposure-boundary traversal duration may differ; their ratio is descriptive and does not establish a common clock origin.\",\n \"Capture data-readout duration is not used to validate exposure-boundary timing linkage.\",\n \"Active capture, native coordinate semantics and shutter mechanism are shared because both component schedules are resolved from the same input.\",\n \"Sensor readout and exposure-window component provenance remain visible; linkage does not upgrade either component to calibrated status.\",\n \"No camera-specific timing relationship is inferred from shutter mechanism, sensor architecture metadata, output resolution or adjacent specifications.\"\n ]\n );\n}\n\n/** Model identity of the electronic rolling-shutter (line reset → line read) exposure schedule. */\nexport const ELECTRONIC_ROLLING_SHUTTER_MODEL = /* @__PURE__ */ Object.freeze({\n id: \"electronic-rolling-shutter-line-reset-read\",\n version: \"1.0.0\"\n} as const);\n\nexport interface ResolveElectronicRollingShutterExposureScheduleInput {\n /** Rolling readout declaration whose scan direction and spatial skew drive both exposure boundaries. */\n readout: RollingSensorReadoutTimingDeclaration;\n /** Per-line integration time between line reset and line read. */\n nominalExposureDurationSeconds: SourcedCaptureTimingSeconds;\n /**\n * Evidence that this capture uses a fully electronic, line-by-line (rolling) shutter whose reset\n * scan runs at the readout line rate. This explicit link is required before sensor readout timing\n * may set the exposure-window timing.\n */\n linkageEvidence: readonly EvidenceProvenance[];\n}\n\n/**\n * Exposure-window schedule of an electronic rolling shutter. Its `shutterMechanism`,\n * `nominalExposureDurationSeconds`, `opening` and `closing` fields form a complete exposure schedule, so\n * they can be passed as a native RAW / environment `exposureWindow` or spread into a production\n * `temporalCapture.exposureWindowInput` (see {@link createRollingShutterCaptureExposureSchedule}).\n */\nexport interface ElectronicRollingShutterExposureSchedule {\n modelId: typeof ELECTRONIC_ROLLING_SHUTTER_MODEL.id;\n modelVersion: typeof ELECTRONIC_ROLLING_SHUTTER_MODEL.version;\n shutterMechanism: \"electronic\";\n nominalExposureDurationSeconds: SourcedCaptureTimingSeconds;\n /** Line-reset scan: a native point at normalized scan position s starts integrating at s·τ. */\n opening: UniformLinearExposureBoundarySchedule;\n /** Line-read scan: the same point stops integrating at T + s·τ. */\n closing: UniformLinearExposureBoundarySchedule;\n /** Explicit linkage declaration accepted by {@link assessReadoutExposureTimingLinkage}. */\n linkage: Extract<ReadoutExposureTimingLinkageDeclaration, { kind: \"spatial-phase-linked\" }>;\n /** τ: offset between the first and last exposure starts (equal to the readout skew). */\n lineScanSkewSeconds: number;\n /** Every point integrates for exactly the nominal exposure duration T. */\n localExposureDurationSeconds: number;\n limitations: readonly string[];\n}\n\n/** The exposure-schedule fields of a derived rolling-shutter schedule, without model metadata. */\nexport function createRollingShutterCaptureExposureSchedule(\n schedule: ElectronicRollingShutterExposureSchedule\n): Pick<CalculateCaptureExposureWindowsInput, \"shutterMechanism\" | \"nominalExposureDurationSeconds\" | \"opening\" | \"closing\"> {\n // 2.0.0: the whole schedule is checked, so an unknown field at any level is rejected.\n const record = requireRecord(schedule, \"rollingShutterSchedule\", [\"modelId\", \"modelVersion\", \"shutterMechanism\",\n \"nominalExposureDurationSeconds\", \"opening\", \"closing\", \"linkage\", \"lineScanSkewSeconds\", \"localExposureDurationSeconds\",\n \"limitations\"]);\n parseSecondsFact(record.nominalExposureDurationSeconds, \"rollingShutterSchedule.nominalExposureDurationSeconds\");\n parseBoundarySchedule(record.opening, \"rollingShutterSchedule.opening\");\n parseBoundarySchedule(record.closing, \"rollingShutterSchedule.closing\");\n if (record.linkage !== undefined) parseLinkageDeclaration(record.linkage);\n return structuredClone({ shutterMechanism: schedule.shutterMechanism,\n nominalExposureDurationSeconds: schedule.nominalExposureDurationSeconds,\n opening: schedule.opening, closing: schedule.closing });\n}\n\n/**\n * Derives the exposure-window schedule of a fully electronic rolling shutter from its rolling readout\n * declaration.\n *\n * In a CMOS rolling shutter, each line is reset, collects light for the exposure time T and is then\n * read. The reset pointer runs T ahead of the read pointer, and both move at the line rate (El Gamal &\n * Eltoukhy 2005; Liang, Chang & Chen 2008). With the uniform linear scan used throughout this engine, a\n * native point at normalized scan position s ∈ [0, 1] (along the declared scan direction, over the\n * active capture) collects light over [s·τ, s·τ + T], where τ is the declared first-to-last readout\n * skew. So both boundaries become uniform linear scans in the readout direction that take τ to sweep.\n *\n * When this schedule feeds the per-site exposure windows, everything that samples motion, moving\n * sources or time-varying light at local exposure times (environment RAW, rotation quadrature,\n * temporal illumination integration) produces rolling-shutter skew, wobble, partial exposure and\n * flicker banding.\n *\n * Approximation: constant line rate, no line-reset jitter, no hybrid of global reset with a mechanical\n * closing shutter, and the active-capture skew is declared by the caller (never scaled from a\n * full-frame value).\n * @see docs/MOTION_AND_SIGNAL.md#electronic-rolling-shutter-image-formation\n */\nexport function resolveElectronicRollingShutterExposureSchedule(\n input: ResolveElectronicRollingShutterExposureScheduleInput\n): ElectronicRollingShutterExposureSchedule {\n requireInputKeys(input, \"resolveElectronicRollingShutterExposureSchedule\", [\"readout\", \"nominalExposureDurationSeconds\", \"linkageEvidence\"]);\n // The input keys are checked above; calculateSensorReadoutTiming checks the readout's own fields.\n const record = requireRecord(input, \"rollingShutter\", null);\n const readout = requireRecord(record.readout, \"rollingShutter.readout\", null);\n if (readout.readoutMode !== \"rolling\") {\n throw new InvalidScientificInputError(\n \"An electronic rolling-shutter exposure schedule requires a rolling sensor readout declaration.\"\n );\n }\n const linkageEvidence = parseEvidenceList(input.linkageEvidence, \"rollingShutter.linkageEvidence\");\n const rolling = input.readout;\n // Check the declaration and the exposure fact with the existing readout and exposure-window calculators.\n const probeRaster = { pixelWidth: 2, pixelHeight: 2 };\n calculateSensorReadoutTiming({ nativeRaster: probeRaster, shutterMechanism: \"electronic\", readout: rolling });\n const opening: UniformLinearExposureBoundarySchedule = {\n kind: \"uniform-linear-native-scan\",\n directionNative: { value: rolling.scanDirectionNative.value, evidence: [...rolling.scanDirectionNative.evidence] },\n traversalDurationSeconds: { value: rolling.spatialSamplingSkewSeconds.value, unit: \"s\",\n evidence: [...rolling.spatialSamplingSkewSeconds.evidence] }\n };\n const closing: UniformLinearExposureBoundarySchedule = structuredClone(opening);\n const nominal = calculateCaptureExposureWindows({ nativeRaster: probeRaster, shutterMechanism: \"electronic\",\n nominalExposureDurationSeconds: input.nominalExposureDurationSeconds, opening, closing }).value.nominalExposureDurationSeconds;\n return {\n modelId: ELECTRONIC_ROLLING_SHUTTER_MODEL.id,\n modelVersion: ELECTRONIC_ROLLING_SHUTTER_MODEL.version,\n shutterMechanism: \"electronic\",\n nominalExposureDurationSeconds: { value: nominal.value, unit: \"s\", evidence: [...nominal.evidence] },\n opening,\n closing,\n linkage: { kind: \"spatial-phase-linked\", links: [\n { boundary: \"opening\", phaseOrientation: \"same\", evidence: linkageEvidence },\n { boundary: \"closing\", phaseOrientation: \"same\", evidence: linkageEvidence }] },\n lineScanSkewSeconds: rolling.spatialSamplingSkewSeconds.value,\n localExposureDurationSeconds: nominal.value,\n limitations: [\n \"Uniform-linear line timing: each native point integrates over [s·τ, s·τ + T] with s its normalized scan position.\",\n \"Line-reset jitter, non-uniform line timing, global-reset release and mechanical-close hybrids are not modeled.\",\n \"The skew τ is the caller-declared active-capture spatial sampling skew; it is never scaled from a full-frame value.\"\n ]\n };\n}\n\n/**\n * True when an exposure schedule is exactly the electronic rolling-shutter schedule derived from this\n * readout declaration: an electronic mechanism, with both boundaries scanning in the readout direction\n * and taking the readout skew to sweep.\n */\nexport function isElectronicRollingShutterExposureSchedule(\n schedule: Pick<CalculateCaptureExposureWindowsInput, \"shutterMechanism\" | \"opening\" | \"closing\">,\n readout: SensorReadoutTimingDeclaration\n): boolean {\n if (readout.readoutMode !== \"rolling\" || schedule.shutterMechanism !== \"electronic\") return false;\n return [schedule.opening, schedule.closing].every((boundary) => boundary.kind === \"uniform-linear-native-scan\" &&\n boundary.directionNative.value === readout.scanDirectionNative.value &&\n boundary.traversalDurationSeconds.value === readout.spatialSamplingSkewSeconds.value);\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  # Public API reference
2
2
 
3
- Generated from the exact root exports for **@photivra/engine 2.13.0 / root contract 2.13.0** by `node scripts/generate-api-reference.mjs`. No module deep import is supported.
3
+ Generated from the exact root exports for **@photivra/engine 2.14.0 / root contract 2.14.0** by `node scripts/generate-api-reference.mjs`. No module deep import is supported.
4
4
 
5
5
  Use [the developer guide](https://github.com/Photivra/photivra/blob/main/docs/DEVELOPERS.md) for executable paths and scientific boundaries. Types are contracts, not runtime validation: parse untrusted data, preserve units/reference frames, read assessment blockers, and retain evidence/limitations. Optional properties do not imply a universal default. Model/schema IDs remain independent of the distribution.
6
6
 
@@ -846,10 +846,14 @@ Start here to find a function by task. [Using Photivra from an agent](AGENT_GUID
846
846
 
847
847
  | Export | Kind |
848
848
  | --- | --- |
849
+ | [`GENERIC_THIRD_STOP_ISO_SETTINGS`](api/equipment-generic-tier-assets.md#generic_third_stop_iso_settings) | Runtime |
850
+ | [`GENERIC_THIRD_STOP_SHUTTER_SECONDS`](api/equipment-generic-tier-assets.md#generic_third_stop_shutter_seconds) | Runtime |
851
+ | [`GENERIC_WHOLE_STOP_ISO_SETTINGS`](api/equipment-generic-tier-assets.md#generic_whole_stop_iso_settings) | Runtime |
849
852
  | [`GenericEquipmentTier`](api/equipment-generic-tier-assets.md#genericequipmenttier) | Type |
850
853
  | [`GenericEquipmentTierPreset`](api/equipment-generic-tier-assets.md#genericequipmenttierpreset) | Type |
851
854
  | [`GenericTierBodyAssets`](api/equipment-generic-tier-assets.md#generictierbodyassets) | Type |
852
855
  | [`GenericTierLensAssets`](api/equipment-generic-tier-assets.md#generictierlensassets) | Type |
856
+ | [`GenericTierPresetVersion`](api/equipment-generic-tier-assets.md#generictierpresetversion) | Type |
853
857
  | [`GenericVersionedAsset`](api/equipment-generic-tier-assets.md#genericversionedasset) | Type |
854
858
 
855
859
  ## equipment/generic-tier-presets.ts
@@ -860,6 +864,7 @@ Start here to find a function by task. [Using Photivra from an agent](AGENT_GUID
860
864
  | --- | --- |
861
865
  | [`createGenericEquipmentTierSelection`](api/equipment-generic-tier-presets.md#creategenericequipmenttierselection) | Runtime |
862
866
  | [`GENERIC_EQUIPMENT_TIER_PRESET_VERSION`](api/equipment-generic-tier-presets.md#generic_equipment_tier_preset_version) | Runtime |
867
+ | [`GENERIC_EQUIPMENT_TIER_PRESET_VERSIONS`](api/equipment-generic-tier-presets.md#generic_equipment_tier_preset_versions) | Runtime |
863
868
  | [`GENERIC_EQUIPMENT_TIERS`](api/equipment-generic-tier-presets.md#generic_equipment_tiers) | Runtime |
864
869
  | [`GenericEquipmentTierProfileReference`](api/equipment-generic-tier-presets.md#genericequipmenttierprofilereference) | Type |
865
870
  | [`GenericEquipmentTierSelection`](api/equipment-generic-tier-presets.md#genericequipmenttierselection) | Type |
@@ -1173,6 +1178,8 @@ Start here to find a function by task. [Using Photivra from an agent](AGENT_GUID
1173
1178
  | [`ChartScenePresetId`](api/facade-chart-scenes.md#chartscenepresetid) | Type |
1174
1179
  | [`ChartSlantedSquare`](api/facade-chart-scenes.md#chartslantedsquare) | Type |
1175
1180
  | [`CheckerboardChartScene`](api/facade-chart-scenes.md#checkerboardchartscene) | Type |
1181
+ | [`ColorPatchesChartScene`](api/facade-chart-scenes.md#colorpatcheschartscene) | Type |
1182
+ | [`ColorPatchReflectance`](api/facade-chart-scenes.md#colorpatchreflectance) | Type |
1176
1183
  | [`CompositeChartElement`](api/facade-chart-scenes.md#compositechartelement) | Type |
1177
1184
  | [`CompositeChartScene`](api/facade-chart-scenes.md#compositechartscene) | Type |
1178
1185
  | [`DeadLeavesChartScene`](api/facade-chart-scenes.md#deadleaveschartscene) | Type |
@@ -1191,6 +1198,25 @@ Start here to find a function by task. [Using Photivra from an agent](AGENT_GUID
1191
1198
  | [`UniformChartScene`](api/facade-chart-scenes.md#uniformchartscene) | Type |
1192
1199
  | [`ZonePlateChartScene`](api/facade-chart-scenes.md#zoneplatechartscene) | Type |
1193
1200
 
1201
+ ## facade/color-patches.ts
1202
+
1203
+ [Detailed contracts](api/facade-color-patches.md) · [Source](https://github.com/Photivra/photivra/blob/main/src/facade/color-patches.ts)
1204
+
1205
+ | Export | Kind |
1206
+ | --- | --- |
1207
+ | [`COLOR_PATCH_IDS`](api/facade-color-patches.md#color_patch_ids) | Runtime |
1208
+ | [`COLOR_PATCH_SET_ID`](api/facade-color-patches.md#color_patch_set_id) | Runtime |
1209
+ | [`COLOR_PATCH_SET_VERSION`](api/facade-color-patches.md#color_patch_set_version) | Runtime |
1210
+ | [`COLOR_PATCH_WAVELENGTH_RANGE_NANOMETERS`](api/facade-color-patches.md#color_patch_wavelength_range_nanometers) | Runtime |
1211
+ | [`ColorPatch`](api/facade-color-patches.md#colorpatch) | Type |
1212
+ | [`ColorPatchGroup`](api/facade-color-patches.md#colorpatchgroup) | Type |
1213
+ | [`ColorPatchId`](api/facade-color-patches.md#colorpatchid) | Type |
1214
+ | [`ColorPatchSet`](api/facade-color-patches.md#colorpatchset) | Type |
1215
+ | [`ColorPatchSpectralTerm`](api/facade-color-patches.md#colorpatchspectralterm) | Type |
1216
+ | [`evaluateColorPatchReflectance`](api/facade-color-patches.md#evaluatecolorpatchreflectance) | Runtime |
1217
+ | [`EvaluateColorPatchReflectanceInput`](api/facade-color-patches.md#evaluatecolorpatchreflectanceinput) | Type |
1218
+ | [`getColorPatchSet`](api/facade-color-patches.md#getcolorpatchset) | Runtime |
1219
+
1194
1220
  ## facade/display-view-gpu.ts
1195
1221
 
1196
1222
  [Detailed contracts](api/facade-display-view-gpu.md) · [Source](https://github.com/Photivra/photivra/blob/main/src/facade/display-view-gpu.ts)
@@ -1250,6 +1276,7 @@ Start here to find a function by task. [Using Photivra from an agent](AGENT_GUID
1250
1276
  | [`SIMULATE_CAPTURE_DEFAULT_ILLUMINANCE_LUX`](api/facade-simulate-capture.md#simulate_capture_default_illuminance_lux) | Runtime |
1251
1277
  | [`SIMULATE_CAPTURE_FACADE_VERSION`](api/facade-simulate-capture.md#simulate_capture_facade_version) | Runtime |
1252
1278
  | [`SIMULATE_CAPTURE_FAST_OPTIONS`](api/facade-simulate-capture.md#simulate_capture_fast_options) | Runtime |
1279
+ | [`SIMULATE_CAPTURE_METERING_MODES`](api/facade-simulate-capture.md#simulate_capture_metering_modes) | Runtime |
1253
1280
  | [`SIMULATE_DISPLAY_VIEW_DEFAULT_SAMPLES_PER_AXIS`](api/facade-simulate-capture.md#simulate_display_view_default_samples_per_axis) | Runtime |
1254
1281
  | [`simulateCapture`](api/facade-simulate-capture.md#simulatecapture) | Runtime |
1255
1282
  | [`SimulateCaptureAssumption`](api/facade-simulate-capture.md#simulatecaptureassumption) | Type |
@@ -1267,6 +1294,7 @@ Start here to find a function by task. [Using Photivra from an agent](AGENT_GUID
1267
1294
  | [`SimulateCaptureFastOption`](api/facade-simulate-capture.md#simulatecapturefastoption) | Type |
1268
1295
  | [`SimulateCaptureFastReport`](api/facade-simulate-capture.md#simulatecapturefastreport) | Type |
1269
1296
  | [`SimulateCaptureLensDistortion`](api/facade-simulate-capture.md#simulatecapturelensdistortion) | Type |
1297
+ | [`SimulateCaptureMetering`](api/facade-simulate-capture.md#simulatecapturemetering) | Type |
1270
1298
  | [`SimulateCaptureMotion`](api/facade-simulate-capture.md#simulatecapturemotion) | Type |
1271
1299
  | [`SimulateCaptureOptions`](api/facade-simulate-capture.md#simulatecaptureoptions) | Type |
1272
1300
  | [`SimulateCaptureOutputFormat`](api/facade-simulate-capture.md#simulatecaptureoutputformat) | Type |
@@ -48,7 +48,7 @@ the example above is [simulate-capture.mjs](examples/simulate-capture.mjs).
48
48
  | `raw` | The packed native RAW: codes, black levels, saturation flags and per-phase white levels |
49
49
  | `plan` | The native RAW plan the engine executed |
50
50
  | `request` | The complete underlying request: `environmentRaw` (the native environment RAW input), `siteInput(index)` (rebuilds the physical input of any sensor site), `development` (the RAW development input) and `whiteBalanceState` |
51
- | `exposure` | Mode, resolved settings, EV100, `isoControl` (`"manual"` or `"automatic"`) and, with `iso: "auto"`, `autoIso` (how the ISO was chosen and what limited it), metered scene luminance, the deviation from the standard exposure in stops (`standardExposureDeviationStops`), the illuminance at which the settings give the standard exposure (`standardExposureIlluminanceLux`) and the exposure-mode resolver's full output |
51
+ | `exposure` | Mode, resolved settings, EV100, `isoControl` (`"manual"` or `"automatic"`) and, with `iso: "auto"`, `autoIso` (how the ISO was chosen and what limited it), metered scene luminance, the deviation from the standard exposure in stops (`standardExposureDeviationStops`), the illuminance at which the settings give the standard exposure (`standardExposureIlluminanceLux`), from 2.14 the deviation from the standard exposure of an 18 % grey card under the scene's illuminance (`greyCardDeviationStops`, incident metering) and the exposure-mode resolver's full output |
52
52
  | `scene` | Chart parameters, illuminant, distance, framed size and the scene's SHA-256 digest (it covers caller illuminant and reflectance spectra; see `sourceSha256`) |
53
53
  | `diagnostics` | Radiance query count, highlight and rendering counters, per-stage timings (`timingsMs`) and, from 2.1, the opt-ins that ran (`options`); see [Cost of each option](#cost-of-each-option) |
54
54
  | `provenance` | Status (`approximation`), camera preset identity, evidence, the composed contracts in order, and limitations |
@@ -75,10 +75,11 @@ modelling choices (such as "no lens PSF") as `kind: "model"`.
75
75
  | `lens.defocus` | off | `{ model: "geometric-disc" }`: defocus of the scene plane as the geometric blur disc (see [Optics](#optics)). `simulateCapture` and `simulateDisplayView` render it. |
76
76
  | `lens.diffraction` | off | 2.1: `"circular-aperture"` or `{ kind: "circular-aperture", kernelRadiusSamples, fast }`: diffraction blur of an ideal lens at the resolved f-number (see [Optics](#optics)). `aperture` is accepted as a deprecated alias of `kind` until 3.0.0. |
77
77
  | `exposure.mode` | `"manual"` | Also `"aperture-priority"`, `"shutter-priority"`, `"program"`. |
78
- | `exposure.iso` | 100 | Selects the body's ISO readout regime (gain and read noise). `"auto"` lets Auto ISO choose it from the meter in any mode (see [Auto ISO](#auto-iso-516)). |
78
+ | `exposure.iso` | 100 | Selects the body's ISO readout regime (gain and read noise). From 2.14 the tier bodies (preset 2.1.0) take the third-stop settings `GENERIC_THIRD_STOP_ISO_SETTINGS` (100, 125, 160, 200 … up to the body's maximum: 6400, 12 800 or 25 600). `"auto"` lets Auto ISO choose it from the meter in any mode (see [Auto ISO](#auto-iso-516)); it steps in whole stops unless `exposure.autoIso.isoSteps: "third-stop"`. |
79
79
  | `exposure.autoIso` | none | With `iso: "auto"` only. `aperture-priority` requires `{ slowestPreferredShutterSeconds, afterMaximumIso? }`, and `program` takes them optionally; the slowest shutter may be `"auto"`, set by `automaticSlowestShutter`. Every mode takes `limits: { maximum, minimum? }`, the photographer's Auto ISO range (see [Auto ISO](#auto-iso-516)). |
80
80
  | `exposure.shutterSeconds` | 1/125 | Used in `manual` and `shutter-priority`. |
81
81
  | `exposure.compensationStops` | 0 | Applied to the meter target. |
82
+ | `exposure.metering` | `"scene-average"` | 2.14: or `"grey-card"`, incident metering: the meter reads an 18 % grey card under the scene's illuminance instead of the scene, so the automatic modes and Auto ISO land on `greyCardDeviationStops` = 0 (see [Exposure](#exposure)). Reported as `exposure.metering`. |
82
83
  | `scene` | required | A chart preset or a caller function (below). |
83
84
  | `subjectDistanceM` | 5 | Distance of the scene plane. |
84
85
  | `chartSize` | fill the frame | 2.3: `{ widthM, heightM?, surroundReflectance? }` places the chart at its physical size (see [Fixed-size chart](#fixed-size-chart-23)). |
@@ -173,6 +174,10 @@ real products, and carry no brand:
173
174
  to 135 mm, f/32 beyond. Any f-number inside the range is accepted; `fNumbers` lists the marked
174
175
  third-stop values within it (`GENERIC_PRIME_THIRD_STOP_F_NUMBERS`: 1, 1.1, 1.2, 1.4, …, 22, 25, 29, 32,
175
176
  the conventional rounded values of 2^(k/6)). An f-number outside the range is rejected.
177
+ - **Third-stop dials (2.14):** `GENERIC_THIRD_STOP_ISO_SETTINGS` lists the bodies' ISO settings (the rounded values of
178
+ 100·2^(k/3)), and `GENERIC_THIRD_STOP_SHUTTER_SECONDS` the standard third-stop shutter times from 1/8000 s to 30 s
179
+ (the rounded values of 2^(k/3) s: 1/8000, 1/6400, 1/5000, … 1/13, 1/10, 1/8, 1/6, 1/5, 1/4, 0.3, 0.4 … 25, 30). The
180
+ bodies' shutters are continuous over 1/8000–30 s and take each value as the exact time.
176
181
  - **Minimum focus** = max(0.10 m, f·(1 + 1/0.15)): the thin-lens object distance at 0.15× magnification,
177
182
  never closer than 10 cm (`GENERIC_PRIME_MINIMUM_FOCUS`). A subject or focus distance below it is
178
183
  rejected. This is a generic thin-lens approximation, measured from the lens, not a real lens's
@@ -375,6 +380,32 @@ The facade meters the scene like a reflected-light meter:
375
380
  illuminances). With the defaults it is the default illuminance; the marked f/11 gives 2^0.919 times it, about
376
381
  40 612 lux. It is `null` for a black scene and for a `radiance-function` scene, whose radiance does not depend on the
377
382
  illuminance. It can exceed the 1e7 lux `illuminanceLux` accepts. The display view reports the same value.
383
+ - `exposure.greyCardDeviationStops` (2.14) is the same deviation for incident metering: the light falling on the scene,
384
+ as an 18 % grey card or a hand-held incident meter reads it, instead of the scene's own luminance. It uses the grey
385
+ card's luminance L = 0.18·E/π (a Lambertian reflector; E the request's `illuminanceLux`) in place of the metered L:
386
+ Δ_grey = log₂(L_ref/(0.18·E/π)), L_ref = 10·N²/(0.65·t·S). Same sign: positive is darker (under). It depends only on
387
+ the settings used and the illuminance, never on the scene's reflectances, so a chart's white paper cannot fool it.
388
+ With the default settings and illuminance it is exactly 0 (the default illuminance is defined from this grey card,
389
+ below); each stop of shutter, aperture or ISO, and each halving of the illuminance, moves it by one stop. It is `null`
390
+ for a `radiance-function` scene, whose radiance ignores the illuminance. The display view reports the same value.
391
+ - **By default the automatic modes meter the scene** (`exposure.metering: "scene-average"`: the body's
392
+ 16 × 16-zone meter, `meteredSceneLuminanceCdPerSquareMeter`), exactly as before 2.14, and `greyCardDeviationStops`
393
+ reports where their exposure sits against the grey card. On a scene brighter than 18 % grey they expose darker
394
+ than the grey card: a uniform 90 % white gives log₂(0.9/0.18) ≈ +2.32, and the app's round 4 All-in-one chart on
395
+ 90 % white paper about +1.78 (its metered luminance is 3.4 times the grey card's).
396
+ - **`exposure.metering: "grey-card"` (2.14) meters by the grey card**: every meter zone reads L = 0.18·E/π, so
397
+ aperture priority, shutter priority and program, with a fixed ISO or Auto ISO, choose settings with
398
+ `greyCardDeviationStops` = 0 whatever the scene's reflectances, as a manual exposure at the default settings and
399
+ illuminance has. What is left is the mode's own steps and limits: the resolvers set a continuous shutter or
400
+ aperture, so only rounding remains unless the lens or shutter range stops them (shutter priority at 1/125 s and
401
+ 5000 lx would need f/3.9 and stops at f/4, 0.10 stop under); Auto ISO chooses whole-stop ISOs, so up to half a
402
+ stop remains, and that remainder is the Auto ISO report's `residualStops`, with `limitingConstraint` naming
403
+ `iso-grid-quantization` or the ISO limit. Compensation still applies (+⅔ gives −⅔). Manual exposure ignores the
404
+ meter, so the option changes nothing there except Auto ISO. `meteredSceneLuminanceCdPerSquareMeter`,
405
+ `standardExposureDeviationStops` and `standardExposureIlluminanceLux` still describe the scene's own luminance.
406
+ A `radiance-function` scene is refused, since its radiance ignores the illuminance. The capture reports
407
+ `exposure.metering`, and the assumption `exposure.meteringSource` gives the card's luminance. When the option is
408
+ omitted, nothing is added to the assumptions, so 2.13 requests give the same assumptions and bytes.
378
409
 
379
410
  DC-004 ties the reference level to an 18 % grey (Explanation 1 and 2.2). The default illuminance
380
411
  puts an 18 % Lambertian grey card, whose luminance is L = 0.18·E/π, exactly at that luminance:
@@ -482,18 +513,22 @@ console.log(handheld.exposure.autoIso?.automaticSlowestShutter, handheld.exposur
482
513
 
483
514
  All charts are analytic patterns written by the engine; no third-party chart data is bundled. Each
484
515
  is a flat Lambertian reflector (it looks equally bright from every direction). Neutral reflectances
485
- are spectrally flat.
516
+ are spectrally flat. Where a preset takes a reflectance spectrum (`uniform`, `patches`), it accepts a
517
+ neutral number, a caller spectrum (`{ kind: "tabulated" }` or `{ kind: "function" }`, 380–780 nm at
518
+ least) or, from 2.14, a built-in color patch `{ kind: "color-patch", patchId }` (see
519
+ [Color patches](https://github.com/Photivra/photivra/blob/main/docs/COLOR_PATCHES.md)).
486
520
 
487
521
  | Preset | Parameters (defaults) | Definition |
488
522
  | --- | --- | --- |
489
- | `uniform` | `reflectance` (0.18) | One reflectance or caller spectrum everywhere. |
523
+ | `uniform` | `reflectance` (0.18) | One reflectance, caller spectrum or built-in colour patch everywhere. |
490
524
  | `slanted-edge` | `angleDegrees` (5), `darkReflectance` (0.15), `lightReflectance` (0.6) | An edge through the centre, tilted clockwise from vertical: light where u·cos θ − v·sin θ ≥ 0. The geometry follows the slanted-edge method described by Burns (2000), also used by ISO 12233 (a few degrees of slant, 4:1 contrast by default). |
491
525
  | `siemens-star` | `cycles` (36), `radiusFraction` (0.9), `darkReflectance` (0.05), `lightReflectance` (0.8), `backgroundReflectance` (0.18) | Alternating sectors of angular width π/cycles inside radius `radiusFraction` × frame half-height. |
492
526
  | `step-wedge` | `steps` (11), `minimumReflectance` (0.03), `maximumReflectance` (0.9), `spacing` (`equal-density`) | Equal-width vertical steps from left to right. Equal density spaces D = −log₁₀ R evenly; `linear` spaces R evenly. |
493
527
  | `grey-ramp` | `minimumReflectance` (0.03), `maximumReflectance` (0.9) | Reflectance linear in u, represented by 1024 equal levels. |
494
528
  | `checkerboard` | `squaresAcross` (8), `darkReflectance` (0.05), `lightReflectance` (0.8) | Square cells, light at the top-left. |
495
529
  | `point-grid` | `columns` (5), `rows` (3), `radius` (0.01), `pointReflectance` (0.9), `backgroundReflectance` (0.02) | Small disks centred in equal cells, for PSF and defocus inspection. |
496
- | `patches` | `patches` (required), `columns`, `gapFraction` (0.1), `backgroundReflectance` (0.18) | Caller reflectance per patch, row-major. Use it for a ColorChecker-like chart with reflectance data you are entitled to use; the engine does not ship such data because its licence is unclear. |
530
+ | `patches` | `patches` (required), `columns`, `gapFraction` (0.1), `backgroundReflectance` (0.18) | Caller reflectance per patch, row-major. Use it for a chart with reflectance data you are entitled to use; the engine does not ship third-party chart data because its licence is unclear. |
531
+ | `color-patches` | `patchIds` (all 30), `columns` (min(6, count)), `gapFraction` (0.1), `backgroundReflectance` (0.18) | 2.14: Photivra's own 30 color patches (neutral ramp, strong and soft primaries and secondaries, skin tones, memory colors, two near the sRGB gamut edge) drawn like `patches`. Each is an analytic reflectance spectrum; see [Color patches](https://github.com/Photivra/photivra/blob/main/docs/COLOR_PATCHES.md) for the formula, the parameters and the code, name, CIELAB (D65) and sRGB of every patch. |
497
532
 
498
533
  ### Chart elements and composite charts (2.3)
499
534
 
package/docs/USAGE.md CHANGED
@@ -7696,6 +7696,26 @@ console.log(star.assumptions.filter((a) => a.kind === "default").map((a) => a.id
7696
7696
  console.log(star.request.siteInput(0).environment.optics.nominalFNumber);
7697
7697
  ```
7698
7698
 
7699
+ ### Color charts (2.14)
7700
+
7701
+ The engine's own [color patches](https://github.com/Photivra/photivra/blob/main/docs/COLOR_PATCHES.md) are reflectance spectra lit by the capture's illuminant. Draw the
7702
+ whole set with the `color-patches` preset, or use one patch wherever a chart takes a reflectance spectrum, as
7703
+ `{ kind: "color-patch", patchId }`. `getColorPatchSet()` gives each patch's code, name, reason for being on the chart and reference CIELAB and sRGB
7704
+ under D65.
7705
+
7706
+ ```ts
7707
+ import { getColorPatchSet, simulateCapture } from "@photivra/engine";
7708
+
7709
+ const tungsten = await simulateCapture({
7710
+ scene: { preset: "color-patches", patchIds: ["neutral-4", "red", "skin-medium", "sky-blue"], columns: 4 },
7711
+ illuminant: "cie-a",
7712
+ sensor: { spectralStepNanometers: 5 },
7713
+ output: { width: 64, height: 16 }
7714
+ });
7715
+ const sky = getColorPatchSet().patches.find((p) => p.patchId === "sky-blue");
7716
+ console.log(tungsten.scene.parameters.patchSet, sky?.code, sky?.name, sky?.reference.cielabD65); // "PV-M1", "Midday Sky"
7717
+ ```
7718
+
7699
7719
  In `aperture-priority` mode, the shutter comes from the meter (CIPA DC-004 relations S = 10/H and H = 0.65·L·t/N²) through
7700
7720
  `resolveAperturePriorityExposureMode()`.
7701
7721
  `exposure.standardExposureDeviationStops` is the exposure used minus the standard exposure for the metered luminance L, in stops,
@@ -7703,7 +7723,21 @@ log₂(N²/t) − log₂(0.65·L·S/10) from the same relations: positive is dar
7703
7723
  mode it is minus the compensation plus any limit the resolver hit; it is `null` only for a scene with no metered luminance.
7704
7724
  `exposure.standardExposureIlluminanceLux` is the illuminance at which those settings give the standard exposure, E·2^Δ for the
7705
7725
  request's illuminance E (a reflective scene's metered luminance is proportional to E); `null` for a black scene or a
7706
- `radiance-function` scene. The display view's `exposure` carries the same values.
7726
+ `radiance-function` scene. `exposure.greyCardDeviationStops` (2.14) is the deviation from the standard exposure of an 18 % grey
7727
+ card under that illuminance, log₂(L_ref/(0.18·E/π)): incident metering, independent of the scene's reflectances, exactly 0 at
7728
+ the defaults. The automatic modes meter the scene by default; `exposure.metering: "grey-card"` makes them (and Auto ISO) meter
7729
+ that grey card instead, so they land on a grey-card deviation of 0. The display view's `exposure` carries the same values.
7730
+
7731
+ ```ts
7732
+ import { simulateCapture } from "@photivra/engine";
7733
+
7734
+ const chart = await simulateCapture({
7735
+ scene: { preset: "uniform", reflectance: 0.9 },
7736
+ exposure: { mode: "aperture-priority", metering: "grey-card" },
7737
+ output: { width: 8, height: 8 }
7738
+ });
7739
+ console.log(chart.exposure.metering, chart.exposure.settings.shutterSeconds, chart.exposure.greyCardDeviationStops);
7740
+ ```
7707
7741
 
7708
7742
  With `exposure.iso: "auto"` (#516) the ISO is chosen too, in every mode, by the resolvers' Auto ISO branches
7709
7743
  ([Priority modes with Auto ISO](#priority-modes-with-auto-iso)). Aperture priority then needs the slowest shutter
@@ -11,5 +11,5 @@ version they were made with. Proof-of-concept (POC), schema and model contracts
11
11
  own versions. Release checks confirm it matches package.json and the packed package.
12
12
 
13
13
  ```ts
14
- ENGINE_API_VERSION = "2.13.0" as const
14
+ ENGINE_API_VERSION = "2.14.0" as const
15
15
  ```
@@ -2,6 +2,37 @@
2
2
 
3
3
  The package and root API versions are recorded once in [Navigation](../API_REFERENCE.md) · [Developer guide](https://github.com/Photivra/photivra/blob/main/docs/DEVELOPERS.md). Generated signatures retain independent schema/model versions. Only the exports listed here are root-package contracts; module-local helpers are not supported deep imports.
4
4
 
5
+ ## GENERIC_THIRD_STOP_ISO_SETTINGS
6
+
7
+ The exact contract is declared below; use the domain guide for assumptions and staged integration.
8
+
9
+ ```ts
10
+ GENERIC_THIRD_STOP_ISO_SETTINGS = Object.freeze([100, 125, 160, 200, 250, 320, 400, 500, 640, 800, 1000, 1250, 1600,
11
+ 2000, 2500, 3200, 4000, 5000, 6400, 8000, 10000, 12800, 16000, 20000, 25600] as const)
12
+ ```
13
+
14
+ ## GENERIC_THIRD_STOP_SHUTTER_SECONDS
15
+
16
+ The standard third-stop shutter times, seconds, from 1/8000 s to 30 s: the conventional markings of t = 2^(k/3) s
17
+ (1/8000 for 2^−13, 1/6 for 2^−8/3, 0.3 for 2^−5/3, 30 for 2^5; each within 7 % of the exact value). The generic bodies'
18
+ shutters are continuous over 1/8000–30 s and take any time as exact, so these are the values to offer in a third-stop
19
+ dial, not a restriction.
20
+
21
+ ```ts
22
+ GENERIC_THIRD_STOP_SHUTTER_SECONDS = Object.freeze([1 / 8000, 1 / 6400, 1 / 5000, 1 / 4000, 1 / 3200, 1 / 2500, 1 / 2000,
23
+ 1 / 1600, 1 / 1250, 1 / 1000, 1 / 800, 1 / 640, 1 / 500, 1 / 400, 1 / 320, 1 / 250, 1 / 200, 1 / 160, 1 / 125, 1 / 100, 1 / 80, 1 / 60, 1 / 50, 1 / 40,
24
+ 1 / 30, 1 / 25, 1 / 20, 1 / 15, 1 / 13, 1 / 10, 1 / 8, 1 / 6, 1 / 5, 1 / 4, 0.3, 0.4, 0.5, 0.6, 0.8, 1, 1.3, 1.6, 2, 2.5, 3.2, 4, 5, 6, 8, 10, 13,
25
+ 15, 20, 25, 30] as const)
26
+ ```
27
+
28
+ ## GENERIC_WHOLE_STOP_ISO_SETTINGS
29
+
30
+ Whole-stop ISO settings of the 2.0.0 bodies (each body's list stops at its maximum).
31
+
32
+ ```ts
33
+ GENERIC_WHOLE_STOP_ISO_SETTINGS = Object.freeze([100, 200, 400, 800, 1600, 3200, 6400, 12800, 25600] as const)
34
+ ```
35
+
5
36
  ## GenericEquipmentTier
6
37
 
7
38
  The exact contract is declared below; use the domain guide for assumptions and staged integration.
@@ -18,7 +49,7 @@ The exact contract is declared below; use the domain guide for assumptions and s
18
49
  export interface GenericEquipmentTierPreset {
19
50
  tier: GenericEquipmentTier;
20
51
  label: "Consumer" | "Prosumer" | "Professional";
21
- presetId: string; presetVersion: "2.0.0";
52
+ presetId: string; presetVersion: GenericTierPresetVersion;
22
53
  scientificStatus: "approximation";
23
54
  evidence: readonly EvidenceProvenance[];
24
55
  body: GenericTierBodyAssets;
@@ -78,6 +109,15 @@ export interface GenericTierLensAssets {
78
109
  }
79
110
  ```
80
111
 
112
+ ## GenericTierPresetVersion
113
+
114
+ The exact versions of the generic tier presets (no "latest" alias). 2.1.0 (2.14, round 5) adds the third-stop ISO
115
+ settings; 2.0.0 is kept, whole-stop, so saved selections and records of it still resolve to exactly what they named.
116
+
117
+ ```ts
118
+ export type GenericTierPresetVersion = "2.0.0" | "2.1.0";
119
+ ```
120
+
81
121
  ## GenericVersionedAsset
82
122
 
83
123
  The exact contract is declared below; use the domain guide for assumptions and staged integration.