@photivra/engine 2.19.0 → 2.20.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 (129) hide show
  1. package/CHANGELOG.md +112 -1
  2. package/CITATION.cff +1 -1
  3. package/NODE_SUPPORT.md +7 -5
  4. package/README.md +6 -6
  5. package/SECURITY.md +1 -1
  6. package/THIRD_PARTY.md +14 -10
  7. package/dist/capture/dng-lens-correction-opcodes.d.ts +32 -0
  8. package/dist/capture/dng-lens-correction-opcodes.js +119 -0
  9. package/dist/capture/dng-lens-correction-opcodes.js.map +1 -0
  10. package/dist/capture/native-environment-lean.js +1 -5
  11. package/dist/capture/native-environment-lean.js.map +1 -1
  12. package/dist/capture/native-raw-dng.d.ts +6 -0
  13. package/dist/capture/native-raw-dng.js +16 -2
  14. package/dist/capture/native-raw-dng.js.map +1 -1
  15. package/dist/capture/sensor-raw-producer.js +3 -5
  16. package/dist/capture/sensor-raw-producer.js.map +1 -1
  17. package/dist/core/shared-helpers.d.ts +2 -0
  18. package/dist/core/shared-helpers.js +4 -0
  19. package/dist/core/shared-helpers.js.map +1 -1
  20. package/dist/core/version.d.ts +1 -1
  21. package/dist/core/version.js +1 -1
  22. package/dist/core/version.js.map +1 -1
  23. package/dist/equipment/generic-lens-catalogue.d.ts +8 -2
  24. package/dist/equipment/generic-lens-catalogue.js +31 -2
  25. package/dist/equipment/generic-lens-catalogue.js.map +1 -1
  26. package/dist/equipment/generic-tier-assets.js +2 -3
  27. package/dist/equipment/generic-tier-assets.js.map +1 -1
  28. package/dist/equipment/generic-tier-presets.js +2 -3
  29. package/dist/equipment/generic-tier-presets.js.map +1 -1
  30. package/dist/facade/display-view-gpu.d.ts +7 -4
  31. package/dist/facade/display-view-gpu.js +4 -4
  32. package/dist/facade/display-view-gpu.js.map +1 -1
  33. package/dist/facade/display-view-lens-signal.d.ts +1 -1
  34. package/dist/facade/display-view-lens-signal.js +5 -3
  35. package/dist/facade/display-view-lens-signal.js.map +1 -1
  36. package/dist/facade/display-view.d.ts +3 -1
  37. package/dist/facade/display-view.js +1 -1
  38. package/dist/facade/display-view.js.map +1 -1
  39. package/dist/facade/fast-methods.d.ts +50 -0
  40. package/dist/facade/fast-methods.js +50 -0
  41. package/dist/facade/fast-methods.js.map +1 -0
  42. package/dist/facade/fast-resolution.d.ts +12 -0
  43. package/dist/facade/fast-resolution.js +18 -0
  44. package/dist/facade/fast-resolution.js.map +1 -0
  45. package/dist/facade/generic-lens-capture.d.ts +3 -2
  46. package/dist/facade/generic-lens-capture.js +3 -2
  47. package/dist/facade/generic-lens-capture.js.map +1 -1
  48. package/dist/facade/simulate-capture.d.ts +51 -11
  49. package/dist/facade/simulate-capture.js +144 -75
  50. package/dist/facade/simulate-capture.js.map +1 -1
  51. package/dist/facade/simulate-display-view.d.ts +3 -2
  52. package/dist/facade/simulate-display-view.js +22 -12
  53. package/dist/facade/simulate-display-view.js.map +1 -1
  54. package/dist/index.d.ts +7 -0
  55. package/dist/index.js +5 -0
  56. package/dist/index.js.map +1 -1
  57. package/dist/optics/aperture-diameter.d.ts +47 -0
  58. package/dist/optics/aperture-diameter.js +50 -0
  59. package/dist/optics/aperture-diameter.js.map +1 -0
  60. package/dist/optics/detail-limit.d.ts +182 -0
  61. package/dist/optics/detail-limit.js +336 -0
  62. package/dist/optics/detail-limit.js.map +1 -0
  63. package/dist/optics/generic-lens-psf.d.ts +18 -2
  64. package/dist/optics/generic-lens-psf.js +25 -2
  65. package/dist/optics/generic-lens-psf.js.map +1 -1
  66. package/dist/optics/scene-to-sensor-irradiance.d.ts +13 -0
  67. package/dist/optics/scene-to-sensor-irradiance.js +7 -0
  68. package/dist/optics/scene-to-sensor-irradiance.js.map +1 -1
  69. package/dist/output/crop-to-match-focal-length.d.ts +62 -0
  70. package/dist/output/crop-to-match-focal-length.js +63 -0
  71. package/dist/output/crop-to-match-focal-length.js.map +1 -0
  72. package/dist/output/developed-lens-corrections.js +2 -2
  73. package/dist/output/developed-lens-corrections.js.map +1 -1
  74. package/dist/output/generic-lens-corrections.d.ts +3 -1
  75. package/dist/output/generic-lens-corrections.js +8 -4
  76. package/dist/output/generic-lens-corrections.js.map +1 -1
  77. package/dist/output/lens-corrections.d.ts +12 -2
  78. package/dist/output/lens-corrections.js +22 -7
  79. package/dist/output/lens-corrections.js.map +1 -1
  80. package/dist/output/print-contrast-reference.js +1 -1
  81. package/dist/output/print-contrast-reference.js.map +1 -1
  82. package/dist/output/print-size-blur-noise.d.ts +110 -0
  83. package/dist/output/print-size-blur-noise.js +138 -0
  84. package/dist/output/print-size-blur-noise.js.map +1 -0
  85. package/dist/sensor/eqe-electron-rate.js +1 -7
  86. package/dist/sensor/eqe-electron-rate.js.map +1 -1
  87. package/dist/sensor/exposure-window.d.ts +2 -0
  88. package/dist/sensor/exposure-window.js +2 -2
  89. package/dist/sensor/exposure-window.js.map +1 -1
  90. package/dist/sensor/readout-timing.js +2 -23
  91. package/dist/sensor/readout-timing.js.map +1 -1
  92. package/dist/sensor/response-application-compatibility.js +1 -6
  93. package/dist/sensor/response-application-compatibility.js.map +1 -1
  94. package/dist/sensor/responsivity-photocurrent.js +1 -7
  95. package/dist/sensor/responsivity-photocurrent.js.map +1 -1
  96. package/dist/sensor/sensor-characteristics.d.ts +137 -0
  97. package/dist/sensor/sensor-characteristics.js +115 -0
  98. package/dist/sensor/sensor-characteristics.js.map +1 -0
  99. package/dist/sensor/spatial-sample-reduction.js +1 -5
  100. package/dist/sensor/spatial-sample-reduction.js.map +1 -1
  101. package/dist/sensor/spectral-quadrature.js +1 -6
  102. package/dist/sensor/spectral-quadrature.js.map +1 -1
  103. package/dist/sensor/temporal-photo-signal.d.ts +3 -0
  104. package/dist/sensor/temporal-photo-signal.js +5 -2
  105. package/dist/sensor/temporal-photo-signal.js.map +1 -1
  106. package/dist/stabilization/system.d.ts +9 -0
  107. package/dist/stabilization/system.js +5 -6
  108. package/dist/stabilization/system.js.map +1 -1
  109. package/docs/AGENT_GUIDE.md +4 -0
  110. package/docs/API_REFERENCE.md +76 -2
  111. package/docs/QUICK_START.md +88 -6
  112. package/docs/USAGE.md +65 -2
  113. package/docs/api/capture-native-raw-dng.md +6 -0
  114. package/docs/api/core-version.md +1 -1
  115. package/docs/api/equipment-generic-lens-catalogue.md +7 -2
  116. package/docs/api/exports.json +152 -2
  117. package/docs/api/facade-display-view-gpu.md +5 -2
  118. package/docs/api/facade-fast-methods.md +91 -0
  119. package/docs/api/facade-simulate-capture.md +36 -11
  120. package/docs/api/facade-simulate-display-view.md +3 -2
  121. package/docs/api/optics-aperture-diameter.md +51 -0
  122. package/docs/api/optics-detail-limit.md +203 -0
  123. package/docs/api/output-crop-to-match-focal-length.md +67 -0
  124. package/docs/api/output-generic-lens-corrections.md +3 -1
  125. package/docs/api/output-lens-corrections.md +11 -1
  126. package/docs/api/output-print-size-blur-noise.md +115 -0
  127. package/docs/api/sensor-sensor-characteristics.md +105 -0
  128. package/llms.txt +4 -0
  129. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"responsivity-photocurrent.js","sourceRoot":"","sources":["../../src/sensor/responsivity-photocurrent.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Responsivity photocurrent: converts light power to detector current using responsivity in amperes\n * per watt (A/W).\n *\n * For each wavelength bin, current = responsivity × radiant power; the bins are summed. The result is\n * the steady (quasi-static) current magnitude at the detector terminals. The electrical bias and load\n * conditions under which the responsivity applies are explicit. This is not EQE, an electron rate, a\n * transimpedance voltage or RAW. Fast-changing current needs a separate time-dependent detector model.\n * @see docs/MOTION_AND_SIGNAL.md for equations, coordinate/unit conventions, blockers and support\n * limits.\n */\n\nimport {\n approximationResult,\n type CalculationResult\n} from \"../core/calculation-result.js\";\nimport { stringifyCanonicalJson } from \"../core/canonical-json.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport {\n parseEvidenceList,\n type EvidenceProvenance\n} from \"../core/evidence-provenance.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport {\n parseSensorColorSamplingProfile,\n type SensorColorSamplingProfile\n} from \"./color-sampling.js\";\nimport type {\n SensorResponseApplicationCompatibilityAssessment\n} from \"./response-application-compatibility.js\";\nimport type {\n SensorResponseOperatingRangeAssessment\n} from \"./response-operating-range.js\";\nimport {\n createSensorSpectralResponseChannelBinding,\n requireCurrentSensorSpectralResponseChannelBinding,\n parseSensorSpectralResponseProfile,\n resolveParsedSensorSpectralResponseAtWavelength,\n type ResolvedSensorSpectralResponse,\n type SensorSpectralResponseProfile,\n type SensorSpectralResponseScientificStatus,\n type SensorSpectralResponseUncertainty\n} from \"./spectral-response.js\";\nimport type {\n SensorSpatioSpectralIrradianceReduction,\n SensorSpatioSpectralWavelengthReduction\n} from \"./spatio-spectral-reduction.js\";\nimport { requireInputObject, requireInputKeys, rejectUnknownVariantFields, requireConfigRecord as requireRecord, requireConfigString as requireNonEmptyString, requireConfigPositiveFinite as requirePositiveFiniteConfiguration, requireInputNonNegativeFinite as requireNonNegativeFiniteInput, requireInputPositiveFinite as requirePositiveFiniteInput, requireInputNonNegativeSafeInteger as requireSafeNonNegativeInteger } from \"../core/record-validation.js\";\nimport { addCompensated } from \"../core/shared-helpers.js\";\nimport { parseReferenceConditionPolicy as parseConditionPolicy } from \"./response-application-compatibility.js\";\nimport { parseUncertainty } from \"./response-operating-range.js\";\n\nconst REDUCTION_TOLERANCE = 1e-10;\n\nexport type SensorResponsivityBiasCondition =\n | {\n kind: \"zero-bias-photovoltaic\";\n }\n | {\n kind: \"reverse-biased\";\n magnitudeVolts: number;\n };\n\nexport type SensorResponsivityReadoutLoadCondition =\n | {\n kind: \"virtual-ground-current-readout\";\n }\n | {\n kind: \"finite-input-impedance\";\n inputImpedanceOhms: number;\n };\n\nexport interface SensorResponsivityElectricalConditions {\n bias: SensorResponsivityBiasCondition;\n readoutLoad:\n SensorResponsivityReadoutLoadCondition;\n}\n\nexport type SensorResponsivityElectricalConditionPolicy =\n | {\n kind: \"exact-match-required\";\n }\n | {\n kind: \"assume-compatible\";\n limitation: string;\n evidence: readonly EvidenceProvenance[];\n };\n\nexport interface SensorResponsivityElectricalApplicabilityProfile {\n schemaVersion: \"0.1.0\";\n profileId: string;\n responseApplicationProfileId: string;\n spectralResponseProfileId: string;\n colorSamplingProfileId: string;\n channelId: string;\n outputMeaning:\n \"detector-terminal-photocurrent-magnitude\";\n referenceConditions:\n SensorResponsivityElectricalConditions;\n conditionPolicy:\n SensorResponsivityElectricalConditionPolicy;\n scientificStatus:\n SensorSpectralResponseScientificStatus;\n uncertainty:\n SensorSpectralResponseUncertainty;\n evidence: readonly EvidenceProvenance[];\n}\n\nexport interface CalculateSensorResponsivityPhotocurrentInput {\n reduction:\n SensorSpatioSpectralIrradianceReduction;\n compatibility:\n SensorResponseApplicationCompatibilityAssessment;\n operatingRange:\n SensorResponseOperatingRangeAssessment;\n colorSamplingProfile:\n SensorColorSamplingProfile;\n spectralResponseProfile:\n SensorSpectralResponseProfile;\n electricalApplicabilityProfile:\n SensorResponsivityElectricalApplicabilityProfile;\n operatingElectricalConditions:\n SensorResponsivityElectricalConditions;\n}\n\nexport interface SensorResponsivityWavelengthCurrentContribution {\n spectralSampleIndex: number;\n wavelengthNanometers: number;\n wavelengthMeasureNanometers: number;\n geometricApertureIncidentSpectralFluxWattsPerNanometer:\n number;\n radiantPowerContributionWatts: number;\n spectralResponsivityAmperesPerWatt:\n number;\n responseInterpolationUsed: boolean;\n photocurrentMagnitudeContributionAmperes:\n number;\n}\n\nexport interface SensorResponsivityPhotocurrent {\n colorSamplingProfileId: string;\n responseProfileId: string;\n responseApplicationProfileId: string;\n operatingRangeProfileId: string;\n electricalApplicabilityProfileId:\n string;\n /** The exact source colour site. Results produced by the engine fill this in. */\n site?: {\n x: number;\n y: number;\n };\n channelId: string;\n sourceResponseKind:\n \"effective-spectral-responsivity\";\n responseScope:\n SensorSpatioSpectralIrradianceReduction[\"responseScope\"];\n responseReferencePlane:\n SensorResponseApplicationCompatibilityAssessment[\"requiredSourcePlane\"];\n wavelengthBasis:\n SensorSpatioSpectralIrradianceReduction[\"wavelengthBasis\"];\n wavelengthRangeNanometers:\n SensorSpatioSpectralIrradianceReduction[\"wavelengthRangeNanometers\"];\n electricalReferenceConditions:\n SensorResponsivityElectricalConditions;\n operatingElectricalConditions:\n SensorResponsivityElectricalConditions;\n electricalConditionPolicy:\n SensorResponsivityElectricalConditionPolicy;\n electricalCompatibility:\n | \"exact-match\"\n | \"assumed-compatible\";\n spectralNodeCount: number;\n perWavelength:\n readonly SensorResponsivityWavelengthCurrentContribution[];\n photocurrentMagnitudeAmperes: number;\n currentSignConvention:\n \"magnitude-only-no-circuit-polarity\";\n summationMethod:\n \"kahan-compensated\";\n responseApplicationPerformed: true;\n spectralResponsivityApplied: true;\n quantumEfficiencyApplied: false;\n photonRateCalculated: false;\n electronRateCalculated: false;\n currentCalculated: true;\n chargeCalculated: false;\n temporalResponseModel:\n \"quasi-static-steady-state-only\";\n detectorBandwidthModeled: false;\n transimpedanceGainApplied: false;\n voltageCalculated: false;\n temporalIntegrationApplied: false;\n exposureDurationApplied: false;\n saturationAssessed: false;\n readoutElectronicsLinearityAssessed:\n false;\n shotNoiseApplied: false;\n readNoiseApplied: false;\n adcQuantizationApplied: false;\n rawCodeValueProduced: false;\n responseUncertaintyPropagated: false;\n electricalApplicabilityUncertaintyPropagated:\n false;\n quadratureConvergenceErrorEstimated:\n false;\n componentEvidence: {\n electricalApplicability:\n readonly EvidenceProvenance[];\n electricalConditionAssumption:\n readonly EvidenceProvenance[];\n };\n}\n\ninterface CompensatedSum {\n sum: number;\n correction: number;\n}\n\nfunction parseElectricalConditions(\n value: unknown,\n path: string\n): SensorResponsivityElectricalConditions {\n const record = requireRecord(\n value,\n path, [\"bias\", \"readoutLoad\"]\n );\n const bias = requireRecord(\n record.bias,\n path + \".bias\", null\n );\n rejectUnknownVariantFields(bias, \"kind\", { \"zero-bias-photovoltaic\": [\"kind\"], \"reverse-biased\": [\"kind\", \"magnitudeVolts\"] }, path + \".bias\");\n const readoutLoad = requireRecord(\n record.readoutLoad,\n path + \".readoutLoad\", null\n );\n rejectUnknownVariantFields(readoutLoad, \"kind\", { \"virtual-ground-current-readout\": [\"kind\"], \"finite-input-impedance\": [\"kind\", \"inputImpedanceOhms\"] }, path + \".readoutLoad\");\n\n const parsedBias:\n SensorResponsivityBiasCondition =\n bias.kind ===\n \"zero-bias-photovoltaic\"\n ? {\n kind:\n \"zero-bias-photovoltaic\"\n }\n : bias.kind ===\n \"reverse-biased\"\n ? {\n kind:\n \"reverse-biased\",\n magnitudeVolts:\n requirePositiveFiniteConfiguration(\n bias.magnitudeVolts,\n path +\n \".bias.magnitudeVolts\"\n )\n }\n : ((): never => {\n throw new InvalidConfigurationError(\n path +\n \".bias.kind is invalid.\"\n );\n })();\n\n const parsedReadout:\n SensorResponsivityReadoutLoadCondition =\n readoutLoad.kind ===\n \"virtual-ground-current-readout\"\n ? {\n kind:\n \"virtual-ground-current-readout\"\n }\n : readoutLoad.kind ===\n \"finite-input-impedance\"\n ? {\n kind:\n \"finite-input-impedance\",\n inputImpedanceOhms:\n requirePositiveFiniteConfiguration(\n readoutLoad\n .inputImpedanceOhms,\n path +\n \".readoutLoad.inputImpedanceOhms\"\n )\n }\n : ((): never => {\n throw new InvalidConfigurationError(\n path +\n \".readoutLoad.kind is invalid.\"\n );\n })();\n\n return {\n bias: parsedBias,\n readoutLoad: parsedReadout\n };\n}\n\n/**\n * Validates untrusted input and returns it as a normalized, typed value. Unknown enum values,\n * missing required fields and inconsistent scientific data are rejected.\n *\n * The profile states the electrical bias and load conditions under which an A/W responsivity applies.\n * @param value - Untrusted data. TypeScript types alone are not validation.\n * @returns The result. Success does not imply calibration.\n *\n * @see docs/API_REFERENCE.md for the package export and its full type definitions.\n */\nexport function parseSensorResponsivityElectricalApplicabilityProfile(\n value: unknown\n): SensorResponsivityElectricalApplicabilityProfile {\n const record = requireRecord(\n value,\n \"sensorResponsivityElectricalApplicability\", [\"schemaVersion\", \"outputMeaning\", \"scientificStatus\", \"uncertainty\", \"profileId\", \"responseApplicationProfileId\", \"spectralResponseProfileId\", \"colorSamplingProfileId\", \"channelId\", \"referenceConditions\", \"conditionPolicy\", \"evidence\"]\n );\n\n if (\n record.schemaVersion !== \"0.1.0\"\n ) {\n throw new InvalidConfigurationError(\n 'sensorResponsivityElectricalApplicability.schemaVersion must be \"0.1.0\".'\n );\n }\n if (\n record.outputMeaning !==\n \"detector-terminal-photocurrent-magnitude\"\n ) {\n throw new InvalidConfigurationError(\n \"sensorResponsivityElectricalApplicability.outputMeaning is invalid.\"\n );\n }\n if (\n record.scientificStatus !==\n \"calibrated\" &&\n record.scientificStatus !==\n \"approximation\"\n ) {\n throw new InvalidConfigurationError(\n \"sensorResponsivityElectricalApplicability.scientificStatus is invalid.\"\n );\n }\n\n const uncertainty =\n parseUncertainty(\n record.uncertainty,\n \"sensorResponsivityElectricalApplicability.uncertainty\"\n );\n\n if (\n record.scientificStatus ===\n \"calibrated\" &&\n uncertainty.kind ===\n \"not-quantified\"\n ) {\n throw new InvalidConfigurationError(\n \"A calibrated electrical applicability profile must declare quantified relative uncertainty.\"\n );\n }\n\n return {\n schemaVersion: \"0.1.0\",\n profileId:\n requireNonEmptyString(\n record.profileId,\n \"sensorResponsivityElectricalApplicability.profileId\"\n ),\n responseApplicationProfileId:\n requireNonEmptyString(\n record\n .responseApplicationProfileId,\n \"sensorResponsivityElectricalApplicability.responseApplicationProfileId\"\n ),\n spectralResponseProfileId:\n requireNonEmptyString(\n record\n .spectralResponseProfileId,\n \"sensorResponsivityElectricalApplicability.spectralResponseProfileId\"\n ),\n colorSamplingProfileId:\n requireNonEmptyString(\n record.colorSamplingProfileId,\n \"sensorResponsivityElectricalApplicability.colorSamplingProfileId\"\n ),\n channelId:\n requireNonEmptyString(\n record.channelId,\n \"sensorResponsivityElectricalApplicability.channelId\"\n ),\n outputMeaning:\n \"detector-terminal-photocurrent-magnitude\",\n referenceConditions:\n parseElectricalConditions(\n record.referenceConditions,\n \"sensorResponsivityElectricalApplicability.referenceConditions\"\n ),\n conditionPolicy:\n parseConditionPolicy(\n record.conditionPolicy,\n \"sensorResponsivityElectricalApplicability.conditionPolicy\"\n ),\n scientificStatus:\n record.scientificStatus,\n uncertainty,\n evidence:\n parseEvidenceList(\n record.evidence,\n \"sensorResponsivityElectricalApplicability.evidence\"\n )\n };\n}\n\n/**\n * Identity string for parsed plain data that ignores key order, so records with\n * the same content compare equal whatever order their properties were added in.\n */\nfunction canonicalJson(\n value: unknown\n): string {\n if (value === undefined) return \"undefined\";\n return stringifyCanonicalJson(value, {\n undefinedObjectProperties: \"omit\",\n nonFiniteNumberMessage:\n \"A/W photocurrent identity data must contain finite numbers.\",\n unsupportedValueMessage:\n \"A/W photocurrent identity data must be plain JSON data.\"\n });\n}\n\nfunction approximatelyEqual(\n actual: number,\n expected: number\n): boolean {\n const scale = Math.max(\n 1,\n Math.abs(actual),\n Math.abs(expected)\n );\n return (\n Math.abs(actual - expected) <=\n REDUCTION_TOLERANCE * scale\n );\n}\n\nfunction electricalConditionsMatch(\n reference:\n SensorResponsivityElectricalConditions,\n operating:\n SensorResponsivityElectricalConditions\n): boolean {\n return (\n canonicalJson(reference) ===\n canonicalJson(operating)\n );\n}\n\nfunction validateAuthorizationChain(\n reduction:\n SensorSpatioSpectralIrradianceReduction,\n compatibility:\n SensorResponseApplicationCompatibilityAssessment,\n operatingRange:\n SensorResponseOperatingRangeAssessment\n): void {\n requireInputObject(reduction, \"reduction input\");\n if (\n reduction.outputMeaning !==\n \"pre-response-spatio-spectral-radiometric-reduction\" ||\n reduction\n .spectralResponseApplicationPerformed !==\n false ||\n reduction\n .spectralResponsivityApplied !==\n false ||\n reduction.temporalIntegrationApplied !==\n false\n ) {\n throw new InvalidScientificInputError(\n \"reduction must remain pre-response and pre-temporal before A/W photocurrent conversion.\"\n );\n }\n\n if (\n !compatibility\n .structuralCompatibilityEstablished ||\n compatibility.compatibilityStatus ===\n \"blocked\" ||\n compatibility.blockers.length !== 0 ||\n !compatibility.sourcePlaneMatched ||\n compatibility.requiredSignalPath !==\n \"radiant-power-to-current\"\n ) {\n throw new InvalidScientificInputError(\n \"compatibility must establish a non-blocked radiant-power-to-current response path.\"\n );\n }\n\n if (\n compatibility\n .spectralResponseProfileId !==\n reduction.responseProfileId ||\n compatibility.colorSamplingProfileId !==\n reduction.colorSamplingProfileId ||\n compatibility.channelId !==\n reduction.channelId ||\n compatibility.responseScope !==\n reduction.responseScope\n ) {\n throw new InvalidScientificInputError(\n \"compatibility identities must exactly match the reduction.\"\n );\n }\n\n if (\n !operatingRange\n .operatingRangeCompatibilityAssessed ||\n !operatingRange\n .instantaneousResponseRangeCompatible ||\n !operatingRange\n .responseRateConversionAuthorized ||\n operatingRange.status === \"blocked\" ||\n operatingRange.blockers.length !== 0 ||\n operatingRange.requiredSignalPath !==\n \"radiant-power-to-current\" ||\n operatingRange\n .responseApplicationPerformed !==\n false ||\n operatingRange\n .currentConversionPerformed !== false ||\n operatingRange\n .temporalIntegrationAuthorized !== false ||\n !operatingRange\n .spectralNodeRangeCompatibilityAssessed ||\n operatingRange\n .spectralInputModel.kind !==\n \"per-spectral-bin\" ||\n operatingRange\n .spatialLinearityModel.kind !==\n \"linear-superposition-over-geometric-aperture\"\n ) {\n throw new InvalidScientificInputError(\n \"operatingRange must authorize a clean per-bin radiant-power-to-current path.\"\n );\n }\n\n if (\n operatingRange\n .spectralResponseProfileId !==\n reduction.responseProfileId ||\n operatingRange\n .colorSamplingProfileId !==\n reduction.colorSamplingProfileId ||\n operatingRange.channelId !==\n reduction.channelId ||\n operatingRange\n .responseApplicationProfileId !==\n compatibility.applicationProfileId\n ) {\n throw new InvalidScientificInputError(\n \"operatingRange identities must exactly match the reduction and compatibility assessment.\"\n );\n }\n}\n\nfunction validateReductionNodes(\n reduction:\n SensorSpatioSpectralIrradianceReduction\n): readonly SensorSpatioSpectralWavelengthReduction[] {\n if (\n !Array.isArray(\n reduction.perWavelength\n ) ||\n !Number.isSafeInteger(\n reduction.spectralNodeCount\n ) ||\n reduction.spectralNodeCount <= 0 ||\n reduction.perWavelength.length !==\n reduction.spectralNodeCount\n ) {\n throw new InvalidScientificInputError(\n \"reduction.perWavelength must contain exactly spectralNodeCount entries.\"\n );\n }\n\n let priorWavelength =\n Number.NEGATIVE_INFINITY;\n let normalizedWeightSum = 0;\n const totalPower:\n CompensatedSum = {\n sum: 0,\n correction: 0\n };\n\n reduction.perWavelength.forEach(\n (node, arrayIndex) => {\n const sampleIndex =\n requireSafeNonNegativeInteger(\n node.spectralSampleIndex,\n \"reduction.perWavelength[\" +\n arrayIndex +\n \"].spectralSampleIndex\"\n );\n if (sampleIndex !== arrayIndex) {\n throw new InvalidScientificInputError(\n \"reduction spectralSampleIndex values must form the exact contiguous range 0..spectralNodeCount-1.\"\n );\n }\n\n const wavelength =\n requirePositiveFiniteInput(\n node.wavelengthNanometers,\n \"reduction.perWavelength[\" +\n arrayIndex +\n \"].wavelengthNanometers\"\n );\n if (\n wavelength <= priorWavelength\n ) {\n throw new InvalidScientificInputError(\n \"reduction wavelength nodes must be strictly increasing.\"\n );\n }\n priorWavelength = wavelength;\n\n const measure =\n requirePositiveFiniteInput(\n node.wavelengthMeasureNanometers,\n \"reduction.perWavelength[\" +\n arrayIndex +\n \"].wavelengthMeasureNanometers\"\n );\n const normalizedWeight =\n requirePositiveFiniteInput(\n node.normalizedWavelengthWeight,\n \"reduction.perWavelength[\" +\n arrayIndex +\n \"].normalizedWavelengthWeight\"\n );\n const fluxDensity =\n requireNonNegativeFiniteInput(\n node\n .geometricApertureIncidentSpectralFluxWattsPerNanometer,\n \"reduction.perWavelength[\" +\n arrayIndex +\n \"].geometricApertureIncidentSpectralFluxWattsPerNanometer\"\n );\n const power =\n requireNonNegativeFiniteInput(\n node\n .wavelengthIntegratedGeometricApertureIncidentFluxContributionWatts,\n \"reduction.perWavelength[\" +\n arrayIndex +\n \"].wavelengthIntegratedGeometricApertureIncidentFluxContributionWatts\"\n );\n const expected =\n fluxDensity * measure;\n if (\n !Number.isFinite(expected) ||\n !approximatelyEqual(\n power,\n expected\n )\n ) {\n throw new InvalidScientificInputError(\n \"reduction per-wavelength radiant-power contribution must equal spectral flux density × d-lambda.\"\n );\n }\n\n normalizedWeightSum +=\n normalizedWeight;\n addCompensated(\n totalPower,\n power\n );\n }\n );\n\n if (\n !Number.isFinite(\n normalizedWeightSum\n ) ||\n !approximatelyEqual(\n normalizedWeightSum,\n 1\n )\n ) {\n throw new InvalidScientificInputError(\n \"reduction normalized wavelength weights must sum to 1.\"\n );\n }\n\n const reductionPower =\n requireNonNegativeFiniteInput(\n reduction\n .wavelengthIntegratedGeometricApertureIncidentFluxWatts,\n \"reduction.wavelengthIntegratedGeometricApertureIncidentFluxWatts\"\n );\n if (\n !approximatelyEqual(\n totalPower.sum,\n reductionPower\n )\n ) {\n throw new InvalidScientificInputError(\n \"reduction total geometric-aperture radiant power must match the per-wavelength sum.\"\n );\n }\n\n return reduction.perWavelength;\n}\n\nfunction validateOperatingRangeNodeBinding(\n reduction:\n SensorSpatioSpectralIrradianceReduction,\n nodes:\n readonly SensorSpatioSpectralWavelengthReduction[],\n operatingRange:\n SensorResponseOperatingRangeAssessment\n): void {\n const evaluated =\n operatingRange\n .evaluatedSpectralNodeInputs;\n\n if (\n evaluated === undefined ||\n evaluated.length !== nodes.length\n ) {\n throw new InvalidScientificInputError(\n \"operatingRange must carry one evaluated spectral-node input per reduction wavelength node.\"\n );\n }\n\n if (\n operatingRange\n .evaluatedWavelengthRangeNanometers\n .minimum !==\n reduction.wavelengthRangeNanometers\n .minimum ||\n operatingRange\n .evaluatedWavelengthRangeNanometers\n .maximum !==\n reduction.wavelengthRangeNanometers\n .maximum\n ) {\n throw new InvalidScientificInputError(\n \"operatingRange wavelength range must exactly match the reduction.\"\n );\n }\n\n evaluated.forEach(\n (entry, index) => {\n const node = nodes[index]!;\n if (\n entry.spectralSampleIndex !==\n node.spectralSampleIndex ||\n entry.wavelengthNanometers !==\n node.wavelengthNanometers ||\n entry.wavelengthMeasureNanometers !==\n node.wavelengthMeasureNanometers\n ) {\n throw new InvalidScientificInputError(\n \"operatingRange spectral-node identities must exactly match the reduction.\"\n );\n }\n\n const expected =\n operatingRange.inputRange.kind ===\n \"wavelength-integrated-geometric-aperture-radiant-power\"\n ? node\n .wavelengthIntegratedGeometricApertureIncidentFluxContributionWatts\n : node\n .wavelengthIntegratedSpatialAverageContributionWattsPerSquareMeter;\n\n if (\n entry.kind !==\n operatingRange.inputRange.kind ||\n entry.unit !==\n operatingRange.inputRange.unit ||\n !approximatelyEqual(\n entry.value,\n expected\n )\n ) {\n throw new InvalidScientificInputError(\n \"operatingRange spectral-node values must match the reduction values in the calibrated input domain.\"\n );\n }\n }\n );\n}\n\nfunction validateResponseBinding(\n resolved:\n ResolvedSensorSpectralResponse,\n reduction:\n SensorSpatioSpectralIrradianceReduction\n): void {\n if (\n resolved.profileId !==\n reduction.responseProfileId ||\n resolved.colorSamplingProfileId !==\n reduction.colorSamplingProfileId ||\n resolved.channelId !==\n reduction.channelId ||\n resolved.sourceResponseKind !==\n reduction.sourceResponseKind ||\n resolved.responseScope !==\n reduction.responseScope ||\n resolved.scientificStatus !==\n reduction.responseScientificStatus ||\n canonicalJson(\n resolved.uncertainty\n ) !==\n canonicalJson(\n reduction.responseUncertainty\n ) ||\n canonicalJson(\n resolved.componentEvidence\n ) !==\n canonicalJson({\n profile:\n reduction.componentEvidence\n .spectral.profile,\n channel:\n reduction.componentEvidence\n .spectral.channel,\n curves:\n reduction.componentEvidence\n .spectral.curves\n })\n ) {\n throw new InvalidScientificInputError(\n \"Resolved A/W response calibration metadata/evidence must exactly match the reduction.\"\n );\n }\n}\n\nfunction validateExactResponseDataBinding(\n responseProfile:\n SensorSpectralResponseProfile,\n reduction:\n SensorSpatioSpectralIrradianceReduction\n): void {\n const response =\n responseProfile.channels.find(\n (entry) =>\n entry.channelId ===\n reduction.channelId\n );\n if (reduction.responseChannelBinding !== undefined) {\n requireCurrentSensorSpectralResponseChannelBinding(\n reduction.responseChannelBinding\n );\n }\n if (\n response === undefined ||\n reduction.responseChannelBinding ===\n undefined ||\n canonicalJson(\n createSensorSpectralResponseChannelBinding(\n response\n )\n ) !==\n canonicalJson(\n reduction.responseChannelBinding\n )\n ) {\n throw new InvalidScientificInputError(\n \"Supplied spectral response channel data must exactly match the response-channel binding carried by the reduction.\"\n );\n }\n}\n\nfunction validateElectricalBinding(\n profile:\n SensorResponsivityElectricalApplicabilityProfile,\n input:\n CalculateSensorResponsivityPhotocurrentInput,\n operatingElectricalConditions:\n SensorResponsivityElectricalConditions\n): {\n compatibility:\n \"exact-match\" |\n \"assumed-compatible\";\n assumptionEvidence:\n readonly EvidenceProvenance[];\n} {\n if (\n profile.responseApplicationProfileId !==\n input.compatibility\n .applicationProfileId ||\n profile.spectralResponseProfileId !==\n input.reduction\n .responseProfileId ||\n profile.colorSamplingProfileId !==\n input.reduction\n .colorSamplingProfileId ||\n profile.channelId !==\n input.reduction.channelId\n ) {\n throw new InvalidScientificInputError(\n \"Electrical applicability profile identities must exactly match the response pipeline.\"\n );\n }\n\n if (\n profile.conditionPolicy.kind ===\n \"exact-match-required\"\n ) {\n if (\n !electricalConditionsMatch(\n profile.referenceConditions,\n operatingElectricalConditions\n )\n ) {\n throw new InvalidScientificInputError(\n \"Operating electrical conditions must exactly match the A/W calibration reference conditions.\"\n );\n }\n return {\n compatibility: \"exact-match\",\n assumptionEvidence: []\n };\n }\n\n return {\n compatibility:\n \"assumed-compatible\",\n assumptionEvidence:\n profile.conditionPolicy.evidence\n };\n}\n\n/**\n * Sums responsivity (A/W) × radiant power over wavelength bins to get the steady current magnitude,\n * under explicit electrical conditions.\n *\n * This is not EQE, an electron rate, a transimpedance voltage or RAW. Fast-changing current needs a\n * separate time-dependent detector model.\n * @param input - The input; see `CalculateSensorResponsivityPhotocurrentInput` for its fields, units and coordinates.\n * @returns The result. Success does not imply calibration.\n *\n * @see docs/API_REFERENCE.md for the package export and its full type definitions.\n */\nexport function calculateSensorResponsivityPhotocurrent(\n input:\n CalculateSensorResponsivityPhotocurrentInput\n): CalculationResult<SensorResponsivityPhotocurrent> {\n requireInputKeys(input, \"calculateSensorResponsivityPhotocurrent\", [\"reduction\", \"compatibility\", \"operatingRange\", \"colorSamplingProfile\", \"spectralResponseProfile\", \"electricalApplicabilityProfile\", \"operatingElectricalConditions\"]);\n validateAuthorizationChain(\n input.reduction,\n input.compatibility,\n input.operatingRange\n );\n\n if (\n input.reduction\n .sourceResponseKind !==\n \"effective-spectral-responsivity\"\n ) {\n throw new InvalidScientificInputError(\n \"A/W photocurrent conversion requires effective spectral responsivity, not EQE.\"\n );\n }\n\n const colorProfile =\n parseSensorColorSamplingProfile(\n input.colorSamplingProfile\n );\n const responseProfile =\n parseSensorSpectralResponseProfile(\n input.spectralResponseProfile\n );\n const electricalProfile =\n parseSensorResponsivityElectricalApplicabilityProfile(\n input.electricalApplicabilityProfile\n );\n\n if (\n colorProfile.profileId !==\n input.reduction\n .colorSamplingProfileId ||\n responseProfile.profileId !==\n input.reduction\n .responseProfileId ||\n responseProfile\n .colorSamplingProfileId !==\n colorProfile.profileId\n ) {\n throw new InvalidScientificInputError(\n \"Supplied color/spectral response profiles must exactly match the reduction identities.\"\n );\n }\n\n validateExactResponseDataBinding(\n responseProfile,\n input.reduction\n );\n\n const operatingElectricalConditions =\n parseElectricalConditions(\n input.operatingElectricalConditions,\n \"operatingElectricalConditions\"\n );\n const electrical =\n validateElectricalBinding(\n electricalProfile,\n input,\n operatingElectricalConditions\n );\n const nodes =\n validateReductionNodes(\n input.reduction\n );\n validateOperatingRangeNodeBinding(\n input.reduction,\n nodes,\n input.operatingRange\n );\n\n const currentSum:\n CompensatedSum = {\n sum: 0,\n correction: 0\n };\n const perWavelength:\n SensorResponsivityWavelengthCurrentContribution[] =\n [];\n let responseBindingValidated =\n false;\n\n for (const node of nodes) {\n const resolved =\n resolveParsedSensorSpectralResponseAtWavelength({\n colorSamplingProfile:\n colorProfile,\n spectralResponseProfile:\n responseProfile,\n channelId:\n input.reduction.channelId,\n wavelengthNanometers:\n node.wavelengthNanometers,\n wavelengthBasis:\n input.reduction\n .wavelengthBasis\n });\n\n if (!responseBindingValidated) {\n validateResponseBinding(\n resolved.value,\n input.reduction\n );\n responseBindingValidated =\n true;\n }\n\n const responsivity =\n resolved.value.response as Extract<\n typeof resolved.value.response,\n {\n kind:\n \"effective-spectral-responsivity\";\n }\n >;\n\n const radiantPowerContributionWatts =\n node\n .geometricApertureIncidentSpectralFluxWattsPerNanometer *\n node.wavelengthMeasureNanometers;\n const photocurrentMagnitudeContributionAmperes =\n radiantPowerContributionWatts *\n responsivity.amperesPerWatt;\n\n if (\n !Number.isFinite(\n radiantPowerContributionWatts\n ) ||\n !Number.isFinite(\n photocurrentMagnitudeContributionAmperes\n ) ||\n photocurrentMagnitudeContributionAmperes <\n 0\n ) {\n throw new InvalidScientificInputError(\n \"A/W wavelength-node photocurrent calculation must remain finite and nonnegative.\"\n );\n }\n\n addCompensated(\n currentSum,\n photocurrentMagnitudeContributionAmperes\n );\n\n perWavelength.push({\n spectralSampleIndex:\n node.spectralSampleIndex,\n wavelengthNanometers:\n node.wavelengthNanometers,\n wavelengthMeasureNanometers:\n node.wavelengthMeasureNanometers,\n geometricApertureIncidentSpectralFluxWattsPerNanometer:\n node\n .geometricApertureIncidentSpectralFluxWattsPerNanometer,\n radiantPowerContributionWatts,\n spectralResponsivityAmperesPerWatt:\n responsivity.amperesPerWatt,\n responseInterpolationUsed:\n resolved.value\n .interpolationUsed,\n photocurrentMagnitudeContributionAmperes\n });\n }\n\n if (\n !Number.isFinite(\n currentSum.sum\n ) ||\n currentSum.sum < 0\n ) {\n throw new InvalidScientificInputError(\n \"Total A/W photocurrent magnitude must remain finite and nonnegative.\"\n );\n }\n\n return approximationResult(\n {\n colorSamplingProfileId:\n input.reduction\n .colorSamplingProfileId,\n responseProfileId:\n input.reduction\n .responseProfileId,\n responseApplicationProfileId:\n input.compatibility\n .applicationProfileId,\n operatingRangeProfileId:\n input.operatingRange\n .operatingRangeProfileId,\n electricalApplicabilityProfileId:\n electricalProfile.profileId,\n site: {\n ...input.reduction.site\n },\n channelId:\n input.reduction.channelId,\n sourceResponseKind:\n \"effective-spectral-responsivity\",\n responseScope:\n input.reduction.responseScope,\n responseReferencePlane:\n input.compatibility\n .requiredSourcePlane,\n wavelengthBasis:\n input.reduction\n .wavelengthBasis,\n wavelengthRangeNanometers: {\n ...input.reduction\n .wavelengthRangeNanometers\n },\n electricalReferenceConditions:\n electricalProfile\n .referenceConditions,\n operatingElectricalConditions,\n electricalConditionPolicy:\n electricalProfile\n .conditionPolicy,\n electricalCompatibility:\n electrical.compatibility,\n spectralNodeCount:\n nodes.length,\n perWavelength,\n photocurrentMagnitudeAmperes:\n currentSum.sum,\n currentSignConvention:\n \"magnitude-only-no-circuit-polarity\",\n summationMethod:\n \"kahan-compensated\",\n responseApplicationPerformed:\n true,\n spectralResponsivityApplied:\n true,\n quantumEfficiencyApplied:\n false,\n photonRateCalculated: false,\n electronRateCalculated: false,\n currentCalculated: true,\n chargeCalculated: false,\n temporalResponseModel:\n \"quasi-static-steady-state-only\",\n detectorBandwidthModeled: false,\n transimpedanceGainApplied:\n false,\n voltageCalculated: false,\n temporalIntegrationApplied:\n false,\n exposureDurationApplied: false,\n saturationAssessed: false,\n readoutElectronicsLinearityAssessed:\n false,\n shotNoiseApplied: false,\n readNoiseApplied: false,\n adcQuantizationApplied:\n false,\n rawCodeValueProduced: false,\n responseUncertaintyPropagated:\n false,\n electricalApplicabilityUncertaintyPropagated:\n false,\n quadratureConvergenceErrorEstimated:\n false,\n componentEvidence: {\n electricalApplicability:\n electricalProfile.evidence,\n electricalConditionAssumption:\n electrical\n .assumptionEvidence\n }\n },\n \"sensor-spectral-responsivity-photocurrent\",\n \"1.0.0\",\n [\n \"Spectral responsivity in A/W is applied independently at every wavelength quadrature node as detector-terminal photocurrent magnitude per incident radiant power.\",\n \"Current sign/polarity is deliberately not modeled; the result is a nonnegative photocurrent magnitude and does not imply a circuit current direction.\",\n \"The A/W calibration is bound to explicit detector electrical conditions. Exact-match mode requires identical bias and readout-load semantics; approximation mode requires evidence and a stated limitation.\",\n \"Virtual-ground current readout and finite input impedance are modeled as calibration-condition identities only; this function does not simulate amplifier circuits or prove impedance adequacy.\",\n \"NIST photodiode guidance shows detector/electronics/loading can affect linear operation, so transimpedance/current-readout conditions are not treated as scientifically inert.\",\n \"Structural compatibility, geometric-aperture linear superposition and per-spectral-bin operating-range applicability must already authorize the radiant-power→current path.\",\n \"Response metadata/evidence and operating-range spectral-node values are rebound to the exact reduction so matching IDs alone or stale authorization cannot enable conversion.\",\n \"The calculation does not use photon energy, QE, photon rate or electron rate. A/W remains a distinct current-domain response representation.\",\n \"No transimpedance gain or voltage conversion is performed; current-to-voltage converter gain/offset/linearity remain downstream electronics contracts.\",\n \"The A/W response is used only as a quasi-static steady-state power→current mapping. Detector impulse response, bandwidth, modulation-frequency response and transient settling are not modeled.\",\n \"Outputs are instantaneous current only. Exposure-time integration to charge, saturation, noise, ADC/RAW conversion and reconstruction remain downstream.\",\n \"Response/electrical uncertainties and quadrature convergence error are not propagated into a combined current uncertainty by this version.\"\n ]\n );\n}\n"]}
1
+ {"version":3,"file":"responsivity-photocurrent.js","sourceRoot":"","sources":["../../src/sensor/responsivity-photocurrent.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Responsivity photocurrent: converts light power to detector current using responsivity in amperes\n * per watt (A/W).\n *\n * For each wavelength bin, current = responsivity × radiant power; the bins are summed. The result is\n * the steady (quasi-static) current magnitude at the detector terminals. The electrical bias and load\n * conditions under which the responsivity applies are explicit. This is not EQE, an electron rate, a\n * transimpedance voltage or RAW. Fast-changing current needs a separate time-dependent detector model.\n * @see docs/MOTION_AND_SIGNAL.md for equations, coordinate/unit conventions, blockers and support\n * limits.\n */\n\nimport {\n approximationResult,\n type CalculationResult\n} from \"../core/calculation-result.js\";\nimport { stringifyCanonicalJson } from \"../core/canonical-json.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport {\n parseEvidenceList,\n type EvidenceProvenance\n} from \"../core/evidence-provenance.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport {\n parseSensorColorSamplingProfile,\n type SensorColorSamplingProfile\n} from \"./color-sampling.js\";\nimport type {\n SensorResponseApplicationCompatibilityAssessment\n} from \"./response-application-compatibility.js\";\nimport type {\n SensorResponseOperatingRangeAssessment\n} from \"./response-operating-range.js\";\nimport {\n createSensorSpectralResponseChannelBinding,\n requireCurrentSensorSpectralResponseChannelBinding,\n parseSensorSpectralResponseProfile,\n resolveParsedSensorSpectralResponseAtWavelength,\n type ResolvedSensorSpectralResponse,\n type SensorSpectralResponseProfile,\n type SensorSpectralResponseScientificStatus,\n type SensorSpectralResponseUncertainty\n} from \"./spectral-response.js\";\nimport type {\n SensorSpatioSpectralIrradianceReduction,\n SensorSpatioSpectralWavelengthReduction\n} from \"./spatio-spectral-reduction.js\";\nimport { requireInputObject, requireInputKeys, rejectUnknownVariantFields, requireConfigRecord as requireRecord, requireConfigString as requireNonEmptyString, requireConfigPositiveFinite as requirePositiveFiniteConfiguration, requireInputNonNegativeFinite as requireNonNegativeFiniteInput, requireInputPositiveFinite as requirePositiveFiniteInput, requireInputNonNegativeSafeInteger as requireSafeNonNegativeInteger } from \"../core/record-validation.js\";\nimport { addCompensated, approximatelyEqual } from \"../core/shared-helpers.js\";\nimport { parseReferenceConditionPolicy as parseConditionPolicy } from \"./response-application-compatibility.js\";\nimport { parseUncertainty } from \"./response-operating-range.js\";\n\n\nexport type SensorResponsivityBiasCondition =\n | {\n kind: \"zero-bias-photovoltaic\";\n }\n | {\n kind: \"reverse-biased\";\n magnitudeVolts: number;\n };\n\nexport type SensorResponsivityReadoutLoadCondition =\n | {\n kind: \"virtual-ground-current-readout\";\n }\n | {\n kind: \"finite-input-impedance\";\n inputImpedanceOhms: number;\n };\n\nexport interface SensorResponsivityElectricalConditions {\n bias: SensorResponsivityBiasCondition;\n readoutLoad:\n SensorResponsivityReadoutLoadCondition;\n}\n\nexport type SensorResponsivityElectricalConditionPolicy =\n | {\n kind: \"exact-match-required\";\n }\n | {\n kind: \"assume-compatible\";\n limitation: string;\n evidence: readonly EvidenceProvenance[];\n };\n\nexport interface SensorResponsivityElectricalApplicabilityProfile {\n schemaVersion: \"0.1.0\";\n profileId: string;\n responseApplicationProfileId: string;\n spectralResponseProfileId: string;\n colorSamplingProfileId: string;\n channelId: string;\n outputMeaning:\n \"detector-terminal-photocurrent-magnitude\";\n referenceConditions:\n SensorResponsivityElectricalConditions;\n conditionPolicy:\n SensorResponsivityElectricalConditionPolicy;\n scientificStatus:\n SensorSpectralResponseScientificStatus;\n uncertainty:\n SensorSpectralResponseUncertainty;\n evidence: readonly EvidenceProvenance[];\n}\n\nexport interface CalculateSensorResponsivityPhotocurrentInput {\n reduction:\n SensorSpatioSpectralIrradianceReduction;\n compatibility:\n SensorResponseApplicationCompatibilityAssessment;\n operatingRange:\n SensorResponseOperatingRangeAssessment;\n colorSamplingProfile:\n SensorColorSamplingProfile;\n spectralResponseProfile:\n SensorSpectralResponseProfile;\n electricalApplicabilityProfile:\n SensorResponsivityElectricalApplicabilityProfile;\n operatingElectricalConditions:\n SensorResponsivityElectricalConditions;\n}\n\nexport interface SensorResponsivityWavelengthCurrentContribution {\n spectralSampleIndex: number;\n wavelengthNanometers: number;\n wavelengthMeasureNanometers: number;\n geometricApertureIncidentSpectralFluxWattsPerNanometer:\n number;\n radiantPowerContributionWatts: number;\n spectralResponsivityAmperesPerWatt:\n number;\n responseInterpolationUsed: boolean;\n photocurrentMagnitudeContributionAmperes:\n number;\n}\n\nexport interface SensorResponsivityPhotocurrent {\n colorSamplingProfileId: string;\n responseProfileId: string;\n responseApplicationProfileId: string;\n operatingRangeProfileId: string;\n electricalApplicabilityProfileId:\n string;\n /** The exact source colour site. Results produced by the engine fill this in. */\n site?: {\n x: number;\n y: number;\n };\n channelId: string;\n sourceResponseKind:\n \"effective-spectral-responsivity\";\n responseScope:\n SensorSpatioSpectralIrradianceReduction[\"responseScope\"];\n responseReferencePlane:\n SensorResponseApplicationCompatibilityAssessment[\"requiredSourcePlane\"];\n wavelengthBasis:\n SensorSpatioSpectralIrradianceReduction[\"wavelengthBasis\"];\n wavelengthRangeNanometers:\n SensorSpatioSpectralIrradianceReduction[\"wavelengthRangeNanometers\"];\n electricalReferenceConditions:\n SensorResponsivityElectricalConditions;\n operatingElectricalConditions:\n SensorResponsivityElectricalConditions;\n electricalConditionPolicy:\n SensorResponsivityElectricalConditionPolicy;\n electricalCompatibility:\n | \"exact-match\"\n | \"assumed-compatible\";\n spectralNodeCount: number;\n perWavelength:\n readonly SensorResponsivityWavelengthCurrentContribution[];\n photocurrentMagnitudeAmperes: number;\n currentSignConvention:\n \"magnitude-only-no-circuit-polarity\";\n summationMethod:\n \"kahan-compensated\";\n responseApplicationPerformed: true;\n spectralResponsivityApplied: true;\n quantumEfficiencyApplied: false;\n photonRateCalculated: false;\n electronRateCalculated: false;\n currentCalculated: true;\n chargeCalculated: false;\n temporalResponseModel:\n \"quasi-static-steady-state-only\";\n detectorBandwidthModeled: false;\n transimpedanceGainApplied: false;\n voltageCalculated: false;\n temporalIntegrationApplied: false;\n exposureDurationApplied: false;\n saturationAssessed: false;\n readoutElectronicsLinearityAssessed:\n false;\n shotNoiseApplied: false;\n readNoiseApplied: false;\n adcQuantizationApplied: false;\n rawCodeValueProduced: false;\n responseUncertaintyPropagated: false;\n electricalApplicabilityUncertaintyPropagated:\n false;\n quadratureConvergenceErrorEstimated:\n false;\n componentEvidence: {\n electricalApplicability:\n readonly EvidenceProvenance[];\n electricalConditionAssumption:\n readonly EvidenceProvenance[];\n };\n}\n\ninterface CompensatedSum {\n sum: number;\n correction: number;\n}\n\nfunction parseElectricalConditions(\n value: unknown,\n path: string\n): SensorResponsivityElectricalConditions {\n const record = requireRecord(\n value,\n path, [\"bias\", \"readoutLoad\"]\n );\n const bias = requireRecord(\n record.bias,\n path + \".bias\", null\n );\n rejectUnknownVariantFields(bias, \"kind\", { \"zero-bias-photovoltaic\": [\"kind\"], \"reverse-biased\": [\"kind\", \"magnitudeVolts\"] }, path + \".bias\");\n const readoutLoad = requireRecord(\n record.readoutLoad,\n path + \".readoutLoad\", null\n );\n rejectUnknownVariantFields(readoutLoad, \"kind\", { \"virtual-ground-current-readout\": [\"kind\"], \"finite-input-impedance\": [\"kind\", \"inputImpedanceOhms\"] }, path + \".readoutLoad\");\n\n const parsedBias:\n SensorResponsivityBiasCondition =\n bias.kind ===\n \"zero-bias-photovoltaic\"\n ? {\n kind:\n \"zero-bias-photovoltaic\"\n }\n : bias.kind ===\n \"reverse-biased\"\n ? {\n kind:\n \"reverse-biased\",\n magnitudeVolts:\n requirePositiveFiniteConfiguration(\n bias.magnitudeVolts,\n path +\n \".bias.magnitudeVolts\"\n )\n }\n : ((): never => {\n throw new InvalidConfigurationError(\n path +\n \".bias.kind is invalid.\"\n );\n })();\n\n const parsedReadout:\n SensorResponsivityReadoutLoadCondition =\n readoutLoad.kind ===\n \"virtual-ground-current-readout\"\n ? {\n kind:\n \"virtual-ground-current-readout\"\n }\n : readoutLoad.kind ===\n \"finite-input-impedance\"\n ? {\n kind:\n \"finite-input-impedance\",\n inputImpedanceOhms:\n requirePositiveFiniteConfiguration(\n readoutLoad\n .inputImpedanceOhms,\n path +\n \".readoutLoad.inputImpedanceOhms\"\n )\n }\n : ((): never => {\n throw new InvalidConfigurationError(\n path +\n \".readoutLoad.kind is invalid.\"\n );\n })();\n\n return {\n bias: parsedBias,\n readoutLoad: parsedReadout\n };\n}\n\n/**\n * Validates untrusted input and returns it as a normalized, typed value. Unknown enum values,\n * missing required fields and inconsistent scientific data are rejected.\n *\n * The profile states the electrical bias and load conditions under which an A/W responsivity applies.\n * @param value - Untrusted data. TypeScript types alone are not validation.\n * @returns The result. Success does not imply calibration.\n *\n * @see docs/API_REFERENCE.md for the package export and its full type definitions.\n */\nexport function parseSensorResponsivityElectricalApplicabilityProfile(\n value: unknown\n): SensorResponsivityElectricalApplicabilityProfile {\n const record = requireRecord(\n value,\n \"sensorResponsivityElectricalApplicability\", [\"schemaVersion\", \"outputMeaning\", \"scientificStatus\", \"uncertainty\", \"profileId\", \"responseApplicationProfileId\", \"spectralResponseProfileId\", \"colorSamplingProfileId\", \"channelId\", \"referenceConditions\", \"conditionPolicy\", \"evidence\"]\n );\n\n if (\n record.schemaVersion !== \"0.1.0\"\n ) {\n throw new InvalidConfigurationError(\n 'sensorResponsivityElectricalApplicability.schemaVersion must be \"0.1.0\".'\n );\n }\n if (\n record.outputMeaning !==\n \"detector-terminal-photocurrent-magnitude\"\n ) {\n throw new InvalidConfigurationError(\n \"sensorResponsivityElectricalApplicability.outputMeaning is invalid.\"\n );\n }\n if (\n record.scientificStatus !==\n \"calibrated\" &&\n record.scientificStatus !==\n \"approximation\"\n ) {\n throw new InvalidConfigurationError(\n \"sensorResponsivityElectricalApplicability.scientificStatus is invalid.\"\n );\n }\n\n const uncertainty =\n parseUncertainty(\n record.uncertainty,\n \"sensorResponsivityElectricalApplicability.uncertainty\"\n );\n\n if (\n record.scientificStatus ===\n \"calibrated\" &&\n uncertainty.kind ===\n \"not-quantified\"\n ) {\n throw new InvalidConfigurationError(\n \"A calibrated electrical applicability profile must declare quantified relative uncertainty.\"\n );\n }\n\n return {\n schemaVersion: \"0.1.0\",\n profileId:\n requireNonEmptyString(\n record.profileId,\n \"sensorResponsivityElectricalApplicability.profileId\"\n ),\n responseApplicationProfileId:\n requireNonEmptyString(\n record\n .responseApplicationProfileId,\n \"sensorResponsivityElectricalApplicability.responseApplicationProfileId\"\n ),\n spectralResponseProfileId:\n requireNonEmptyString(\n record\n .spectralResponseProfileId,\n \"sensorResponsivityElectricalApplicability.spectralResponseProfileId\"\n ),\n colorSamplingProfileId:\n requireNonEmptyString(\n record.colorSamplingProfileId,\n \"sensorResponsivityElectricalApplicability.colorSamplingProfileId\"\n ),\n channelId:\n requireNonEmptyString(\n record.channelId,\n \"sensorResponsivityElectricalApplicability.channelId\"\n ),\n outputMeaning:\n \"detector-terminal-photocurrent-magnitude\",\n referenceConditions:\n parseElectricalConditions(\n record.referenceConditions,\n \"sensorResponsivityElectricalApplicability.referenceConditions\"\n ),\n conditionPolicy:\n parseConditionPolicy(\n record.conditionPolicy,\n \"sensorResponsivityElectricalApplicability.conditionPolicy\"\n ),\n scientificStatus:\n record.scientificStatus,\n uncertainty,\n evidence:\n parseEvidenceList(\n record.evidence,\n \"sensorResponsivityElectricalApplicability.evidence\"\n )\n };\n}\n\n/**\n * Identity string for parsed plain data that ignores key order, so records with\n * the same content compare equal whatever order their properties were added in.\n */\nfunction canonicalJson(\n value: unknown\n): string {\n if (value === undefined) return \"undefined\";\n return stringifyCanonicalJson(value, {\n undefinedObjectProperties: \"omit\",\n nonFiniteNumberMessage:\n \"A/W photocurrent identity data must contain finite numbers.\",\n unsupportedValueMessage:\n \"A/W photocurrent identity data must be plain JSON data.\"\n });\n}\n\n\nfunction electricalConditionsMatch(\n reference:\n SensorResponsivityElectricalConditions,\n operating:\n SensorResponsivityElectricalConditions\n): boolean {\n return (\n canonicalJson(reference) ===\n canonicalJson(operating)\n );\n}\n\nfunction validateAuthorizationChain(\n reduction:\n SensorSpatioSpectralIrradianceReduction,\n compatibility:\n SensorResponseApplicationCompatibilityAssessment,\n operatingRange:\n SensorResponseOperatingRangeAssessment\n): void {\n requireInputObject(reduction, \"reduction input\");\n if (\n reduction.outputMeaning !==\n \"pre-response-spatio-spectral-radiometric-reduction\" ||\n reduction\n .spectralResponseApplicationPerformed !==\n false ||\n reduction\n .spectralResponsivityApplied !==\n false ||\n reduction.temporalIntegrationApplied !==\n false\n ) {\n throw new InvalidScientificInputError(\n \"reduction must remain pre-response and pre-temporal before A/W photocurrent conversion.\"\n );\n }\n\n if (\n !compatibility\n .structuralCompatibilityEstablished ||\n compatibility.compatibilityStatus ===\n \"blocked\" ||\n compatibility.blockers.length !== 0 ||\n !compatibility.sourcePlaneMatched ||\n compatibility.requiredSignalPath !==\n \"radiant-power-to-current\"\n ) {\n throw new InvalidScientificInputError(\n \"compatibility must establish a non-blocked radiant-power-to-current response path.\"\n );\n }\n\n if (\n compatibility\n .spectralResponseProfileId !==\n reduction.responseProfileId ||\n compatibility.colorSamplingProfileId !==\n reduction.colorSamplingProfileId ||\n compatibility.channelId !==\n reduction.channelId ||\n compatibility.responseScope !==\n reduction.responseScope\n ) {\n throw new InvalidScientificInputError(\n \"compatibility identities must exactly match the reduction.\"\n );\n }\n\n if (\n !operatingRange\n .operatingRangeCompatibilityAssessed ||\n !operatingRange\n .instantaneousResponseRangeCompatible ||\n !operatingRange\n .responseRateConversionAuthorized ||\n operatingRange.status === \"blocked\" ||\n operatingRange.blockers.length !== 0 ||\n operatingRange.requiredSignalPath !==\n \"radiant-power-to-current\" ||\n operatingRange\n .responseApplicationPerformed !==\n false ||\n operatingRange\n .currentConversionPerformed !== false ||\n operatingRange\n .temporalIntegrationAuthorized !== false ||\n !operatingRange\n .spectralNodeRangeCompatibilityAssessed ||\n operatingRange\n .spectralInputModel.kind !==\n \"per-spectral-bin\" ||\n operatingRange\n .spatialLinearityModel.kind !==\n \"linear-superposition-over-geometric-aperture\"\n ) {\n throw new InvalidScientificInputError(\n \"operatingRange must authorize a clean per-bin radiant-power-to-current path.\"\n );\n }\n\n if (\n operatingRange\n .spectralResponseProfileId !==\n reduction.responseProfileId ||\n operatingRange\n .colorSamplingProfileId !==\n reduction.colorSamplingProfileId ||\n operatingRange.channelId !==\n reduction.channelId ||\n operatingRange\n .responseApplicationProfileId !==\n compatibility.applicationProfileId\n ) {\n throw new InvalidScientificInputError(\n \"operatingRange identities must exactly match the reduction and compatibility assessment.\"\n );\n }\n}\n\nfunction validateReductionNodes(\n reduction:\n SensorSpatioSpectralIrradianceReduction\n): readonly SensorSpatioSpectralWavelengthReduction[] {\n if (\n !Array.isArray(\n reduction.perWavelength\n ) ||\n !Number.isSafeInteger(\n reduction.spectralNodeCount\n ) ||\n reduction.spectralNodeCount <= 0 ||\n reduction.perWavelength.length !==\n reduction.spectralNodeCount\n ) {\n throw new InvalidScientificInputError(\n \"reduction.perWavelength must contain exactly spectralNodeCount entries.\"\n );\n }\n\n let priorWavelength =\n Number.NEGATIVE_INFINITY;\n let normalizedWeightSum = 0;\n const totalPower:\n CompensatedSum = {\n sum: 0,\n correction: 0\n };\n\n reduction.perWavelength.forEach(\n (node, arrayIndex) => {\n const sampleIndex =\n requireSafeNonNegativeInteger(\n node.spectralSampleIndex,\n \"reduction.perWavelength[\" +\n arrayIndex +\n \"].spectralSampleIndex\"\n );\n if (sampleIndex !== arrayIndex) {\n throw new InvalidScientificInputError(\n \"reduction spectralSampleIndex values must form the exact contiguous range 0..spectralNodeCount-1.\"\n );\n }\n\n const wavelength =\n requirePositiveFiniteInput(\n node.wavelengthNanometers,\n \"reduction.perWavelength[\" +\n arrayIndex +\n \"].wavelengthNanometers\"\n );\n if (\n wavelength <= priorWavelength\n ) {\n throw new InvalidScientificInputError(\n \"reduction wavelength nodes must be strictly increasing.\"\n );\n }\n priorWavelength = wavelength;\n\n const measure =\n requirePositiveFiniteInput(\n node.wavelengthMeasureNanometers,\n \"reduction.perWavelength[\" +\n arrayIndex +\n \"].wavelengthMeasureNanometers\"\n );\n const normalizedWeight =\n requirePositiveFiniteInput(\n node.normalizedWavelengthWeight,\n \"reduction.perWavelength[\" +\n arrayIndex +\n \"].normalizedWavelengthWeight\"\n );\n const fluxDensity =\n requireNonNegativeFiniteInput(\n node\n .geometricApertureIncidentSpectralFluxWattsPerNanometer,\n \"reduction.perWavelength[\" +\n arrayIndex +\n \"].geometricApertureIncidentSpectralFluxWattsPerNanometer\"\n );\n const power =\n requireNonNegativeFiniteInput(\n node\n .wavelengthIntegratedGeometricApertureIncidentFluxContributionWatts,\n \"reduction.perWavelength[\" +\n arrayIndex +\n \"].wavelengthIntegratedGeometricApertureIncidentFluxContributionWatts\"\n );\n const expected =\n fluxDensity * measure;\n if (\n !Number.isFinite(expected) ||\n !approximatelyEqual(\n power,\n expected\n )\n ) {\n throw new InvalidScientificInputError(\n \"reduction per-wavelength radiant-power contribution must equal spectral flux density × d-lambda.\"\n );\n }\n\n normalizedWeightSum +=\n normalizedWeight;\n addCompensated(\n totalPower,\n power\n );\n }\n );\n\n if (\n !Number.isFinite(\n normalizedWeightSum\n ) ||\n !approximatelyEqual(\n normalizedWeightSum,\n 1\n )\n ) {\n throw new InvalidScientificInputError(\n \"reduction normalized wavelength weights must sum to 1.\"\n );\n }\n\n const reductionPower =\n requireNonNegativeFiniteInput(\n reduction\n .wavelengthIntegratedGeometricApertureIncidentFluxWatts,\n \"reduction.wavelengthIntegratedGeometricApertureIncidentFluxWatts\"\n );\n if (\n !approximatelyEqual(\n totalPower.sum,\n reductionPower\n )\n ) {\n throw new InvalidScientificInputError(\n \"reduction total geometric-aperture radiant power must match the per-wavelength sum.\"\n );\n }\n\n return reduction.perWavelength;\n}\n\nfunction validateOperatingRangeNodeBinding(\n reduction:\n SensorSpatioSpectralIrradianceReduction,\n nodes:\n readonly SensorSpatioSpectralWavelengthReduction[],\n operatingRange:\n SensorResponseOperatingRangeAssessment\n): void {\n const evaluated =\n operatingRange\n .evaluatedSpectralNodeInputs;\n\n if (\n evaluated === undefined ||\n evaluated.length !== nodes.length\n ) {\n throw new InvalidScientificInputError(\n \"operatingRange must carry one evaluated spectral-node input per reduction wavelength node.\"\n );\n }\n\n if (\n operatingRange\n .evaluatedWavelengthRangeNanometers\n .minimum !==\n reduction.wavelengthRangeNanometers\n .minimum ||\n operatingRange\n .evaluatedWavelengthRangeNanometers\n .maximum !==\n reduction.wavelengthRangeNanometers\n .maximum\n ) {\n throw new InvalidScientificInputError(\n \"operatingRange wavelength range must exactly match the reduction.\"\n );\n }\n\n evaluated.forEach(\n (entry, index) => {\n const node = nodes[index]!;\n if (\n entry.spectralSampleIndex !==\n node.spectralSampleIndex ||\n entry.wavelengthNanometers !==\n node.wavelengthNanometers ||\n entry.wavelengthMeasureNanometers !==\n node.wavelengthMeasureNanometers\n ) {\n throw new InvalidScientificInputError(\n \"operatingRange spectral-node identities must exactly match the reduction.\"\n );\n }\n\n const expected =\n operatingRange.inputRange.kind ===\n \"wavelength-integrated-geometric-aperture-radiant-power\"\n ? node\n .wavelengthIntegratedGeometricApertureIncidentFluxContributionWatts\n : node\n .wavelengthIntegratedSpatialAverageContributionWattsPerSquareMeter;\n\n if (\n entry.kind !==\n operatingRange.inputRange.kind ||\n entry.unit !==\n operatingRange.inputRange.unit ||\n !approximatelyEqual(\n entry.value,\n expected\n )\n ) {\n throw new InvalidScientificInputError(\n \"operatingRange spectral-node values must match the reduction values in the calibrated input domain.\"\n );\n }\n }\n );\n}\n\nfunction validateResponseBinding(\n resolved:\n ResolvedSensorSpectralResponse,\n reduction:\n SensorSpatioSpectralIrradianceReduction\n): void {\n if (\n resolved.profileId !==\n reduction.responseProfileId ||\n resolved.colorSamplingProfileId !==\n reduction.colorSamplingProfileId ||\n resolved.channelId !==\n reduction.channelId ||\n resolved.sourceResponseKind !==\n reduction.sourceResponseKind ||\n resolved.responseScope !==\n reduction.responseScope ||\n resolved.scientificStatus !==\n reduction.responseScientificStatus ||\n canonicalJson(\n resolved.uncertainty\n ) !==\n canonicalJson(\n reduction.responseUncertainty\n ) ||\n canonicalJson(\n resolved.componentEvidence\n ) !==\n canonicalJson({\n profile:\n reduction.componentEvidence\n .spectral.profile,\n channel:\n reduction.componentEvidence\n .spectral.channel,\n curves:\n reduction.componentEvidence\n .spectral.curves\n })\n ) {\n throw new InvalidScientificInputError(\n \"Resolved A/W response calibration metadata/evidence must exactly match the reduction.\"\n );\n }\n}\n\nfunction validateExactResponseDataBinding(\n responseProfile:\n SensorSpectralResponseProfile,\n reduction:\n SensorSpatioSpectralIrradianceReduction\n): void {\n const response =\n responseProfile.channels.find(\n (entry) =>\n entry.channelId ===\n reduction.channelId\n );\n if (reduction.responseChannelBinding !== undefined) {\n requireCurrentSensorSpectralResponseChannelBinding(\n reduction.responseChannelBinding\n );\n }\n if (\n response === undefined ||\n reduction.responseChannelBinding ===\n undefined ||\n canonicalJson(\n createSensorSpectralResponseChannelBinding(\n response\n )\n ) !==\n canonicalJson(\n reduction.responseChannelBinding\n )\n ) {\n throw new InvalidScientificInputError(\n \"Supplied spectral response channel data must exactly match the response-channel binding carried by the reduction.\"\n );\n }\n}\n\nfunction validateElectricalBinding(\n profile:\n SensorResponsivityElectricalApplicabilityProfile,\n input:\n CalculateSensorResponsivityPhotocurrentInput,\n operatingElectricalConditions:\n SensorResponsivityElectricalConditions\n): {\n compatibility:\n \"exact-match\" |\n \"assumed-compatible\";\n assumptionEvidence:\n readonly EvidenceProvenance[];\n} {\n if (\n profile.responseApplicationProfileId !==\n input.compatibility\n .applicationProfileId ||\n profile.spectralResponseProfileId !==\n input.reduction\n .responseProfileId ||\n profile.colorSamplingProfileId !==\n input.reduction\n .colorSamplingProfileId ||\n profile.channelId !==\n input.reduction.channelId\n ) {\n throw new InvalidScientificInputError(\n \"Electrical applicability profile identities must exactly match the response pipeline.\"\n );\n }\n\n if (\n profile.conditionPolicy.kind ===\n \"exact-match-required\"\n ) {\n if (\n !electricalConditionsMatch(\n profile.referenceConditions,\n operatingElectricalConditions\n )\n ) {\n throw new InvalidScientificInputError(\n \"Operating electrical conditions must exactly match the A/W calibration reference conditions.\"\n );\n }\n return {\n compatibility: \"exact-match\",\n assumptionEvidence: []\n };\n }\n\n return {\n compatibility:\n \"assumed-compatible\",\n assumptionEvidence:\n profile.conditionPolicy.evidence\n };\n}\n\n/**\n * Sums responsivity (A/W) × radiant power over wavelength bins to get the steady current magnitude,\n * under explicit electrical conditions.\n *\n * This is not EQE, an electron rate, a transimpedance voltage or RAW. Fast-changing current needs a\n * separate time-dependent detector model.\n * @param input - The input; see `CalculateSensorResponsivityPhotocurrentInput` for its fields, units and coordinates.\n * @returns The result. Success does not imply calibration.\n *\n * @see docs/API_REFERENCE.md for the package export and its full type definitions.\n */\nexport function calculateSensorResponsivityPhotocurrent(\n input:\n CalculateSensorResponsivityPhotocurrentInput\n): CalculationResult<SensorResponsivityPhotocurrent> {\n requireInputKeys(input, \"calculateSensorResponsivityPhotocurrent\", [\"reduction\", \"compatibility\", \"operatingRange\", \"colorSamplingProfile\", \"spectralResponseProfile\", \"electricalApplicabilityProfile\", \"operatingElectricalConditions\"]);\n validateAuthorizationChain(\n input.reduction,\n input.compatibility,\n input.operatingRange\n );\n\n if (\n input.reduction\n .sourceResponseKind !==\n \"effective-spectral-responsivity\"\n ) {\n throw new InvalidScientificInputError(\n \"A/W photocurrent conversion requires effective spectral responsivity, not EQE.\"\n );\n }\n\n const colorProfile =\n parseSensorColorSamplingProfile(\n input.colorSamplingProfile\n );\n const responseProfile =\n parseSensorSpectralResponseProfile(\n input.spectralResponseProfile\n );\n const electricalProfile =\n parseSensorResponsivityElectricalApplicabilityProfile(\n input.electricalApplicabilityProfile\n );\n\n if (\n colorProfile.profileId !==\n input.reduction\n .colorSamplingProfileId ||\n responseProfile.profileId !==\n input.reduction\n .responseProfileId ||\n responseProfile\n .colorSamplingProfileId !==\n colorProfile.profileId\n ) {\n throw new InvalidScientificInputError(\n \"Supplied color/spectral response profiles must exactly match the reduction identities.\"\n );\n }\n\n validateExactResponseDataBinding(\n responseProfile,\n input.reduction\n );\n\n const operatingElectricalConditions =\n parseElectricalConditions(\n input.operatingElectricalConditions,\n \"operatingElectricalConditions\"\n );\n const electrical =\n validateElectricalBinding(\n electricalProfile,\n input,\n operatingElectricalConditions\n );\n const nodes =\n validateReductionNodes(\n input.reduction\n );\n validateOperatingRangeNodeBinding(\n input.reduction,\n nodes,\n input.operatingRange\n );\n\n const currentSum:\n CompensatedSum = {\n sum: 0,\n correction: 0\n };\n const perWavelength:\n SensorResponsivityWavelengthCurrentContribution[] =\n [];\n let responseBindingValidated =\n false;\n\n for (const node of nodes) {\n const resolved =\n resolveParsedSensorSpectralResponseAtWavelength({\n colorSamplingProfile:\n colorProfile,\n spectralResponseProfile:\n responseProfile,\n channelId:\n input.reduction.channelId,\n wavelengthNanometers:\n node.wavelengthNanometers,\n wavelengthBasis:\n input.reduction\n .wavelengthBasis\n });\n\n if (!responseBindingValidated) {\n validateResponseBinding(\n resolved.value,\n input.reduction\n );\n responseBindingValidated =\n true;\n }\n\n const responsivity =\n resolved.value.response as Extract<\n typeof resolved.value.response,\n {\n kind:\n \"effective-spectral-responsivity\";\n }\n >;\n\n const radiantPowerContributionWatts =\n node\n .geometricApertureIncidentSpectralFluxWattsPerNanometer *\n node.wavelengthMeasureNanometers;\n const photocurrentMagnitudeContributionAmperes =\n radiantPowerContributionWatts *\n responsivity.amperesPerWatt;\n\n if (\n !Number.isFinite(\n radiantPowerContributionWatts\n ) ||\n !Number.isFinite(\n photocurrentMagnitudeContributionAmperes\n ) ||\n photocurrentMagnitudeContributionAmperes <\n 0\n ) {\n throw new InvalidScientificInputError(\n \"A/W wavelength-node photocurrent calculation must remain finite and nonnegative.\"\n );\n }\n\n addCompensated(\n currentSum,\n photocurrentMagnitudeContributionAmperes\n );\n\n perWavelength.push({\n spectralSampleIndex:\n node.spectralSampleIndex,\n wavelengthNanometers:\n node.wavelengthNanometers,\n wavelengthMeasureNanometers:\n node.wavelengthMeasureNanometers,\n geometricApertureIncidentSpectralFluxWattsPerNanometer:\n node\n .geometricApertureIncidentSpectralFluxWattsPerNanometer,\n radiantPowerContributionWatts,\n spectralResponsivityAmperesPerWatt:\n responsivity.amperesPerWatt,\n responseInterpolationUsed:\n resolved.value\n .interpolationUsed,\n photocurrentMagnitudeContributionAmperes\n });\n }\n\n if (\n !Number.isFinite(\n currentSum.sum\n ) ||\n currentSum.sum < 0\n ) {\n throw new InvalidScientificInputError(\n \"Total A/W photocurrent magnitude must remain finite and nonnegative.\"\n );\n }\n\n return approximationResult(\n {\n colorSamplingProfileId:\n input.reduction\n .colorSamplingProfileId,\n responseProfileId:\n input.reduction\n .responseProfileId,\n responseApplicationProfileId:\n input.compatibility\n .applicationProfileId,\n operatingRangeProfileId:\n input.operatingRange\n .operatingRangeProfileId,\n electricalApplicabilityProfileId:\n electricalProfile.profileId,\n site: {\n ...input.reduction.site\n },\n channelId:\n input.reduction.channelId,\n sourceResponseKind:\n \"effective-spectral-responsivity\",\n responseScope:\n input.reduction.responseScope,\n responseReferencePlane:\n input.compatibility\n .requiredSourcePlane,\n wavelengthBasis:\n input.reduction\n .wavelengthBasis,\n wavelengthRangeNanometers: {\n ...input.reduction\n .wavelengthRangeNanometers\n },\n electricalReferenceConditions:\n electricalProfile\n .referenceConditions,\n operatingElectricalConditions,\n electricalConditionPolicy:\n electricalProfile\n .conditionPolicy,\n electricalCompatibility:\n electrical.compatibility,\n spectralNodeCount:\n nodes.length,\n perWavelength,\n photocurrentMagnitudeAmperes:\n currentSum.sum,\n currentSignConvention:\n \"magnitude-only-no-circuit-polarity\",\n summationMethod:\n \"kahan-compensated\",\n responseApplicationPerformed:\n true,\n spectralResponsivityApplied:\n true,\n quantumEfficiencyApplied:\n false,\n photonRateCalculated: false,\n electronRateCalculated: false,\n currentCalculated: true,\n chargeCalculated: false,\n temporalResponseModel:\n \"quasi-static-steady-state-only\",\n detectorBandwidthModeled: false,\n transimpedanceGainApplied:\n false,\n voltageCalculated: false,\n temporalIntegrationApplied:\n false,\n exposureDurationApplied: false,\n saturationAssessed: false,\n readoutElectronicsLinearityAssessed:\n false,\n shotNoiseApplied: false,\n readNoiseApplied: false,\n adcQuantizationApplied:\n false,\n rawCodeValueProduced: false,\n responseUncertaintyPropagated:\n false,\n electricalApplicabilityUncertaintyPropagated:\n false,\n quadratureConvergenceErrorEstimated:\n false,\n componentEvidence: {\n electricalApplicability:\n electricalProfile.evidence,\n electricalConditionAssumption:\n electrical\n .assumptionEvidence\n }\n },\n \"sensor-spectral-responsivity-photocurrent\",\n \"1.0.0\",\n [\n \"Spectral responsivity in A/W is applied independently at every wavelength quadrature node as detector-terminal photocurrent magnitude per incident radiant power.\",\n \"Current sign/polarity is deliberately not modeled; the result is a nonnegative photocurrent magnitude and does not imply a circuit current direction.\",\n \"The A/W calibration is bound to explicit detector electrical conditions. Exact-match mode requires identical bias and readout-load semantics; approximation mode requires evidence and a stated limitation.\",\n \"Virtual-ground current readout and finite input impedance are modeled as calibration-condition identities only; this function does not simulate amplifier circuits or prove impedance adequacy.\",\n \"NIST photodiode guidance shows detector/electronics/loading can affect linear operation, so transimpedance/current-readout conditions are not treated as scientifically inert.\",\n \"Structural compatibility, geometric-aperture linear superposition and per-spectral-bin operating-range applicability must already authorize the radiant-power→current path.\",\n \"Response metadata/evidence and operating-range spectral-node values are rebound to the exact reduction so matching IDs alone or stale authorization cannot enable conversion.\",\n \"The calculation does not use photon energy, QE, photon rate or electron rate. A/W remains a distinct current-domain response representation.\",\n \"No transimpedance gain or voltage conversion is performed; current-to-voltage converter gain/offset/linearity remain downstream electronics contracts.\",\n \"The A/W response is used only as a quasi-static steady-state power→current mapping. Detector impulse response, bandwidth, modulation-frequency response and transient settling are not modeled.\",\n \"Outputs are instantaneous current only. Exposure-time integration to charge, saturation, noise, ADC/RAW conversion and reconstruction remain downstream.\",\n \"Response/electrical uncertainties and quadrature convergence error are not propagated into a combined current uncertainty by this version.\"\n ]\n );\n}\n"]}
@@ -0,0 +1,137 @@
1
+ import { type SensorReadoutConversionProfile } from "./raw-readout.js";
2
+ import type { SensorFixedPatternNoiseProfile } from "./fixed-pattern-noise.js";
3
+ /**
4
+ * #555: sensor characteristics a capture can set one at a time (`simulateCapture({ sensor })`). Each is off by default: an
5
+ * omitted field keeps the camera body's value, and a request with none of them is byte-identical to one before 2.20.0.
6
+ * Every value set is listed in the result's `assumptions`.
7
+ */
8
+ export interface SimulateCaptureSensorCharacteristics {
9
+ /**
10
+ * Peak external quantum efficiency of the green channel (its value at 555 nm), 0.05–0.9. Default 0.5, the facade's
11
+ * idealized sensor. Every channel's EQE curve is multiplied by `peakQuantumEfficiency / 0.5`, so the curves keep their
12
+ * shape (and the colour matrix is unchanged) and the collected charge scales in proportion.
13
+ */
14
+ peakQuantumEfficiency?: number;
15
+ /**
16
+ * Full-well capacity: the charge, e⁻, that reaches the RAW white level at base ISO. Default: the body's base-ISO saturation
17
+ * at 6 µm, scaled by the pixel area (the `sensor.isoAreaNormalization` approximation). `link` `"pixel-area"` (default)
18
+ * scales `electrons`, the full well at `referencePixelPitchMicrometers` (default 6), by (pitch / reference)²; `"none"`
19
+ * uses `electrons` at any pitch. The result is 100 e⁻ to 1e10 e⁻.
20
+ */
21
+ fullWell?: {
22
+ electrons: number;
23
+ link?: "pixel-area" | "none";
24
+ referencePixelPitchMicrometers?: number;
25
+ };
26
+ /**
27
+ * Read noise, RMS e⁻ (0–100), of the low and the high conversion gain, and the dual-gain switch: the ISO setting from
28
+ * which the high conversion gain is used (any ISO from 100; above the body's highest ISO, the low gain is used throughout).
29
+ * An omitted field keeps the body's value. Needs readout regimes that declare their exposure index (the tiers do).
30
+ */
31
+ readNoise?: {
32
+ lowGainElectrons?: number;
33
+ highGainElectrons?: number;
34
+ highGainFromIso?: number;
35
+ };
36
+ /** Readout time, s (1e-6 to 1): the rolling shutter's top-to-bottom skew. Default: the body's (tiers 8, 15 and 30 ms). */
37
+ readoutSeconds?: number;
38
+ /**
39
+ * ADC bit depth: 12, 14 or 16. Default: the body's (consumer 12, the others 14). The black level scales with the code range
40
+ * (×2^Δbits), and the conversion gain follows so the same charge reaches the same fraction of the range.
41
+ */
42
+ adcBitDepth?: 12 | 14 | 16;
43
+ /**
44
+ * Opt-in (advanced): the body's synthetic fixed-pattern noise profile (PRNU, DSNU, hot pixels, row and column offsets; EMVA
45
+ * 1288 Release 4.0 non-uniformities), the same pattern in every capture. Default false. Not in `simulateDisplayView`.
46
+ */
47
+ fixedPatternNoise?: boolean;
48
+ }
49
+ /** The option keys of {@link SimulateCaptureSensorCharacteristics}, in `sensor`. */
50
+ export declare const SENSOR_CHARACTERISTIC_KEYS: readonly ["peakQuantumEfficiency", "fullWell", "readNoise", "readoutSeconds", "adcBitDepth", "fixedPatternNoise"];
51
+ /** One RMS noise component of the mid-grey figure, e⁻ (input-referred). */
52
+ export interface SimulateCaptureNoiseComponents {
53
+ /** Photon shot noise √S (Poisson). */
54
+ photonShot: number;
55
+ /** Read noise of the capture's readout regime. */
56
+ read: number;
57
+ /** Dark-current shot noise √(D·t), D = 1 e⁻/s at 20 °C. */
58
+ darkCurrent: number;
59
+ /** ADC quantization, one code (and one more after a digital gain) as a uniform 1/12 code² variance, in e⁻. */
60
+ quantization: number;
61
+ /** With `sensor.fixedPatternNoise`: √((PRNU·S)² + (DSNU·D·t)² + row² + column²), hot pixels excluded; a spatial, not temporal, noise. */
62
+ fixedPattern?: number;
63
+ /** √ of the sum of the squares. */
64
+ total: number;
65
+ }
66
+ /**
67
+ * #555, #535: the capture's sensor characteristics as used, and the figures calculated from them (reported, not settable).
68
+ * Model figures from the engine's own noise model, not measurements.
69
+ */
70
+ export interface SimulateCaptureSensorReport {
71
+ pixelPitchMicrometers: number;
72
+ /** The green channel's peak EQE (at 555 nm). */
73
+ peakQuantumEfficiency: number;
74
+ /** Full-well capacity, e⁻: the base-ISO saturation charge (`sensor.capacityElectrons`). */
75
+ fullWellElectrons: number;
76
+ /** Read noise, RMS e⁻, of each conversion gain, and the ISO from which the high gain is used (null: one gain throughout). */
77
+ readNoise: {
78
+ lowGainElectrons: number;
79
+ highGainElectrons: number;
80
+ highGainFromIso: number | null;
81
+ };
82
+ /** The body's rolling readout skew, s (null when it declares no rolling readout). */
83
+ readoutSeconds: number | null;
84
+ /** ADC bit depth of the capture's readout regime. */
85
+ adcBitDepth: number;
86
+ fixedPatternNoise: boolean;
87
+ /**
88
+ * Engineering dynamic range at the capture's ISO, stops: log₂(S_sat / σ_floor), with S_sat the charge at the RAW white
89
+ * level (full well over the analog gain, limited by the charge capacity and the ADC range) and σ_floor the read noise and
90
+ * ADC quantization added in quadrature (the temporal dark noise of EMVA 1288 Release 4.0 without dark current). An ideal
91
+ * noiseless readout is limited by the ADC alone: log₂((white − black)·√12) for unit digital gain.
92
+ */
93
+ dynamicRangeStops: number;
94
+ /** The same per ISO setting of the body, in its order. */
95
+ dynamicRangeByIso: readonly {
96
+ iso: number;
97
+ stops: number;
98
+ }[];
99
+ /**
100
+ * #535: the expected signal-to-noise ratio of an 18 % Lambertian grey patch under the request's illuminant and illuminance
101
+ * at the capture's exposure, ISO and sensor, on the green channel next to the optical axis (no vignetting, PSF or motion).
102
+ * `snr` = S/σ_total and `snrDecibels` = 20·log₁₀(snr). `clipped`: S is at or above the white level, where the RAW clips and
103
+ * the figure no longer describes the image. Computed only when the request resolves to exact (`fast: false`, the default for
104
+ * DNG, JPEG and PNG exports; omitted for a fast request).
105
+ */
106
+ midGrey?: {
107
+ reflectance: 0.18;
108
+ channel: "green";
109
+ signalElectrons: number;
110
+ noiseElectrons: SimulateCaptureNoiseComponents;
111
+ snr: number;
112
+ snrDecibels: number;
113
+ clipped: boolean;
114
+ };
115
+ }
116
+ /** The request's characteristics, range-checked, with the EQE scale peakQuantumEfficiency / 0.5. */
117
+ export type ParsedSensorCharacteristics = SimulateCaptureSensorCharacteristics & {
118
+ qeScale: number;
119
+ };
120
+ /** Checks the characteristics of the request's `sensor` record (its other keys already checked); undefined when it sets none. */
121
+ export declare function parseSensorCharacteristics(sensor: Record<string, unknown>): ParsedSensorCharacteristics | undefined;
122
+ /** The body's dual-gain read noise: the base regime's, and the first high-conversion-gain regime's with its ISO (none: the low gain throughout). */
123
+ export declare function declaredReadNoise(profile: SensorReadoutConversionProfile): SimulateCaptureSensorReport["readNoise"];
124
+ /**
125
+ * The readout profile with the characteristics applied: every regime's charge-domain transfer (system conversion gain,
126
+ * e⁻ per code, and pre-ADC saturation, e⁻ equivalent) multiplied by `chargeScale`, so a full well `chargeScale` times the
127
+ * body's reaches the same white code; with `adcBitDepth`, the black level ×2^Δbits (rounded), the saturation code 2^bits − 1
128
+ * and the conversion gain over the new code span; with `readNoise`, one electronic read-noise component per regime, the high
129
+ * conversion gain's from `highGainFromIso`. Codes per unit charge of the base regime otherwise, digital gain and the ISO
130
+ * selection are unchanged. Re-checked by the readout parser. With nothing to change, the profile itself.
131
+ */
132
+ export declare function characterizeReadout(profile: SensorReadoutConversionProfile, chargeScale: number, c?: ParsedSensorCharacteristics): SensorReadoutConversionProfile;
133
+ /**
134
+ * The report: the characteristics used, the dynamic range per ISO, and the mid-grey noise figure for `greyElectrons`, the
135
+ * green channel's expected charge from an 18 % grey patch, after a `darkElectrons` dark charge.
136
+ */
137
+ export declare function sensorReport(profile: SensorReadoutConversionProfile, regimeId: string, capacityElectrons: number, greyElectrons: number | undefined, darkElectrons: number, base: Pick<SimulateCaptureSensorReport, "pixelPitchMicrometers" | "peakQuantumEfficiency" | "readoutSeconds">, pattern?: SensorFixedPatternNoiseProfile): SimulateCaptureSensorReport;
@@ -0,0 +1,115 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Adjustable sensor characteristics for `simulateCapture` (#555) and the capture's noise figure (#535): the request
4
+ * options that replace a camera body's sensor values one at a time, the readout they give, and the calculated dynamic
5
+ * range and mid-grey signal-to-noise ratio the result reports.
6
+ *
7
+ * Everything here works on the body's declared readout regimes, so it applies to a generic tier and to a caller's
8
+ * preset record alike, and a later sensor profile (#554) can supply the same options as data.
9
+ * @see docs/QUICK_START.md, "Sensor characteristics (#555)", for the options, the equations and their sources.
10
+ */
11
+ import { InvalidConfigurationError } from "../core/configuration-error.js";
12
+ import { requireInputObject } from "../core/record-validation.js";
13
+ import { calculateSiteEffectiveSaturationCode } from "../capture/raw-white-level.js";
14
+ import { parseSensorReadoutConversionProfile } from "./raw-readout.js";
15
+ /** The option keys of {@link SimulateCaptureSensorCharacteristics}, in `sensor`. */
16
+ export const SENSOR_CHARACTERISTIC_KEYS = ["peakQuantumEfficiency", "fullWell", "readNoise", "readoutSeconds", "adcBitDepth", "fixedPatternNoise"];
17
+ /** Accepted ranges (design limits) and values of each characteristic, by path below `sensor`. */
18
+ const RANGES = { peakQuantumEfficiency: [0.05, 0.9], fullWell: 0, "fullWell.electrons": [100, 1e10],
19
+ "fullWell.link": ["pixel-area", "none"], "fullWell.referencePixelPitchMicrometers": [0.5, 1000], readNoise: 0, "readNoise.lowGainElectrons": [0, 100],
20
+ "readNoise.highGainElectrons": [0, 100], "readNoise.highGainFromIso": [100, 1e7], readoutSeconds: [1e-6, 1], adcBitDepth: [12, 14, 16], fixedPatternNoise: [true, false] };
21
+ function fail(message) { throw new InvalidConfigurationError(message); }
22
+ function check(record, keys, prefix) {
23
+ for (const key of keys) {
24
+ const path = prefix + key, value = record[key], range = RANGES[path];
25
+ if (value === undefined)
26
+ continue;
27
+ if (range === undefined)
28
+ fail("Unknown sensor." + prefix.slice(0, -1) + " field: " + key + ".");
29
+ if (range === 0) {
30
+ requireInputObject(value, "sensor." + path);
31
+ check(value, Object.keys(value), path + ".");
32
+ }
33
+ else {
34
+ const choice = range.length > 2 || typeof range[0] !== "number", [minimum, maximum] = range;
35
+ if (choice ? !range.includes(value) : typeof value !== "number" || !(value >= minimum && value <= maximum)) {
36
+ fail("sensor." + path + " must be " + (choice ? range.join(" or ") : "a number from " + minimum + " to " + maximum) + ".");
37
+ }
38
+ }
39
+ }
40
+ }
41
+ /** Checks the characteristics of the request's `sensor` record (its other keys already checked); undefined when it sets none. */
42
+ export function parseSensorCharacteristics(sensor) {
43
+ if (!SENSOR_CHARACTERISTIC_KEYS.some((key) => sensor[key] !== undefined))
44
+ return undefined;
45
+ check(sensor, SENSOR_CHARACTERISTIC_KEYS, "");
46
+ const c = sensor, fullWell = c.fullWell;
47
+ if (fullWell && fullWell.electrons === undefined)
48
+ fail("sensor.fullWell.electrons is required.");
49
+ if (fullWell?.link === "none" && fullWell.referencePixelPitchMicrometers !== undefined)
50
+ fail("sensor.fullWell.referencePixelPitchMicrometers applies only to link \"pixel-area\".");
51
+ return { ...c, qeScale: (c.peakQuantumEfficiency ?? 0.5) / 0.5 };
52
+ }
53
+ const noiseOf = (regime) => Math.hypot(...regime.readNoiseComponents.map((c) => c.rmsElectrons.value));
54
+ /** The body's dual-gain read noise: the base regime's, and the first high-conversion-gain regime's with its ISO (none: the low gain throughout). */
55
+ export function declaredReadNoise(profile) {
56
+ const regimes = profile.regimes, high = regimes.find((r) => r.exposureIndexGain?.conversionGainMode === "high-conversion-gain"), low = noiseOf(regimes[0]);
57
+ return { lowGainElectrons: low, highGainElectrons: high ? noiseOf(high) : low, highGainFromIso: high ? high.exposureIndexGain.exposureIndex : null };
58
+ }
59
+ /**
60
+ * The readout profile with the characteristics applied: every regime's charge-domain transfer (system conversion gain,
61
+ * e⁻ per code, and pre-ADC saturation, e⁻ equivalent) multiplied by `chargeScale`, so a full well `chargeScale` times the
62
+ * body's reaches the same white code; with `adcBitDepth`, the black level ×2^Δbits (rounded), the saturation code 2^bits − 1
63
+ * and the conversion gain over the new code span; with `readNoise`, one electronic read-noise component per regime, the high
64
+ * conversion gain's from `highGainFromIso`. Codes per unit charge of the base regime otherwise, digital gain and the ISO
65
+ * selection are unchanged. Re-checked by the readout parser. With nothing to change, the profile itself.
66
+ */
67
+ export function characterizeReadout(profile, chargeScale, c) {
68
+ const bits = c?.adcBitDepth, n = c?.readNoise, declared = n && declaredReadNoise(profile);
69
+ if (chargeScale === 1 && !bits && !n)
70
+ return profile;
71
+ return parseSensorReadoutConversionProfile({ ...profile, regimes: profile.regimes.map((regime) => {
72
+ const adc = regime.adc, rescale = bits !== undefined && bits !== adc.bitDepth;
73
+ const black = rescale ? Math.round(adc.blackLevelCode * 2 ** (bits - adc.bitDepth)) : adc.blackLevelCode, saturation = rescale ? 2 ** bits - 1 : adc.digitalSaturationCode;
74
+ const out = { ...regime, systemConversionGainElectronsPerCode: { ...regime.systemConversionGainElectronsPerCode, value: rescale ?
75
+ regime.systemConversionGainElectronsPerCode.value * chargeScale * (adc.digitalSaturationCode - adc.blackLevelCode) / (saturation - black) : regime.systemConversionGainElectronsPerCode.value * chargeScale },
76
+ preAdcSaturationElectronEquivalent: { ...regime.preAdcSaturationElectronEquivalent, value: regime.preAdcSaturationElectronEquivalent.value * chargeScale },
77
+ ...(rescale ? { adc: { ...adc, bitDepth: bits, blackLevelCode: black, digitalSaturationCode: saturation } } : {}) };
78
+ if (n) {
79
+ const gain = regime.exposureIndexGain ?? fail("sensor.readNoise needs readout regimes that declare their exposure index.");
80
+ const from = n.highGainFromIso ?? declared.highGainFromIso ?? Infinity, high = gain.exposureIndex >= from;
81
+ const value = high ? n.highGainElectrons ?? declared.highGainElectrons : n.lowGainElectrons ?? declared.lowGainElectrons;
82
+ out.readNoiseComponents = [{ componentId: "electronic", rmsElectrons: { value, evidence: regime.evidence }, evidence: regime.evidence }];
83
+ out.exposureIndexGain = { ...gain, conversionGainMode: high ? "high-conversion-gain" : "low-conversion-gain" };
84
+ }
85
+ return out;
86
+ }) });
87
+ }
88
+ /** Charge at the RAW white level and the read-plus-quantization floor of one regime, e⁻. */
89
+ function regimeRange(profile, regime, capacityElectrons) {
90
+ const adc = regime.adc, gain = regime.systemConversionGainElectronsPerCode.value, digitalGain = adc.digitalGain ?? 1;
91
+ const white = calculateSiteEffectiveSaturationCode({ physicalChargeCapacityElectrons: capacityElectrons, preAdcSaturationElectronEquivalent: regime.preAdcSaturationElectronEquivalent.value,
92
+ systemConversionGainElectronsPerCode: gain, blackLevelCode: adc.blackLevelCode, digitalSaturationCode: adc.digitalSaturationCode, adcMaximumCode: 2 ** adc.bitDepth - 1, digitalGain }, profile, regime.regimeId);
93
+ // Output codes per electron d/G; quantization 1/12 code² of the ADC times d², plus 1/12 of the rounding after a digital gain.
94
+ const perCode = gain / digitalGain;
95
+ return { saturation: (white - adc.blackLevelCode) * perCode, quantization: perCode * Math.sqrt(digitalGain * digitalGain / 12 + (digitalGain === 1 ? 0 : 1 / 12)), read: noiseOf(regime) };
96
+ }
97
+ /**
98
+ * The report: the characteristics used, the dynamic range per ISO, and the mid-grey noise figure for `greyElectrons`, the
99
+ * green channel's expected charge from an 18 % grey patch, after a `darkElectrons` dark charge.
100
+ */
101
+ export function sensorReport(profile, regimeId, capacityElectrons, greyElectrons, darkElectrons, base, pattern) {
102
+ const stops = (r) => { const q = regimeRange(profile, r, capacityElectrons); return Math.log2(q.saturation / Math.hypot(q.read, q.quantization)); };
103
+ const regime = profile.regimes.find((r) => r.regimeId === regimeId), range = regimeRange(profile, regime, capacityElectrons);
104
+ const report = { ...base, fullWellElectrons: capacityElectrons, readNoise: declaredReadNoise(profile), adcBitDepth: regime.adc.bitDepth, fixedPatternNoise: !!pattern,
105
+ dynamicRangeStops: stops(regime), dynamicRangeByIso: profile.regimes.map((r) => ({ iso: r.exposureIndexGain?.exposureIndex ?? NaN, stops: stops(r) })).filter((e) => e.iso > 0) };
106
+ if (greyElectrons === undefined)
107
+ return report;
108
+ const fixedPattern = pattern && Math.hypot(pattern.photoResponseNonUniformity.relativeStandardDeviation.value * greyElectrons, pattern.darkSignalNonUniformity.relativeStandardDeviation.value * darkElectrons, pattern.rowOffset.rmsElectrons.value, pattern.columnOffset.rmsElectrons.value);
109
+ const noise = { photonShot: Math.sqrt(greyElectrons), read: range.read, darkCurrent: Math.sqrt(darkElectrons), quantization: range.quantization, ...(fixedPattern === undefined ? {} : { fixedPattern }), total: 0 };
110
+ noise.total = Math.hypot(noise.photonShot, noise.read, noise.darkCurrent, noise.quantization, fixedPattern ?? 0);
111
+ const snr = greyElectrons / noise.total;
112
+ report.midGrey = { reflectance: 0.18, channel: "green", signalElectrons: greyElectrons, noiseElectrons: noise, snr, snrDecibels: 20 * Math.log10(snr || Number.MIN_VALUE), clipped: greyElectrons + darkElectrons >= range.saturation };
113
+ return report;
114
+ }
115
+ //# sourceMappingURL=sensor-characteristics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sensor-characteristics.js","sourceRoot":"","sources":["../../src/sensor/sensor-characteristics.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Adjustable sensor characteristics for `simulateCapture` (#555) and the capture's noise figure (#535): the request\n * options that replace a camera body's sensor values one at a time, the readout they give, and the calculated dynamic\n * range and mid-grey signal-to-noise ratio the result reports.\n *\n * Everything here works on the body's declared readout regimes, so it applies to a generic tier and to a caller's\n * preset record alike, and a later sensor profile (#554) can supply the same options as data.\n * @see docs/QUICK_START.md, \"Sensor characteristics (#555)\", for the options, the equations and their sources.\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { requireInputObject } from \"../core/record-validation.js\";\nimport { calculateSiteEffectiveSaturationCode } from \"../capture/raw-white-level.js\";\nimport { parseSensorReadoutConversionProfile, type SensorReadoutConversionProfile, type SensorReadoutConversionRegime } from \"./raw-readout.js\";\nimport type { SensorFixedPatternNoiseProfile } from \"./fixed-pattern-noise.js\";\n\n/**\n * #555: sensor characteristics a capture can set one at a time (`simulateCapture({ sensor })`). Each is off by default: an\n * omitted field keeps the camera body's value, and a request with none of them is byte-identical to one before 2.20.0.\n * Every value set is listed in the result's `assumptions`.\n */\nexport interface SimulateCaptureSensorCharacteristics {\n /**\n * Peak external quantum efficiency of the green channel (its value at 555 nm), 0.05–0.9. Default 0.5, the facade's\n * idealized sensor. Every channel's EQE curve is multiplied by `peakQuantumEfficiency / 0.5`, so the curves keep their\n * shape (and the colour matrix is unchanged) and the collected charge scales in proportion.\n */\n peakQuantumEfficiency?: number;\n /**\n * Full-well capacity: the charge, e⁻, that reaches the RAW white level at base ISO. Default: the body's base-ISO saturation\n * at 6 µm, scaled by the pixel area (the `sensor.isoAreaNormalization` approximation). `link` `\"pixel-area\"` (default)\n * scales `electrons`, the full well at `referencePixelPitchMicrometers` (default 6), by (pitch / reference)²; `\"none\"`\n * uses `electrons` at any pitch. The result is 100 e⁻ to 1e10 e⁻.\n */\n fullWell?: { electrons: number; link?: \"pixel-area\" | \"none\"; referencePixelPitchMicrometers?: number };\n /**\n * Read noise, RMS e⁻ (0–100), of the low and the high conversion gain, and the dual-gain switch: the ISO setting from\n * which the high conversion gain is used (any ISO from 100; above the body's highest ISO, the low gain is used throughout).\n * An omitted field keeps the body's value. Needs readout regimes that declare their exposure index (the tiers do).\n */\n readNoise?: { lowGainElectrons?: number; highGainElectrons?: number; highGainFromIso?: number };\n /** Readout time, s (1e-6 to 1): the rolling shutter's top-to-bottom skew. Default: the body's (tiers 8, 15 and 30 ms). */\n readoutSeconds?: number;\n /**\n * ADC bit depth: 12, 14 or 16. Default: the body's (consumer 12, the others 14). The black level scales with the code range\n * (×2^Δbits), and the conversion gain follows so the same charge reaches the same fraction of the range.\n */\n adcBitDepth?: 12 | 14 | 16;\n /**\n * Opt-in (advanced): the body's synthetic fixed-pattern noise profile (PRNU, DSNU, hot pixels, row and column offsets; EMVA\n * 1288 Release 4.0 non-uniformities), the same pattern in every capture. Default false. Not in `simulateDisplayView`.\n */\n fixedPatternNoise?: boolean;\n}\n\n/** The option keys of {@link SimulateCaptureSensorCharacteristics}, in `sensor`. */\nexport const SENSOR_CHARACTERISTIC_KEYS = [\"peakQuantumEfficiency\", \"fullWell\", \"readNoise\", \"readoutSeconds\", \"adcBitDepth\", \"fixedPatternNoise\"] as const;\n\n/** One RMS noise component of the mid-grey figure, e⁻ (input-referred). */\nexport interface SimulateCaptureNoiseComponents {\n /** Photon shot noise √S (Poisson). */\n photonShot: number;\n /** Read noise of the capture's readout regime. */\n read: number;\n /** Dark-current shot noise √(D·t), D = 1 e⁻/s at 20 °C. */\n darkCurrent: number;\n /** ADC quantization, one code (and one more after a digital gain) as a uniform 1/12 code² variance, in e⁻. */\n quantization: number;\n /** With `sensor.fixedPatternNoise`: √((PRNU·S)² + (DSNU·D·t)² + row² + column²), hot pixels excluded; a spatial, not temporal, noise. */\n fixedPattern?: number;\n /** √ of the sum of the squares. */\n total: number;\n}\n\n/**\n * #555, #535: the capture's sensor characteristics as used, and the figures calculated from them (reported, not settable).\n * Model figures from the engine's own noise model, not measurements.\n */\nexport interface SimulateCaptureSensorReport {\n pixelPitchMicrometers: number;\n /** The green channel's peak EQE (at 555 nm). */\n peakQuantumEfficiency: number;\n /** Full-well capacity, e⁻: the base-ISO saturation charge (`sensor.capacityElectrons`). */\n fullWellElectrons: number;\n /** Read noise, RMS e⁻, of each conversion gain, and the ISO from which the high gain is used (null: one gain throughout). */\n readNoise: { lowGainElectrons: number; highGainElectrons: number; highGainFromIso: number | null };\n /** The body's rolling readout skew, s (null when it declares no rolling readout). */\n readoutSeconds: number | null;\n /** ADC bit depth of the capture's readout regime. */\n adcBitDepth: number;\n fixedPatternNoise: boolean;\n /**\n * Engineering dynamic range at the capture's ISO, stops: log₂(S_sat / σ_floor), with S_sat the charge at the RAW white\n * level (full well over the analog gain, limited by the charge capacity and the ADC range) and σ_floor the read noise and\n * ADC quantization added in quadrature (the temporal dark noise of EMVA 1288 Release 4.0 without dark current). An ideal\n * noiseless readout is limited by the ADC alone: log₂((white − black)·√12) for unit digital gain.\n */\n dynamicRangeStops: number;\n /** The same per ISO setting of the body, in its order. */\n dynamicRangeByIso: readonly { iso: number; stops: number }[];\n /**\n * #535: the expected signal-to-noise ratio of an 18 % Lambertian grey patch under the request's illuminant and illuminance\n * at the capture's exposure, ISO and sensor, on the green channel next to the optical axis (no vignetting, PSF or motion).\n * `snr` = S/σ_total and `snrDecibels` = 20·log₁₀(snr). `clipped`: S is at or above the white level, where the RAW clips and\n * the figure no longer describes the image. Computed only when the request resolves to exact (`fast: false`, the default for\n * DNG, JPEG and PNG exports; omitted for a fast request).\n */\n midGrey?: { reflectance: 0.18; channel: \"green\"; signalElectrons: number; noiseElectrons: SimulateCaptureNoiseComponents; snr: number; snrDecibels: number; clipped: boolean };\n}\n\n/** The request's characteristics, range-checked, with the EQE scale peakQuantumEfficiency / 0.5. */\nexport type ParsedSensorCharacteristics = SimulateCaptureSensorCharacteristics & { qeScale: number };\n\n/** Accepted ranges (design limits) and values of each characteristic, by path below `sensor`. */\nconst RANGES: Readonly<Record<string, readonly [number, number] | readonly unknown[] | 0>> = { peakQuantumEfficiency: [0.05, 0.9], fullWell: 0, \"fullWell.electrons\": [100, 1e10],\n \"fullWell.link\": [\"pixel-area\", \"none\"], \"fullWell.referencePixelPitchMicrometers\": [0.5, 1000], readNoise: 0, \"readNoise.lowGainElectrons\": [0, 100],\n \"readNoise.highGainElectrons\": [0, 100], \"readNoise.highGainFromIso\": [100, 1e7], readoutSeconds: [1e-6, 1], adcBitDepth: [12, 14, 16], fixedPatternNoise: [true, false] };\nfunction fail(message: string): never { throw new InvalidConfigurationError(message); }\nfunction check(record: Record<string, unknown>, keys: readonly string[], prefix: string): void {\n for (const key of keys) {\n const path = prefix + key, value = record[key], range = RANGES[path];\n if (value === undefined) continue;\n if (range === undefined) fail(\"Unknown sensor.\" + prefix.slice(0, -1) + \" field: \" + key + \".\");\n if (range === 0) {\n requireInputObject(value, \"sensor.\" + path);\n check(value as Record<string, unknown>, Object.keys(value as object), path + \".\");\n } else {\n const choice = range.length > 2 || typeof range[0] !== \"number\", [minimum, maximum] = range as readonly [number, number];\n if (choice ? !(range as readonly unknown[]).includes(value) : typeof value !== \"number\" || !(value >= minimum && value <= maximum)) {\n fail(\"sensor.\" + path + \" must be \" + (choice ? range.join(\" or \") : \"a number from \" + minimum + \" to \" + maximum) + \".\");\n }\n }\n }\n}\n\n/** Checks the characteristics of the request's `sensor` record (its other keys already checked); undefined when it sets none. */\nexport function parseSensorCharacteristics(sensor: Record<string, unknown>): ParsedSensorCharacteristics | undefined {\n if (!SENSOR_CHARACTERISTIC_KEYS.some((key) => sensor[key] !== undefined)) return undefined;\n check(sensor, SENSOR_CHARACTERISTIC_KEYS, \"\");\n const c = sensor as SimulateCaptureSensorCharacteristics, fullWell = c.fullWell;\n if (fullWell && fullWell.electrons === undefined) fail(\"sensor.fullWell.electrons is required.\");\n if (fullWell?.link === \"none\" && fullWell.referencePixelPitchMicrometers !== undefined) fail(\"sensor.fullWell.referencePixelPitchMicrometers applies only to link \\\"pixel-area\\\".\");\n return { ...c, qeScale: (c.peakQuantumEfficiency ?? 0.5) / 0.5 };\n}\n\nconst noiseOf = (regime: SensorReadoutConversionRegime): number => Math.hypot(...regime.readNoiseComponents.map((c) => c.rmsElectrons.value));\n/** The body's dual-gain read noise: the base regime's, and the first high-conversion-gain regime's with its ISO (none: the low gain throughout). */\nexport function declaredReadNoise(profile: SensorReadoutConversionProfile): SimulateCaptureSensorReport[\"readNoise\"] {\n const regimes = profile.regimes, high = regimes.find((r) => r.exposureIndexGain?.conversionGainMode === \"high-conversion-gain\"), low = noiseOf(regimes[0]!);\n return { lowGainElectrons: low, highGainElectrons: high ? noiseOf(high) : low, highGainFromIso: high ? high.exposureIndexGain!.exposureIndex : null };\n}\n\n/**\n * The readout profile with the characteristics applied: every regime's charge-domain transfer (system conversion gain,\n * e⁻ per code, and pre-ADC saturation, e⁻ equivalent) multiplied by `chargeScale`, so a full well `chargeScale` times the\n * body's reaches the same white code; with `adcBitDepth`, the black level ×2^Δbits (rounded), the saturation code 2^bits − 1\n * and the conversion gain over the new code span; with `readNoise`, one electronic read-noise component per regime, the high\n * conversion gain's from `highGainFromIso`. Codes per unit charge of the base regime otherwise, digital gain and the ISO\n * selection are unchanged. Re-checked by the readout parser. With nothing to change, the profile itself.\n */\nexport function characterizeReadout(profile: SensorReadoutConversionProfile, chargeScale: number, c?: ParsedSensorCharacteristics): SensorReadoutConversionProfile {\n const bits = c?.adcBitDepth, n = c?.readNoise, declared = n && declaredReadNoise(profile);\n if (chargeScale === 1 && !bits && !n) return profile;\n return parseSensorReadoutConversionProfile({ ...profile, regimes: profile.regimes.map((regime) => {\n const adc = regime.adc, rescale = bits !== undefined && bits !== adc.bitDepth;\n const black = rescale ? Math.round(adc.blackLevelCode * 2 ** (bits - adc.bitDepth)) : adc.blackLevelCode, saturation = rescale ? 2 ** bits - 1 : adc.digitalSaturationCode;\n const out = { ...regime, systemConversionGainElectronsPerCode: { ...regime.systemConversionGainElectronsPerCode, value: rescale ?\n regime.systemConversionGainElectronsPerCode.value * chargeScale * (adc.digitalSaturationCode - adc.blackLevelCode) / (saturation - black) : regime.systemConversionGainElectronsPerCode.value * chargeScale },\n preAdcSaturationElectronEquivalent: { ...regime.preAdcSaturationElectronEquivalent, value: regime.preAdcSaturationElectronEquivalent.value * chargeScale },\n ...(rescale ? { adc: { ...adc, bitDepth: bits, blackLevelCode: black, digitalSaturationCode: saturation } } : {}) };\n if (n) {\n const gain = regime.exposureIndexGain ?? fail(\"sensor.readNoise needs readout regimes that declare their exposure index.\");\n const from = n.highGainFromIso ?? declared!.highGainFromIso ?? Infinity, high = gain.exposureIndex >= from;\n const value = high ? n.highGainElectrons ?? declared!.highGainElectrons : n.lowGainElectrons ?? declared!.lowGainElectrons;\n out.readNoiseComponents = [{ componentId: \"electronic\", rmsElectrons: { value, evidence: regime.evidence }, evidence: regime.evidence }];\n out.exposureIndexGain = { ...gain, conversionGainMode: high ? \"high-conversion-gain\" : \"low-conversion-gain\" };\n }\n return out;\n }) });\n}\n\n/** Charge at the RAW white level and the read-plus-quantization floor of one regime, e⁻. */\nfunction regimeRange(profile: SensorReadoutConversionProfile, regime: SensorReadoutConversionRegime, capacityElectrons: number): { saturation: number; quantization: number; read: number } {\n const adc = regime.adc, gain = regime.systemConversionGainElectronsPerCode.value, digitalGain = adc.digitalGain ?? 1;\n const white = calculateSiteEffectiveSaturationCode({ physicalChargeCapacityElectrons: capacityElectrons, preAdcSaturationElectronEquivalent: regime.preAdcSaturationElectronEquivalent.value,\n systemConversionGainElectronsPerCode: gain, blackLevelCode: adc.blackLevelCode, digitalSaturationCode: adc.digitalSaturationCode, adcMaximumCode: 2 ** adc.bitDepth - 1, digitalGain }, profile, regime.regimeId);\n // Output codes per electron d/G; quantization 1/12 code² of the ADC times d², plus 1/12 of the rounding after a digital gain.\n const perCode = gain / digitalGain;\n return { saturation: (white - adc.blackLevelCode) * perCode, quantization: perCode * Math.sqrt(digitalGain * digitalGain / 12 + (digitalGain === 1 ? 0 : 1 / 12)), read: noiseOf(regime) };\n}\n\n/**\n * The report: the characteristics used, the dynamic range per ISO, and the mid-grey noise figure for `greyElectrons`, the\n * green channel's expected charge from an 18 % grey patch, after a `darkElectrons` dark charge.\n */\nexport function sensorReport(profile: SensorReadoutConversionProfile, regimeId: string, capacityElectrons: number, greyElectrons: number | undefined, darkElectrons: number,\n base: Pick<SimulateCaptureSensorReport, \"pixelPitchMicrometers\" | \"peakQuantumEfficiency\" | \"readoutSeconds\">, pattern?: SensorFixedPatternNoiseProfile): SimulateCaptureSensorReport {\n const stops = (r: SensorReadoutConversionRegime): number => { const q = regimeRange(profile, r, capacityElectrons); return Math.log2(q.saturation / Math.hypot(q.read, q.quantization)); };\n const regime = profile.regimes.find((r) => r.regimeId === regimeId)!, range = regimeRange(profile, regime, capacityElectrons);\n const report: SimulateCaptureSensorReport = { ...base, fullWellElectrons: capacityElectrons, readNoise: declaredReadNoise(profile), adcBitDepth: regime.adc.bitDepth, fixedPatternNoise: !!pattern,\n dynamicRangeStops: stops(regime), dynamicRangeByIso: profile.regimes.map((r) => ({ iso: r.exposureIndexGain?.exposureIndex ?? NaN, stops: stops(r) })).filter((e) => e.iso > 0) };\n if (greyElectrons === undefined) return report;\n const fixedPattern = pattern && Math.hypot(pattern.photoResponseNonUniformity.relativeStandardDeviation.value * greyElectrons,\n pattern.darkSignalNonUniformity.relativeStandardDeviation.value * darkElectrons, pattern.rowOffset.rmsElectrons.value, pattern.columnOffset.rmsElectrons.value);\n const noise = { photonShot: Math.sqrt(greyElectrons), read: range.read, darkCurrent: Math.sqrt(darkElectrons), quantization: range.quantization, ...(fixedPattern === undefined ? {} : { fixedPattern }), total: 0 };\n noise.total = Math.hypot(noise.photonShot, noise.read, noise.darkCurrent, noise.quantization, fixedPattern ?? 0);\n const snr = greyElectrons / noise.total;\n report.midGrey = { reflectance: 0.18, channel: \"green\", signalElectrons: greyElectrons, noiseElectrons: noise, snr, snrDecibels: 20 * Math.log10(snr || Number.MIN_VALUE), clipped: greyElectrons + darkElectrons >= range.saturation };\n return report;\n}\n"]}
@@ -15,6 +15,7 @@ import { InvalidScientificInputError } from "../core/validation.js";
15
15
  import { requireInputKeys, requireInputNonNegativeSafeInteger as requireSafeNonNegativeInteger } from "../core/record-validation.js";
16
16
  import { InvalidConfigurationError } from "../core/configuration-error.js";
17
17
  import { requirePositive as requirePositiveFinite } from "../exposure/flash-ttl.js";
18
+ import { approximatelyEqual } from "../core/shared-helpers.js";
18
19
  const SQUARE_MICROMETERS_TO_SQUARE_METERS = 1e-12;
19
20
  const WEIGHT_TOLERANCE = 1e-10;
20
21
  const AREA_TOLERANCE = 1e-10;
@@ -95,11 +96,6 @@ function validateQuadratureNode(node, index) {
95
96
  "].combinedAreaMeasureSquareMicrometers");
96
97
  return identity;
97
98
  }
98
- function approximatelyEqual(actual, expected, tolerance) {
99
- const scale = Math.max(1, Math.abs(actual), Math.abs(expected));
100
- return (Math.abs(actual - expected) <=
101
- tolerance * scale);
102
- }
103
99
  /**
104
100
  * Rechecks node coordinates, weights, geometric measures and coverage before a spatial plan is trusted
105
101
  * as scientific input.