Software Design Document (SDD) — Detailed (CDR)

Contents

Software Design Document (SDD) — Detailed (CDR)#

Field

Value

Document

SDD — Software Design Document (DETAILED / element-level)

DRD ref

ECSS-E-ST-40C Rev.1, Annex F

Container

Design Definition File (DDF) — compliance/drd/ (source), published subset in docs/sdd/

Project

msi-processor (gitlab.eopf.copernicus.eu/ipf/msi-processor)

Software criticality

Category C (ECSS-Q-ST-80C Rev.2 / ECSS-E-ST-40C Annex R)

Baselined at

CDR (detailed per-component design + internal-interface data; supersedes the PDR architectural issue)

Status

Draft for CDR

Detailed issue (CDR). This is the detailed issue of the SDD, the design constituent of the DDF (Annex F, F.1.2). It supersedes the PDR architectural issue: clauses <1>–<4> and <5.1>–<5.3> retain (and where needed refine) the baselined architecture; clause <5.4> is expanded from architectural granularity to the element-level detailed design required by Annex F <5.4.2>–<5.4.11> (component identifier/type/purpose/function/subordinates/dependencies/interfaces/resources/references/ data, with data-element dtype/dimension/range/initial value); clause <5.5> gives the full internal-interface data design (Annex F <5.5>c/e), the file-level data structures previously postponed (<5.5>d); clause <6> closes the requirements→design traceability and the critical-component measures for the DJF. Per the SDP (RD-1) §5.2.3/§5.5.2 the full SDD is delivered at CDR; implementation (SDP WP-5) starts only after CDR. Element-level code internals that are legitimately fixed during implementation (the NDA-bound L0 bit-codec body, the rigorous collinearity geolocation kernel, the down-selected atmospheric RT engine and scene classifier, exact filter/matcher tunings) are specified here at interface + algorithm level and explicitly marked [impl] where the body is finalised post-CDR. Sensor-private numbers (calibration coefficients, accuracy budgets) stay in the profile/ADFs and are not reproduced (data policy, SRS <5.8>). This SDD does not re-specify requirements (SRS, RD-4) nor algorithm mathematics (DPM RD-6 / ATBD RD-7); it specifies how the software is structured and built to satisfy them, binding each design element to its REQ-*, DPM-M-*/DPM-* and ALG-*.


<1> Introduction#

Purpose. This SDD describes the software design of msi-processor, a generic high-resolution pushbroom multispectral imager (MSI) ground-segment processor that transforms downlinked RAW Level-0 (L0c) data into calibrated, orthorectified, atmospherically corrected products up to Level-2 (L2A). At this (CDR) issue it gives both the architectural design (static, dynamic and behavioural views; patterns; standards; requirement allocation) and the detailed design (per-component core signatures and data structures, the EOProcessingUnit wrapper contracts, the CPM computing-model JSON, error handling and the element-level internal-interface data).

Objective. The SDD is the bridge from the baselined requirements (SRS, RD-4) and processing model (DPM RD-6 / ATBD RD-7) to the implementation (SDP WP-5) and the unit/integration test design (V&V plan, RD-8). Every design component traces upward to one or more REQ-* software requirements (clause <6>) and to the DPM module(s) / ATBD algorithm(s) it realises, and is the unit of work for implementation and unit test.

Content. Clause <2> lists applicable/reference documents; clause <3> adds design-specific terms. Clause <4> gives the design overview — static architecture (<4.1>), dynamic architecture and computational model (<4.2>), behaviour (<4.3>), interfaces context (<4.4>), long-lifetime design (<4.5>), memory & PU budget (<4.6>), and design standards/conventions/trade-offs (<4.7>). Clause <5> gives the software design proper — general (<5.1>), overall architecture (<5.2>), component design general (<5.3>), per-component detailed design (<5.4>) and internal-interface design (<5.5>). Clause <6> gives the requirements→design traceability and the measures for critical components.

Reason for preparation. msi-processor is an integration and ECSS productisation effort: the processing algorithms already exist as prior work (RD-9 / SRF RD-10) and the runtime platform is the EOPF CPM (eopf == 2.8.1). This detailed SDD records, at CDR, the element-level design that turns that heritage into a sensor-agnostic, configuration-driven, verifiable CPM product, so that implementation can begin against a fixed, traceable design.


<2> Applicable and reference documents#

Applicable documents (AD)#

Id

Document

Reference

AD-1

ECSS Space engineering — Software

ECSS-E-ST-40C Rev.1 (30 April 2025)

AD-2

ECSS Space product assurance — Software product assurance

ECSS-Q-ST-80C Rev.2

AD-3

EOPF CPM — Product Structure & Format Definition (PSFD) / common data model

EOPF CPM docs (eopf == 2.8.1)

Reference documents (RD)#

Id

Document

Reference

RD-1

msi-processor Software Development Plan (SDP)

compliance/software-development-plan.md

RD-2

msi-processor Software System Specification (SSS) — SYS-*

compliance/drd/sss-software-system-specification.md

RD-3

msi-processor Interface Requirements Document (IRD) — REQ-IF-*

compliance/drd/ird-interface-requirements.md

RD-4

msi-processor Software Requirements Specification (SRS) — REQ-*

compliance/drd/srs-software-requirements.md

RD-5

msi-processor Interface Control Document (ICD) — ICD-IF-*

compliance/drd/icd-interface-control.md

RD-6

msi-processor Detailed Processing Model (DPM) — DPM-M-*, DPM-PR/ADF/PRM/BKP-*

compliance/drd/dpm-data-processing-model.md

RD-7

msi-processor Algorithm Theoretical Basis Document (ATBD) — ALG-*

compliance/drd/atbd-algorithm-theoretical-basis.md

RD-8

msi-processor V&V plan (SVerP/SValP/SUITP merged)

compliance/drd/vv-plan.md

RD-9

Prior work — multispectral pushbroom preprocessing pipeline (02_scripts/: level_0.py, level_1.py, band_coreg.py, georeferencing_v1.py, pansharp.py, metrics_ips.py)

RD-10

RD-10

msi-processor Software Reuse File (SRF)

compliance/drd/srf-software-reuse-file.md

RD-11

msi-processor Traceability matrix (forward/backward, design→test)

compliance/traceability/traceability-matrix.md

RD-12

EOPF CPM API documentation (EOProduct, EOProcessingUnit, EOZarrStore, triggering / computing model)

EOPF CPM (eopf == 2.8.1)

RD-13

Cloud-native data conventions

Zarr v2/v3, CF metadata, STAC, GeoZarr


<3> Terms, definitions and abbreviated terms#

The SSS <3>, IRD <3>, SRS <3>, DPM <3> and ATBD <3> glossaries apply in full. Only design-specific terms not defined there are added here (per Annex F <3>).

Term / abbr.

Definition

Processing Unit (PU)

An EOPF CPM EOProcessingUnit: the framework-facing executable component for one processing stage

Core

The pure, framework-independent algorithmic function set behind a PU (no CPM dependency); the unit of numerical test (<pkg>.<stage>.core)

Wrapper / adapter

The thin EOProcessingUnit subclass adapting a Core to the CPM runtime (<pkg>.<stage>.unit)

Computing model (JSON)

The per-PU CPM declarative model (models/<name>_<version>.json) declaring inputs, ADFs, outputs, parameters, modes; loaded via the CPM processing_model()

Triggering payload

The CPM job order (workflow+io+…) wiring PUs into a chain (ICD <5.3.5>); distinct from the per-PU computing model

Profile

Per-sensor configuration set (schema-validated) specialising the generic chain; data only, no algorithm code

ADF

Auxiliary Data File (private instrument calibration / auxiliary input), referenced by URI as a CPM AuxiliaryDataFile

DataType / MappingDataType

CPM EOProduct/EOGroup object / Mapping[str, DataType] exchanged at a PU boundary

Block / chunk / tile

A bounded (line- or grid-) block processed independently to bound memory (Zarr/Dask unit); a halo is the overlap added to a tile for spatially-coupled stages

QAFlag

The per-pixel quality bit registry (C-COM-QAFLAG), bits per ICD <5.3.3>E

[impl]

Marks an element-level code internal whose body is legitimately finalised during implementation (post-CDR); its interface and algorithm are fixed here

[TBC@CDR]

A field whose exact encoding is sensor-private or pinned-CPM-specific and confirmed at CDR finalisation (per ICD <3>)

DDF / DJF

Design Definition File / Design Justification File (ECSS-E-ST-40C)

Component aspect

The Annex F <5.4> description axes (identifier, type, purpose, function, subordinates, dependencies, interfaces, resources, references, data)


<4> Software design overview#

This clause introduces the system context and the design before the formal design in clause <5>.

<4.1> Software static architecture#

msi-processor is a batch, non-interactive, single-process (optionally Dask-distributed) Python library + CLI. Its static architecture is a layered, three-package decomposition over the EOPF CPM, where the processing chain is a sequence of EOProcessingUnits and every product is an EOProduct persisted as cloud-native Zarr (REQ-D-01, REQ-F-PRD-01).

The three top-level components and their relationship:

                     ┌─────────────────────────────────────────────────────────────┐
                     │                     msi_processor (item)                       │
                     │                                                               │
   trigger/payload   │   msi_processor.sensors        msi_processor.common          │
   ───────────────►  │   (C-SENSORS)                  (C-COMMON)                     │
   (triggering JSON) │   profile schema + per-sensor  product/Zarr I/O, ADF access, │
                     │   profile data + ADF bindings  profile loader, provenance,   │
                     │        │  (config, data only)   QA-flag registry, chunking,   │
                     │        │                        URI/store map, orchestration  │
                     │        ▼            ▲   ▲   ▲        │                        │
                     │   ┌──────────────────────────────────────────────────────┐  │
                     │   │            msi_processor.computing (C-COMPUTING)       │  │
                     │   │   l0_decode → radiometric → enhancement → toa  ───►    │  │
                     │   │   coregistration → georeference → atmospheric ──►      │  │
                     │   │   [pansharpen] (opt, post-L2A) ;  qa (cross-cutting)   │  │
                     │   │   each stage = pure Core + thin EOProcessingUnit Wrap  │  │
                     │   └──────────────────────────────────────────────────────┘  │
                     └─────────────────────────────────────────────────────────────┘
                          ▲ ADFs (private, by URI)        ▼ EOProduct (Zarr) out
  • msi_processor.computing (C-COMPUTING) — the processing chain. One subpackage per stage; each contains a pure Core (algorithm, no CPM/IO) and a thin Wrapper (the EOProcessingUnit). This is where the heritage algorithms (RD-9) live, realising the DPM modules DPM-M-* (RD-6) and the ATBD algorithms ALG-* (RD-7).

  • msi_processor.sensors (C-SENSORS) — the adaptation layer: the versioned profile schema and per-sensor profile data + ADF bindings. It contains no instrument constants in code and no algorithm code (REQ-AD-01, REQ-D-04); the first instantiated profile is the owner’s sensor (REQ-AD-02).

  • msi_processor.common (C-COMMON) — the platform/services layer shared by all PUs: EOProduct/EOZarrStore build and I/O, ADF resolution, profile load/validation, provenance assembly, QA-flag registry/propagation, chunking/Dask wiring, URI/store mapping, run configuration, chain orchestration + computing-model handling, and the CLI.

Dependency rule (acyclic, downward only). computing depends on common and consumes a resolved sensors profile; common depends only on EOPF CPM and the reused stack (RD-10); a Core depends on neither the CPM nor common (it takes plain arrays/parameters). sensors is data + schema and depends on nothing. There is no upward or cyclic dependency.

Main relationship between major components. The chain is data-driven: each PU consumes the upstream EOProduct (+ ADFs + profile-derived parameters), produces the next-level EOProduct, and hands it on. The level breakpoints (L1A/L1B/L1C/L2A, DPM-BKP-*) are the inter-component contract (internal interface, <5.5>). QA (qa) is cross-cutting: it is invoked by every measurement PU to attach metrics and accumulate the per-pixel flag layer (REQ-F-QA-01/02).

a. Architecture of the software item. As above: a 3-layer package decomposition (computing / sensors / common) realising a CPM PU pipeline with a pure-core + thin-wrapper pattern.

b. System states/modes in which the software operates. The processor implements the three states/modes of SRS <4.1>/<8> and REQ-O-04: configured/idle (profile, inputs and ADFs resolved), processing (one or more PUs executing), terminating in completed or error/aborted (fail-stop, REQ-F-DEP-01). There is no resident, cyclic or real-time mode (SSS <4.2>). The behavioural model is in <4.3>/<5.2>e.

c. Separated mission and configuration data (Annex F <4.1>c). All sensor-, mission- and site-dependent data is externalised from the code into the profile and ADFs (REQ-AD-01/03/04, REQ-D-04), classified per the Annex F NOTE categories:

Data category (Annex F <4.1>c)

In msi-processor

Location / owner

Changeable without code change

Mission-analysis data, varies per mission

Orbit/attitude & viewing-model references, acquisition geometry sources

L0c telemetry + profile-referenced ADF (DPM-ADF-GEOM)

Yes (by reference)

Reference data specific to a family of products

Profile schema defaults, band list / spectral-response references, output CRS/grid/tiling

sensors/<profile> (data)

Yes (profile edit)

Reference data that never changes

Physical constants (π, solar-spectrum integration constants) used by Cores

code constants in common, no instrument specificity

n/a (universal)

Data depending on the specific mission (e.g. sensor calibration)

Dark/DSNU, PRNU/flat-field, bad-pixel map, radiometric gain/offset, viewing model

private ADFs, referenced by URI (DPM-ADF-*)

Yes (ADF swap, REQ-M-02)

Data varying with the higher-level system design

Output store target + chunking, breakpoints, optional-stage toggles, atmospheric source

run config / triggering JSON (ICD-IF-TRIG)

Yes (config)

This separation is the mechanism by which the same software runs across sensors and contexts (REQ-D-07, REQ-AD-01..04) and by which no private calibration is ever embedded in the code (REQ-S-01/05).

<4.2> Software dynamic architecture (computational model)#

msi-processor has no hard real-time constraint (REQ-R-05): there are no cyclic/sporadic threads, no scheduling-by-deadline, and no RMS/DMS/EDF analysis is applicable. The computational model required by Annex F <4.2>/<5.2>c is expressed as a data-flow DAG of passive components with two interchangeable execution strategies:

  • Component types. All components are passive (called, not self-scheduling). The active element is the chain runner (C-COM-ORC, realising DPM-M-PRD orchestration), which executes the PU DAG. PUs are stateless across invocations; state lives in the EOProducts flowing between them.

  • Scheduling model. Two modes selected by configuration: (1) sequential — the runner executes PUs in topological order in a single process (the CI / local-workstation path, REQ-PORT-03); (2) distributed — the per-PU Core operates on Zarr/Dask blocks and the Dask scheduler builds and runs the task graph across workers (REQ-F-ORC-02, REQ-R-04). The two share identical Cores; only the block mapping (C-COM-CHUNK) changes.

  • Means of communication. Components communicate exclusively through EOProduct objects (in-memory xarray-backed MappingDataType) and, at level breakpoints, through Zarr stores read/written via EOZarrStore (REQ-F-PRD-01). There are no mailboxes, shared mutable globals or RPC; the only inter-process channel is the Dask task graph over chunked arrays.

  • Means of synchronisation. None at application level beyond the DAG topology (a PU starts when its inputs are materialised). Concurrency control is delegated to the Dask scheduler; Cores are pure and free of shared state, so they are inherently safe to parallelise per block.

  • Distribution (Annex F <5.2>c). Optional, via Dask only; nodes are Dask workers, the unit of distribution is the block/tile. No custom inter-node protocol is defined.

Computing-model / triggering approach. The static DAG is wired declaratively by the CPM triggering payload (ICD <5.3.5>, RD-12): a workflow[] of WorkFlowUnitDescriptions naming, for the requested chain or sub-chain, the active PUs, the io (input product(s), ADF set, output target), the per-PU parameters and the breakpoints. Each PU additionally carries its own computing-model JSON (models/<name>_<version>.json, <5.4>) declaring its mandatory inputs, ADFs, outputs, parameters and modes; the CPM validates a unit’s wiring against this model. The runner (C-COM-ORC) parses the payload, resolves URIs (C-COM-IO), validates the profile (C-COM-PROFILE), instantiates the required EOProcessingUnits and executes them honouring breakpoints. This is the single mechanism realising “run a level, a sub-chain, or the full L0cL2A chain” (REQ-F-ORC-01, REQ-I-05, DPM-BKP-*).

<4.3> Software behaviour#

A single run is a finite state machine (mirroring SRS <8>):

        stateDiagram-v2
  [*] --> ConfiguredIdle: parse triggering JSON,\nresolve URIs, validate profile+ADF
  ConfiguredIdle --> Processing: inputs+ADFs+profile OK
  ConfiguredIdle --> ErrorAborted: invalid profile/ADF/input (REQ-DAT-03, REQ-S-04)
  Processing --> Processing: next PU (per DAG / breakpoints)
  Processing --> Completed: last PU OK -> publish EOProduct(s)
  Processing --> ErrorAborted: any PU fails (fail-stop, REQ-F-DEP-01)
  Completed --> [*]: status 0 + report
  ErrorAborted --> [*]: status !=0, no partial product published
    

Behaviour is deterministic and reproducible for a fixed (input, ADF, profile, processor) quadruple (REQ-F-DEP-02, REQ-REL-01); stochastic kernels (RANSAC) use a profile-fixed seed (ATBD <6>). Error handling and fault tolerance are detailed in <5.2>g and, per-component, in <5.4>.

<4.4> Interfaces context#

The external interfaces are specified at requirements level in the IRD (RD-3, REQ-IF-*) and defined concretely in the ICD (RD-5, ICD-IF-*); per Annex F <4.4>a this SDD refers to the ICD and does not re-derive them. The context (IRD <4.1> external entities E1–E6) is:

        flowchart LR
  E1[E1 L0 ingestion / downlink\nL0c product] -->|input, read-only| MSI
  E2[E2 Calibration facility / ADF provider\nprivate ADFs] -->|input, by URI| MSI
  E3[E3 Sensor-profile / config provider] -->|input, profile| MSI
  E4[E4 Orchestration / trigger\ntriggering JSON] -->|invoke| MSI
  MSI[msi-processor] -->|output, Zarr EOProduct| E5[E5 Product store / archive / dissemination]
  MSI -. executes within .-> E6[E6 EOPF CPM + storage backend]
    

Internal interfaces (between the design components of clause <5>) are designed in detail in <5.5>; external interfaces are bound by REQ-I-01..07 and controlled in the ICD (RD-5, ICD-IF-*).

<4.5> Long lifetime software#

Design choices that minimise dependency on the operating system and hardware to support a long planned lifetime and portability (Annex F <4.5>; REQ-PORT-01/02, REQ-D-09):

  • Pure Python 3.11, no native build, no GPU (REQ-R-01): the only platform assumption is a POSIX x86-64 Linux host; the Cores use portable array libraries (numpy/xarray).

  • Open data format: outputs are Zarr with CF/STAC/GeoZarr metadata (RD-13, REQ-D-09) — readable independently of this software and of any vendor runtime.

  • Location-transparent I/O: all inputs/outputs are referenced by URI and resolved through the EOPF store abstraction (C-COM-IO), so local-FS, POSIX and S3 backends are interchangeable without code change (REQ-PORT-02, REQ-I-06).

  • Externalised volatile data: instrument constants, CRS/grid, and per-stage parameters live in the profile/ADFs, so sensor and mission evolution does not touch the code (REQ-AD-01, <4.1>c).

  • Single pinned platform dependency (eopf == 2.8.1) with a documented, V&V-gated bump procedure (REQ-M-03), isolating the one component most likely to force change. The CPM coupling is confined to the Wrappers and the C-COM-* services (see <5.4>); the Cores are CPM-free.

  • Pure-core isolation keeps the algorithm bodies independent of the CPM, so a future platform change re-touches only the thin wrappers, not the numerics (REQ-D-03, REQ-M-04).

<4.6> Memory and PU budget#

Per Annex F <4.6>, the allocation of memory and processing time to components. The governing design rule is bounded, block-proportional memory: peak per-worker memory is set by the configured block/tile size (C-COM-CHUNK), not by product size (REQ-F-ORC-02, REQ-P-05, REQ-R-04), and wall-time scales with the number of tiles × workers (REQ-P-04). Absolute numeric budgets (MEM_BUDGET, THRU_SCENE) are per-profile parameters held in the private calibration/auxiliary store (SRS <5.1>, DPM-PRM-GEN-02) and are verified locally; they are not reproduced here. The qualitative allocation and the per-stage blocking strategy (used by the detailed design of <5.4>):

PU (component)

DPM module

Memory intensity

Compute intensity

Dominant cost

Blocking strategy (C-COM-CHUNK)

C-PU-L0 l0_decode

DPM-M-L0

low–med

low

full-frame assembly

per-band/per-detector line streaming

C-PU-RAD radiometric

DPM-M-RAD

low

low

element-wise per band

line-chunked; per-detector vectors broadcast

C-PU-ENH enhancement

DPM-M-ENH

med

med–high

MTFC/PSF-deconvolution + denoise kernels

tiled with halo (mandatory)

C-PU-TOA toa

DPM-M-TOA

low

low

element-wise scaling

line-chunked

C-PU-COR coregistration

DPM-M-COR

med

high

feature detect/match (SIFT/FLANN/RANSAC)

per-band-pair on reference-band overview; full-band warp

C-PU-GEO georeference

DPM-M-GEO

high

high

DEM ortho + resampling to grid

tiled resampling, windowed DEM reads

C-PU-PAN pansharpen (opt, post-L2A)

DPM-M-PAN

med–high

med

MS↔PAN fusion at PAN resolution on L2A BOA (post-AC, terminal derivative)

tiled; default-off

C-PU-ATM atmospheric

DPM-M-ATM

med

high

RT/retrieval per pixel + classification

grid-chunked; LUT-based RT

C-PU-QA qa

DPM-M-QA

low

low

metric reductions

streaming reductions

C-COMMON services

DPM-M-PRD

low

low

Zarr I/O, provenance

lazy/chunked store I/O

georeference and atmospheric are the design’s memory/compute hot-spots and the priority targets for chunking/Dask validation (REQ-P-04/05). The per-component numeric budget table is delivered with the per-profile data and verified locally (REQ-Q-03).

<4.7> Design standards, conventions and procedures#

Per Annex F <4.7>a the adopted software methods are summarised here and refer to the SDP (RD-1) §5.3 for the toolchain and process detail. The Annex F <4.7>b items:

  1. Architectural design method. EOPF CPM processing-unit architecture: the chain is a DAG of EOProcessingUnits over EOProduct/Zarr, decomposed by processing stage (one per DPM module), each stage realised as pure Core + thin Wrapper, specialised by an externalised sensor-profile layer (SDP §5.3).

  2. Detailed design method. Structured, per-component description against the Annex F <5.4> aspect set, with element-level data structures (dataclasses + numpy dtypes), Python type-annotated core signatures and run() contracts, and the CPM computing-model JSON. This issue (CDR) provides that detailed design in <5.4>/<5.5>; bodies marked [impl] are completed in WP-5.

  3. Code documentation standards. Module/class/function docstrings (NumPy/Sphinx style); the published design subset rendered to docs/sdd/ via Sphinx (SDP §5.4/§5.5).

  4. Naming conventions. PEP 8 for code; EOPF data-model conventions for product variables, bands, dimensions and command/payload fields (REQ-I-07); hierarchical component naming package.module.Component mirroring the package tree (Annex F <5.4.2>c). PU class attributes PROCESSOR_NAME = "msi_<stage>".

  5. Programming standards. Python 3.11, PEP 8 enforced by black/ruff/flake8/isort; typing by mypy; security by bandit/trivy; complexity bounded by xenon; quality gate SonarQube; tests by pytest (SDP §5.4; REQ-D-02, REQ-Q-01).

  6. Intended list of reuse components. EOPF CPM (EOProcessingUnit/EOProduct/EOZarrStore), numpy, xarray, zarr, Dask, scikit-image, OpenCV, GDAL/rasterio, pywt, scikit-learn, plus the prior-work algorithm heritage (RD-9). The authoritative reuse declaration and licences are in the SRF (RD-10); product I/O is implemented only through the CPM abstractions (REQ-D-06).

  7. Main design trade-offs.

    • Pure Core + thin Wrapper (chosen) vs algorithm-in-PU. Cost: a small adapter per stage and a stable Core↔Wrapper contract (IF-CORE-01, <5.5>). Benefit: Cores are unit-testable without the CPM runtime (CI shell runner, REQ-PORT-03, REQ-D-03), numerically verifiable in isolation, and portable across a future platform change (REQ-M-04).

    • Sensor-agnostic profile layer (chosen) vs per-sensor code forks. Benefit: a new sensor is a new profile + private ADFs, no core change (REQ-D-07, REQ-AD-01); no instrument constants in code (REQ-D-04, REQ-S-05). Cost: an up-front profile schema + validation (C-COM-PROFILE).

    • One PU per stage with optional stages toggleable (chosen) vs fused mega-stages. Benefit: level breakpoints, independent verification and re-run granularity (REQ-F-ORC-01, REQ-REL-02, REQ-M-04); pansharpen is optional/default-off, while enhancement is mandatory (its MTF-compensation / PSF-deconvolution sub-step is a required Level-1 image-quality restoration) with the denoise sub-step profile-configurable (REQ-F-ENH-03). Cost: more inter-PU EOProduct hand-offs (mitigated by lazy Zarr).

    • Chunked + optional Dask (chosen) vs whole-product in memory. Benefit: bounded memory and horizontal scaling (REQ-F-ORC-02, REQ-P-05). Cost: tiling/halo handling in spatial PUs.

    • EOProduct/Zarr-exclusive I/O (chosen) vs custom formats. Benefit: cloud-native, CPM-native, open (REQ-D-09, REQ-D-06). Cost: bound to the pinned eopf (REQ-M-03).

    • Atmospheric correction as new development (no heritage) vs reuse. The only non-reused Core; designed interface-first against the DPM (RD-6)/ATBD (RD-7) so the rest of the chain is unaffected; the RT engine and classifier are [impl]/DPM down-select.


<5> Software design#

<5.1> General#

This clause describes the software architectural design (<5.2>, <5.3>) and the detailed design (<5.4> per-component element level, <5.5> internal-interface data). The architecture is described identifying the software components, their hierarchical relationships, dependencies and interfaces (Annex F <5.1>b). Flight-software in-flight-modification design is not applicable (ground software, REQ-D-08; Annex F <5.1>c). The structure of <5.2>–<5.5> is used (Annex F <5.1>d). The DJF is referenced in <6>b.

<5.2> Overall architecture#

<5.2>a/b Static architecture (summary)#

The software item decomposes into the three top-level components of <4.1> and, within computing, nine processing-stage components plus the cross-cutting common services. The static hierarchy:

msi_processor                                   (software item)
├── computing            C-COMPUTING            (processing chain)
│   ├── l0_decode        C-PU-L0    {core, unit}
│   ├── radiometric      C-PU-RAD   {core, unit}
│   ├── enhancement      C-PU-ENH   {core, unit}   (MTFC mandatory)
│   ├── toa              C-PU-TOA   {core, unit}
│   ├── coregistration   C-PU-COR   {core, unit}
│   ├── georeference     C-PU-GEO   {core, unit}
│   ├── pansharpen       C-PU-PAN   {core, unit}   (opt, post-L2A derivative)
│   ├── atmospheric      C-PU-ATM   {core, unit}
│   └── qa               C-PU-QA    {core, unit}   (cross-cutting)
├── sensors              C-SENSORS                 (profile schema + per-sensor data + ADF bindings)
└── common               C-COMMON
    ├── product          C-COM-PRODUCT  (EOProduct build + EOZarrStore I/O)
    ├── io               C-COM-IO       (URI / store mapping: local FS, POSIX, S3)
    ├── adf              C-COM-ADF      (ADF resolution + read, private, by URI)
    ├── profile          C-COM-PROFILE  (profile schema load + validation)
    ├── provenance       C-COM-PROV     (provenance metadata assembly)
    ├── qaflags          C-COM-QAFLAG   (QA/mask flag bit registry + propagation)
    ├── chunking         C-COM-CHUNK    (tile/chunk planning + Dask wiring)
    ├── config           C-COM-CONFIG   (run configuration / parameter resolution)
    ├── orchestration    C-COM-ORC      (chain runner + triggering/computing-model handling)
    ├── errors          (C-COM-ORC)     (typed exception hierarchy — <5.4.1>)
    └── cli              C-COM-CLI      (`msi-processor` batch entry point)

Each computing stage subpackage holds exactly two subordinate components — a core (pure algorithm, package.stage.core) and a unit (the EOProcessingUnit, package.stage.unit) — realising the design pattern of REQ-D-03.

<5.2>c/d Dynamic architecture (computational model)#

As <4.2>: a passive-component data-flow DAG executed by C-COM-ORC, sequential or Dask-distributed, communicating through EOProduct/Zarr, with the CPM triggering payload + per-PU computing-model JSON as the declarative wiring. There is no real-time scheduling/analytical model (REQ-R-05); the Annex F <5.2>d items (scheduling type/model, analytical model, task priorities, timing) are not applicable and are recorded as such.

<5.2>e Software behaviour#

As <4.3>: the ConfiguredIdle Processing {Completed | ErrorAborted} automaton, fail-stop on any PU error, deterministic for a fixed input/ADF/profile/processor quadruple (REQ-F-DEP-02).

<5.2>f Consistency with the design method#

The static (package/PU), dynamic (DAG/Dask) and behavioural (state machine) views above are all expressed in the adopted CPM PU + pure-core/thin-wrapper method (<4.7>), satisfying Annex F <5.2>f. The detailed design of <5.4> instantiates this method uniformly across all components.

<5.2>g Error handling and fault tolerance principles#

The principles below are specialised, per component, in the Error/exception handling aspect of <5.4>; the typed exception hierarchy is defined in <5.4.1>.

  • Detection. Inputs/ADFs/profile validated before processing (REQ-F-L0-03, REQ-DAT-03, REQ-S-04); per-PU acceptance checks (e.g. co-registration match count REQ-F-COR-03); numerical range/saturation/no-data checks (REQ-F-RAD-04, REQ-D-05).

  • Containment region. The PU boundary is the fault-containment region: a Core raises a typed MsiProcessorError subclass, the Wrapper converts it to a flagged, reported failure; faults do not propagate silently across the DAG.

  • Reporting & logging. Structured logs + a machine-readable processing report (ICD-IF-DIAG) carry success/failure, parameters and provenance (REQ-O-02/03, REQ-HF-02); per-pixel QA flags carry localised defects (REQ-F-QA-02, C-COM-QAFLAG).

  • Recovery policy. Fail-stop (REQ-F-DEP-01; CPM triggering__error_policy: FAIL_FAST): on any PU failure the run exits non-zero and no partial or misleading product is published; the chain is re-runnable at breakpoint granularity (REQ-REL-02, REQ-F-ORC-01, DPM-BKP-*). There is no in-run retry/redundancy (ground, reprocessable).

  • Residual-hazard alignment. The residual hazard class is product-data integrity (REQ-SAF-01); the QA flags + provenance + fail-stop triad is its design mitigation.

<5.3> Software components design — General#

Per Annex F <5.3>a/b: the components, their relationships, purpose, development type (new vs reused), and the requirements allocation (each component uniquely identified). Components written for reuse expose their function and interfaces externally (the Cores; <5.4>). Handling of reused components is governed by the SRF (RD-10), per Annex N.

Id

Component (package path)

Type

Purpose (one line)

Dev type

Allocated REQ-*

DPM/ATBD

C-COMPUTING

msi_processor.computing

package

Processing chain container

new

REQ-D-01, REQ-F-ORC-01

C-PU-L0

…computing.l0_decode

PU (core+unit)

Decode/reformat L0cL1A, loss handling, assembly

reuse-adapt (RD-9 level_0)

REQ-F-L0-01..05

DPM-M-L0 / ALG-L0-*

C-PU-RAD

…computing.radiometric

PU (core+unit)

Dark/DSNU, NUC/PRNU, BPR, saturation/no-data

reuse-adapt (RD-9 level_1.NUC)

REQ-F-RAD-01..05

DPM-M-RAD / ALG-RAD-*

C-PU-ENH

…computing.enhancement

PU (core+unit)

MTF compensation (MTFC via PSF deconvolution, mandatory) + configurable denoise, radiometry-preserving

reuse-adapt (RD-9 level_1.Denoiser,sharpening)

REQ-F-ENH-01..03

DPM-M-ENH / ALG-ENH-*

C-PU-TOA

…computing.toa

PU (core+unit)

DN→TOA radiance (+opt reflectance), emit L1B

reuse-adapt (RD-9 level_1.TOA)

REQ-F-TOA-01..03

DPM-M-TOA / ALG-TOA-*

C-PU-COR

…computing.coregistration

PU (core+unit)

Inter-band co-registration to reference band

reuse-adapt (RD-9 band_coreg)

REQ-F-COR-01..03

DPM-M-COR / ALG-COR-*

C-PU-GEO

…computing.georeference

PU (core+unit)

Viewing-model geoloc + GCP + DEM ortho → L1C

reuse-adapt (RD-9 georeferencing_v1)

REQ-F-GEO-01..04

DPM-M-GEO / ALG-GEO-*

C-PU-PAN

…computing.pansharpen

PU (core+unit) (opt, post-L2A)

MS↔PAN fusion to high-res MS on L2A BOA (post-AC); terminal optional derivative, default-off

reuse-adapt (RD-9 pansharp)

REQ-F-PAN-01/02

DPM-M-PAN / ALG-PAN-*

C-PU-ATM

…computing.atmospheric

PU (core+unit)

AOT/WV, TOA→BOA, scene class + masks → L2A

new (DPM RD-6/ATBD RD-7)

REQ-F-ATM-01..04

DPM-M-ATM / ALG-ATM-*

C-PU-QA

…computing.qa

PU/library (core+unit)

QA metrics + per-pixel flag propagation

reuse-adapt (RD-9 metrics_ips)

REQ-F-QA-01/02

DPM-M-QA / ALG-QA-*

C-SENSORS

msi_processor.sensors

data + schema

Profile schema + per-sensor profile data + ADF bindings

new

REQ-AD-01..04, REQ-DAT-03

DPM <7.4>

C-COM-PRODUCT

…common.product

library

Build EOProduct, write/read Zarr via EOZarrStore

new (over CPM)

REQ-F-PRD-01, REQ-DAT-01, REQ-D-09

DPM-M-PRD

C-COM-IO

…common.io

library

URI resolution / store mapping (local/POSIX/S3)

new (over CPM)

REQ-I-06, REQ-PORT-02/03

C-COM-ADF

…common.adf

library

Resolve + read private ADFs by URI, validity check

new

REQ-F-RAD-01, REQ-DAT-02, REQ-S-04

DPM <7.2>

C-COM-PROFILE

…common.profile

library

Load + schema-validate the sensor profile

new

REQ-AD-01/02, REQ-DAT-03

ICD <5.3.6>

C-COM-PROV

…common.provenance

library

Assemble provenance metadata

new

REQ-F-PRD-02, REQ-S-05

DPM-M-PRD

C-COM-QAFLAG

…common.qaflags

library

QA/mask flag bit registry + propagation

new

REQ-F-QA-02

ICD <5.3.3>E

C-COM-CHUNK

…common.chunking

library

Tile/chunk planning + optional Dask wiring

new (over Dask)

REQ-F-ORC-02, REQ-R-04, REQ-P-05

DPM-PRM-GEN-02

C-COM-CONFIG

…common.config

library

Resolve run config / parameters

new

REQ-AD-04, REQ-O-01

ICD <5.3.5>

C-COM-ORC

…common.orchestration

executable

Chain runner; parse/execute triggering payload

new (over CPM)

REQ-F-ORC-01, REQ-F-DEP-01, REQ-I-05

DPM-M-PRD

C-COM-CLI

…common.cli

executable

msi-processor batch CLI entry point

new

REQ-I-02, REQ-O-01, REQ-HF-01

ICD <5.3.7>

Relationships. C-COM-ORC drives the C-PU-* chain; every C-PU-* Wrapper uses C-COM-PRODUCT, C-COM-ADF, C-COM-PROFILE, C-COM-PROV, C-COM-QAFLAG, C-COM-CHUNK; every C-PU-* Core uses none of common (pure). C-SENSORS is consumed (as data) by C-COM-PROFILE. C-COM-CLI is the entry point to C-COM-ORC. The backward (component→requirement) trace is the inverse of the allocation column above and is consolidated in <6> and RD-11.

<5.4> Software components design — Detailed design of each component#

Per Annex F <5.4.1>a / <5.4.2>–<5.4.11> each component is described against the aspect set, at element-level for this CDR issue. To avoid repetition, the common PU aspects and the shared data structures / exception hierarchy are stated once in <5.4.1>, then specialised per stage in <5.4.2>–<5.4.10>; the common/sensors components are in <5.4.11>–<5.4.12>.

Reading the per-component blocks. Each stage block gives: Identifier/type; Purpose & trace; Pure-core — signatures & data structures (the CPM-free algorithm, IF-CORE-01, grounded in RD-9 and the cited ALG-*); EOProcessingUnit wrapper — run() I/O (inputs/adfs/outputs/parameters/modes, ICD <5.3.4>A); Computing-model JSON (the per-PU CPM declaration); Error/exception handling. Python signatures are the designed public contract; private helper bodies and tuning are [impl].

<5.4.1> Common PU aspects, shared data structures and exception hierarchy#

Common PU aspects (apply to all C-PU-*).

  • Type (Annex F <5.4.3>). Logical: a stage subpackage in msi_processor.computing exposing core (pure functions, non-executable from the CPM’s view) and unit (an executable EOProcessingUnit subclass). Physical: Python package with modules core.py, unit.py, and models/<name>_<ver>.json.

  • Subordinates (Annex F <5.4.6>). core (algorithm) and unit (CPM adapter); unit uses core and the C-COM-* services.

  • Dependencies (Annex F <5.4.7>). The upstream-level EOProduct must exist; the required ADFs and profile parameters must be resolved (C-COM-PROFILE/ADF) before unit.run(); core has no precondition beyond valid array arguments and a validated parameter object.

  • Interfaces (Annex F <5.4.8>). Control flow: unit.run() invoked by C-COM-ORC (start = call, terminate = return Mapping[str, DataType] or raise MsiProcessorError). Data flow: in = upstream EOProduct + ADF mapping + **kwargs parameters; out = next product + updated QA layer (detailed in <5.5>).

  • Resources (Annex F <5.4.9>). CPU + RAM bounded by block size (<4.6>); no GPU, no special device; the only environmental need beyond the interface is a temp/scratch area for large intermediate Zarr (resolved via C-COM-IO).

  • References (Annex F <5.4.10>). SRS <5.2> (the stage’s REQ-F-*), DPM <8> (the module), ATBD <5> (the algorithm), SRF (RD-10)/prior-work (RD-9) for the reused Core, ICD <5.3.4> for the CPM contract.

Shared core data structures (module msi_processor.computing._types, pure, no CPM):

# Per-pixel QA bit registry — single source of truth (mirrors ICD <5.3.3>E); see C-COM-QAFLAG.
class QAFlag(enum.IntFlag):
    NO_DATA       = 1 << 0   # fill / no-data
    LOST_PACKET   = 1 << 1   # line/packet loss (REQ-F-L0-02)
    SATURATED     = 1 << 2   # at/above saturation (REQ-F-RAD-04)
    DEFECTIVE     = 1 << 3   # bad pixel replaced (REQ-F-RAD-03)
    COREG_FAIL    = 1 << 4   # co-registration failure (REQ-F-COR-03)
    CLOUD         = 1 << 5   # cloud (REQ-F-ATM-03)
    CLOUD_SHADOW  = 1 << 6   # cloud shadow (REQ-F-ATM-03)
    # bit 7 reserved [TBC@CDR]

@dataclass(frozen=True)
class BandImage:                 # one band in a generic 2-D geometry
    data: np.ndarray             # dtype float32 (working) | uint16 (DN/packed); dims (line|y, detector|x)
    qa:   np.ndarray             # dtype uint16 (QAFlag bitmask), same shape as data
    name: str                    # band id, from PROF:bands[].name (e.g. "b2")
    geom: str                    # "focal_plane" | "instrument" | "map"

@dataclass(frozen=True)
class BandStack:                 # the inter-stage payload of the Cores (one entry per band)
    bands: dict[str, BandImage]
    meta:  dict[str, object]     # acquisition/telemetry & per-stage parameters echoed for provenance

@dataclass(frozen=True)
class MetricSet:                 # QA metrics per band (ALG-QA-*)
    snr: float; rmse: float; psnr: float; mse: float; variance: float
  • data valid range is [0, 2^bit_depth 1] for DN/packed (bit_depth from the profile, DPM-PRM-GEN-01, default 12 → [0, 4095]) and [0.0, 1.0] for reflectance; intermediate arithmetic is float32 (REQ-D-05, ATBD <4.3>). qa initial value = 0; flags are OR-accumulated only (monotone, REQ-F-QA-02). Element-level numpy dtype/dim/range/fill for each variable are tabulated in <5.5> (internal interface) and the ICD (external product).

Typed exception hierarchy (module msi_processor.common.errors; the fault-containment vocabulary of <5.2>g):

class MsiProcessorError(Exception):            # base; carries .stage, .qa_flag, .report_fields
    ...
class InputValidationError(MsiProcessorError): ...   # REQ-F-L0-03, REQ-DAT-03
class ProfileValidationError(MsiProcessorError): ... # REQ-DAT-03  (C-COM-PROFILE)
class AdfResolutionError(MsiProcessorError): ...     # REQ-S-04    (C-COM-ADF)
class RadiometricError(MsiProcessorError): ...       # REQ-F-RAD-*
class CoregistrationError(MsiProcessorError): ...    # REQ-F-COR-03 (insufficient matches/residual)
class GeolocationError(MsiProcessorError): ...       # REQ-F-GEO-*  (missing DEM/model coverage)
class AtmosphericError(MsiProcessorError): ...       # REQ-F-ATM-*
class ProductWriteError(MsiProcessorError): ...      # REQ-F-PRD-01 (C-COM-PRODUCT)

A Core raises the typed error on a precondition/acceptance violation; the Wrapper catches it, attaches the relevant QAFlag and report fields, and re-raises to C-COM-ORC, which applies fail-stop (REQ-F-DEP-01). Observational deviations (QA metric out of tolerance) are warnings, never exceptions (DPM-M-QA).

EOProcessingUnit wrapper template (every unit.py; ICD <5.3.4>A):

class <Stage>Unit(eopf.computing.EOProcessingUnit):
    PROCESSOR_NAME    = "msi_<stage>"
    PROCESSOR_VERSION = "<semver>"
    PROCESSOR_LEVEL   = "<L1A|L1B|L1C|L2A|intermediate>"
    PROCESSOR_MODEL   = True                      # a models/<name>_<ver>.json is provided

    def run(self, inputs: Mapping[str, DataType],
            adfs: Optional[Mapping[str, AuxiliaryDataFile]] = None,
            mode: Optional[str] = None, **kwargs) -> Mapping[str, DataType]:
        # 1. read parameters (kwargs + resolved profile)        -> C-COM-CONFIG/PROFILE
        # 2. extract input bands + QA from EOProduct(s)          -> C-COM-PRODUCT
        # 3. load+validate required ADFs                         -> C-COM-ADF (raise AdfResolutionError)
        # 4. core call(s) over blocks (BandStack in/out)         -> <stage>.core (raise typed error)
        # 5. attach QA flags + metrics                           -> C-COM-QAFLAG, C-PU-QA
        # 6. build output EOProduct + provenance                 -> C-COM-PRODUCT, C-COM-PROV
        return {"<out_name>": product}

The wrapper is intentionally thin (no algorithm); its body is the six steps above. Per-stage I/O, parameters and modes differ and are given below.

<5.4.2> C-PU-L0 — l0_decode (DPM-M-L0; ALG-L0-DEC, ALG-L0-LOSS)#

Identifier/type. msi_processor.computing.l0_decode; PU (core+unit); PROCESSOR_LEVEL="L1A". Purpose & trace. Decode the L0c stream to per-band/per-detector focal-plane arrays, detect/handle line loss, assemble the L1A EOProduct; the ground_decode module implements the consumer-side canonical-L0 decompression (reassemble + CCSDS-122 decode, bit-exact — the real-chain L1A-side operation). Trace: REQ-F-L0-01..06; DPM-M-L0; ALG-L0-DEC/LOSS; ICD-IF-L0-*.

Pure-core — signatures & data structures (l0_decode.core):

@dataclass(frozen=True)
class LineLoss:                       # per-band loss record -> processing report + QA
    band: str; start_line: int; n_lost: int

def decode(raw: "RawL0", codec: "CodecSpec") -> dict[str, np.ndarray]:
    """ALG-L0-DEC. Ground-decode the documented canonical L0 (CCSDS space packets
    carrying CCSDS-122 lossless payloads) bit-exactly via `ground_decode` (REQ-F-L0-06);
    map other L0c source packets to {band: DN[line, detector]} in focal-plane
    geometry, applying the profile focal-plane layout (line order / flips, per-band line_factor).
    Body is sensor/NDA-specific and profile-bound -> [impl]; heritage Decoder.decode is a stub."""

def detect_and_truncate_loss(
    bands: dict[str, np.ndarray],
    line_factor: Mapping[str, int],         # DPM-PRM-L0-01; e.g. PAN band -> 2
) -> tuple[dict[str, np.ndarray], list[LineLoss]]:
    """ALG-L0-LOSS (heritage level_0.lost_package). For each band, first index l with
    DN[l]!=0 and DN[l+1]==0 marks a loss start l*=min over bands; truncate DN[:-(l*·s_b)]
    and record LineLoss. Pure numpy; deterministic."""

def check_legality(bands, telemetry, profile) -> None:
    """REQ-F-L0-03 precondition check; raises InputValidationError on a malformed/mismatched input."""

def initial_qa(bands, losses) -> dict[str, np.ndarray]:
    """Initialise QA bitmask (uint16); set LOST_PACKET on truncated/zero-filled lines, NO_DATA on fill."""

Data structures: RawL0/CodecSpec are profile-bound descriptors [impl] (decode internals are NDA, ATBD <5.1>); the output of the Core is a BandStack of geom="focal_plane" plus the telemetry meta.

EOProcessingUnit wrapper — run() I/O (l0_decode.unit.L0DecodeUnit):

Aspect

Value

inputs

{"l0": EOProduct} — opened via a profile-registered CPM store/reader (store_type, ICD <5.3.5>); read-only (REQ-F-L0-05)

adfs

none (no calibration at L0)

outputs

{"l1a": EOProduct} — DataTree ICD <5.3.1>A (/measurements/detector/<band>, /conditions/{time,orbit,attitude}, /quality/l0_flags/<band>)

parameters (**kwargs)

profile_id, profile_version, bit_depth (DPM-PRM-GEN-01), line_factor map (DPM-PRM-L0-01), legality_thresholds

modes

"nominal"

Computing-model JSON (models/msi_l0_decode_1.0.0.json; CPM schema, exact keys bound to 2.8.1 [TBC@CDR]):

{
  "name": "msi_l0_decode", "version": "1.0.0", "level": "L1A",
  "inputs":  [{"name": "l0", "mandatory": true}],
  "adfs":    [],
  "outputs": [{"name": "l1a", "mandatory": true}],
  "parameters": {
    "profile_id":      {"type": "string"},
    "profile_version": {"type": "string"},
    "bit_depth":       {"type": "integer", "default": 12},
    "line_factor":     {"type": "object",  "default": {}}
  },
  "modes": ["nominal"]
}

Error/exception handling. Malformed/mismatched input or failed profile/ADF resolution ⇒ InputValidationError before any radiometric step (REQ-F-L0-03), fail-stop; L0 never written (REQ-F-L0-05). Loss is not an error: truncate + LOST_PACKET flag + report (REQ-F-L0-02). Open point (ATBD <5.1>): non-zero corruption / CRC handling is [impl] beyond the zero-line rule.

<5.4.3> C-PU-RAD — radiometric (DPM-M-RAD; ALG-RAD-NUC/DARK/BPR/SAT)#

Identifier/type. msi_processor.computing.radiometric; PU; intermediate level (optional breakpoint DPM-BKP-RAD). Purpose & trace. Dark/DSNU subtraction, NUC/PRNU equalisation, bad-pixel replacement, saturation/no-data flagging; optional NUC derivation (calibration mode). Trace: REQ-F-RAD-01..05; DPM-M-RAD; ALG-RAD-*; ICD-IF-ADF-01.

Pure-core — signatures & data structures (radiometric.core):

@dataclass(frozen=True)
class RadiometricParams:
    bit_depth: int = 12
    g_min: float | None = None         # DPM-PRM-RAD-02 bad-pixel gain bounds
    g_max: float | None = None
    saturation: int | None = None      # else 2**bit_depth - 1
    fill_value: int | None = None
    remove_dark_fft: bool = False      # DPM-PRM-RAD-03

def estimate_nuc(dark: np.ndarray, flat: np.ndarray,
                 cut_dark: int = 0, cut_flat: int = 0) -> tuple[np.ndarray, np.ndarray]:
    """ALG-RAD-NUC (heritage NUC.compute_nuc). Column-mean dark/flat -> per-detector
    g = (mean(F)-mean(D))/(F-D), o = mean(F) - g·F. Returns (gain[detector], offset[detector])."""

def apply_nuc(dn: np.ndarray, gain: np.ndarray, offset: np.ndarray,
              dark_offset: np.ndarray | float) -> np.ndarray:
    """ALG-RAD-DARK (heritage apply_nuc_and_bpr). X = dn·g + o − d  (float32)."""

def detect_bad_pixels(gain: np.ndarray, params: RadiometricParams,
                      bpm: np.ndarray | None) -> np.ndarray:
    """ALG-RAD-BPR detection. bad = (g>=g_max)|(g<=g_min) | bpm. Returns bool[detector]."""

def replace_bad_pixels(corrected: np.ndarray, bad: np.ndarray) -> np.ndarray:
    """ALG-RAD-BPR replacement: across-track neighbour interpolation
    (½(left+right); copy nearest valid at clusters/edges). Flags DEFECTIVE upstream."""

def flag_saturation(corrected: np.ndarray, params: RadiometricParams
                    ) -> tuple[np.ndarray, np.ndarray]:
    """ALG-RAD-SAT. Clip to [0, 2^bit_depth−1]; return (clipped, qa) with SATURATED/NO_DATA set."""

def remove_dark_fft(dn: np.ndarray, dark: np.ndarray) -> np.ndarray:   # optional, DPM-PRM-RAD-03
    """heritage dark_noise_removal: Re{IFFT2(FFT2(dn) − FFT2(dark))}, clipped."""

The Core consumes/produces a BandStack (geom="focal_plane"). Per-detector gain/offset/dark are 1-D float32 vectors broadcast across lines; element-level shapes/ranges in <5.5>.

EOProcessingUnit wrapper — run() I/O (radiometric.unit.RadiometricUnit):

Aspect

Value

inputs

{"l1a": EOProduct}

adfs

{"dark"} (mandatory), {"nuc"} or {"flatfield"} (mode-dependent), {"badpixel"} (profile) — ICD <5.3.2>A

outputs

{"rad": EOProduct} (corrected DN + QA); optional {"nuc": EOProduct} calibration ADF in calibration mode

parameters

RadiometricParams fields (DPM-PRM-RAD-01..04, DPM-PRM-GEN-01)

modes

"nominal" (read nuc ADF), "calibration" (derive nuc from dark+flatfield, REQ-F-RAD-05)

Computing-model JSON (models/msi_radiometric_1.0.0.json):

{
  "name": "msi_radiometric", "version": "1.0.0", "level": "L1A",
  "inputs":  [{"name": "l1a", "mandatory": true}],
  "adfs":    [{"name": "dark", "mandatory": true},
              {"name": "nuc", "mandatory": false},
              {"name": "flatfield", "mandatory": false},
              {"name": "badpixel", "mandatory": false}],
  "outputs": [{"name": "rad", "mandatory": true},
              {"name": "nuc", "mandatory": false}],
  "parameters": {
    "bit_depth": {"type": "integer", "default": 12},
    "nuc_mode":  {"type": "string", "enum": ["read", "derive"], "default": "read"},
    "g_min": {"type": "number"}, "g_max": {"type": "number"},
    "remove_dark_fft": {"type": "boolean", "default": false}
  },
  "modes": ["nominal", "calibration"]
}

Error/exception handling. Missing/validity-mismatched ADF ⇒ AdfResolutionError (REQ-S-04), fail-stop. NUC singularity (F==D detector) and out-of-range gain are handled as bad pixels (flagged DEFECTIVE), not exceptions. All outputs clipped to the valid range (REQ-F-RAD-04, REQ-D-05). Open point (ATBD <5.2>): non-linear response term is [impl].

<5.4.4> C-PU-ENH — enhancement (mandatory) (DPM-M-ENH; ALG-ENH-*)#

Identifier/type. msi_processor.computing.enhancement; PU (mandatory); intermediate (DPM-BKP-ENH). Purpose & trace. Mandatory Level-1 image-quality restoration: MTF compensation (MTFC) via PSF deconvolution (mandatory sub-step — recovers the high-spatial-frequency content attenuated by the instrument MTF: optics + detector + platform motion) plus a profile-configurable denoise sub-step, radiometry-preserving. The stage always runs because MTFC is mandatory; MTFC materially affects radiometric/spatial product quality. Change note (CR): enhancement promoted to mandatory; “sharpening” = MTFC / PSF deconvolution. Change note (CR-3): the PSF/MTF kernel is now the mandatory per-band psf ADF (DPM-ADF-PSF) — per-band 2-D kernels (focal-plane geometry), float32, normalised to unit DC gain (sum=1) so radiometry is preserved, referenced by ADF URI (values not reproduced here) and opened read-only — not a parameter; the optional dark ADF (fft_dark only) is retained. Trace: REQ-F-ENH-01..03; DPM-M-ENH; ALG-ENH-BWLP/WAVE/PCA/MA/GAUSS/FFTDARK/DECONV.

Pure-core — signatures & data structures (enhancement.core):

DenoiseMethod = Literal["butterworth", "wavelet", "pca", "moving_average", "gaussian", "fft_dark"]

def denoise(image: np.ndarray, method: DenoiseMethod, params: Mapping[str, object],
            dark: np.ndarray | None = None) -> np.ndarray:
    """Dispatch to the selected denoiser (heritage Denoiser.*). Returns clipped float32."""

# concrete kernels (each pure; bodies reuse skimage/scipy/pywt/sklearn — RD-10):
def butterworth_lowpass(image, cutoff_ratio=0.2, order=10.0,
                        squared=False, npad=0) -> np.ndarray: ...     # ALG-ENH-BWLP
def wavelet_visushrink(image, wavelet="db3", levels=20,
                       sigma_scale=1/3, mode="soft") -> np.ndarray: ...# ALG-ENH-WAVE
def pca_denoise(image, n_components: int) -> np.ndarray: ...          # ALG-ENH-PCA
def moving_average(image, n: int = 60) -> np.ndarray: ...             # ALG-ENH-MA ((2N+1) sliding)
def gaussian_smooth(image, ksize=5, sigma=None) -> np.ndarray: ...    # ALG-ENH-GAUSS (σ=std if None)
def fft_dark_subtract(image, dark) -> np.ndarray: ...                 # ALG-ENH-FFTDARK

def mtf_compensate(image: np.ndarray, psf_kernel: np.ndarray) -> np.ndarray:
    """ALG-ENH-DECONV — MTF compensation (MTFC) via PSF deconvolution (MANDATORY sub-step).
    Restores the high-spatial-frequency content attenuated by the instrument MTF
    (optics + detector + platform motion); materially affects radiometric/spatial quality.
    Heritage sharpening.deconvolution_kernel (cv2.filter2D), clipped. PSF-derived per-band
    kernel (broader for the PAN band); `psf_kernel` is sourced at run() from the mandatory
    `psf` ADF (DPM-ADF-PSF), not a parameter."""

EOProcessingUnit wrapper — run() I/O (enhancement.unit.EnhancementUnit):

Aspect

Value

inputs

{"rad": EOProduct}

adfs

{"psf"} (mandatoryDPM-ADF-PSF, per-band 2-D PSF/MTF kernel for MTFC, float32, unit-DC-gain normalised, by URI/read-only), {"dark"} (optional, only if fft_dark)

outputs

{"enh": EOProduct} (MTFC-restored, optionally denoised bands + QA-metric deltas via C-PU-QA)

parameters

{mtfc:{regularization}, denoise:{method,params,enabled}} (DPM-PRM-ENH-01..05); per-band overrides. PSF kernel is the mandatory psf ADF (DPM-ADF-PSF), no longer a parameter

modes

"nominal"; the stage always runs (MTFC mandatory); only the denoise sub-step is profile-configurable (REQ-F-ENH-03)

Computing-model JSON (models/msi_enhancement_1.0.0.json): inputs:[{rad,true}], adfs:[{psf,true},{dark,false}], outputs:[{enh,true}], parameters carrying the nested mtfc (PSF deconvolution, always applied; kernel from the psf ADF) and denoise (configurable) objects, modes:["nominal"].

Error/exception handling. Mandatory stage — MTFC (PSF deconvolution) is always applied; only the denoise sub-step is profile-configurable (REQ-F-ENH-03). Outputs always clipped to the valid range; radiometric impact reported via QA metrics (REQ-F-QA-01), never silently applied. A missing/invalid psf ADF (DPM-ADF-PSF) or a denoise method-not-in-allowed-set ⇒ InputValidationError at profile validation (C-COM-PROFILE), not at run.

<5.4.5> C-PU-TOA — toa (DPM-M-TOA; ALG-TOA-RAD/REF)#

Identifier/type. msi_processor.computing.toa; PU; PROCESSOR_LEVEL="L1B". Purpose & trace. DN→TOA radiance (mandatory) and TOA reflectance (optional); emit L1B. Trace: REQ-F-TOA-01..03; DPM-M-TOA; ALG-TOA-RAD/REF; ICD-IF-OUT-*.

Pure-core — signatures & data structures (toa.core):

def dn_to_radiance(dn: np.ndarray, gain: np.ndarray | float,
                   offset: np.ndarray | float) -> np.ndarray:
    """ALG-TOA-RAD (heritage TOA.dn_to_radiance). L = (DN − offset)·gain  (float32).
    NOTE: the heritage `radiance -= radiance.min()` is DROPPED (ATBD <5.3> open point 3)."""

def radiance_to_reflectance(radiance: np.ndarray, esun: float,
                            sun_zenith_rad: float, earth_sun_dist_au: float) -> np.ndarray:
    """ALG-TOA-REF. ρ = π·L·d² / (E·cos θ_s). Clipped to [0,1]."""

def earth_sun_distance(doy: int) -> float:
    """d ≈ 1 − 0.01672·cos(0.9856°·(DOY−4)); or solar ephemeris [impl]."""

def solar_geometry(acq_time: "datetime", lon: float, lat: float) -> tuple[float, float]:
    """Proper solar ephemeris -> (sun_zenith_rad, sun_azimuth_rad).
    Replaces the heritage sub-point conflation (ATBD <5.3> open point 3) -> [impl] ephemeris choice."""

EOProcessingUnit wrapper — run() I/O (toa.unit.ToaUnit):

Aspect

Value

inputs

{"enh": EOProduct} (enhancement is a mandatory upstream stage)

adfs

{"radiometric"} (mandatory), {"spectral"} (for reflectance) — ICD <5.3.2>A

outputs

{"l1b": EOProduct}/measurements/radiance/<band> (+ /measurements/reflectance/<band>), QA, provenance

parameters

emit_reflectance (DPM-PRM-TOA-03), esun per band & geometry source (DPM-PRM-TOA-01/02, mostly ADF/derived)

modes

"nominal"

Computing-model JSON (models/msi_toa_1.0.0.json): inputs:[{enh}], adfs:[{radiometric,true},{spectral,false}], outputs:[l1b], parameters:{emit_reflectance:{boolean,default:false}}, modes:["nominal"].

Error/exception handling. Validity-mismatched radiometric/spectral ADF ⇒ AdfResolutionError, fail-stop. Non-physical (negative) radiance clipped + flagged. ESUN is profile/ADF data — the heritage hard-coded ESUN table is not carried over (REQ-AD-01, ATBD <5.3>).

<5.4.6> C-PU-COR — coregistration (DPM-M-COR; ALG-COR-FEAT/HOM/WARP)#

Identifier/type. msi_processor.computing.coregistration; PU; intermediate (DPM-BKP-COR). Purpose & trace. Feature-based inter-band alignment to a reference band with fail-stop on failure. Trace: REQ-F-COR-01..03; DPM-M-COR; ALG-COR-*.

Pure-core — signatures & data structures (coregistration.core):

@dataclass(frozen=True)
class CoregParams:
    reference_band: str                       # DPM-PRM-COR-01 (heritage "b2")
    clahe_clip: float = 2.0; clahe_grid: tuple[int, int] = (8, 8)   # DPM-PRM-COR-02
    match_fraction: float = 0.10              # top-fraction matches kept
    min_keypoints: int = 20; min_keypoints_pan: int = 40
    ransac_tau: float = 5.0                   # reprojection threshold (px)
    max_residual: float | None = None         # acceptance vs BAND_COREG (DPM-PRM-COR-04, private)
    seed: int = 0                             # deterministic RANSAC (REQ-F-DEP-02)

@dataclass(frozen=True)
class CoregResidual:
    band: str; n_inliers: int; rms_residual_px: float; accepted: bool

def estimate_homography(band: np.ndarray, reference: np.ndarray, params: CoregParams
                        ) -> tuple[np.ndarray, CoregResidual]:
    """ALG-COR-FEAT+HOM: 8-bit normalise → CLAHE → SIFT → FLANN(top-fraction) → RANSAC H(3×3).
    Raises CoregistrationError on insufficient keypoints/matches. Reuses cv2; tuning [impl]."""

def warp_to_reference(band: np.ndarray, H: np.ndarray, shape: tuple[int, int]) -> np.ndarray:
    """ALG-COR-WARP: I_reg = warpPerspective(band, H, shape)."""

def coregister(bands: Mapping[str, np.ndarray], params: CoregParams
               ) -> tuple[dict[str, np.ndarray], list[CoregResidual]]:
    """Per non-reference band: estimate H, check residual ≤ max_residual, warp; crop to common
    extent. Raises CoregistrationError if any required band is not accepted (REQ-F-COR-03)."""

EOProcessingUnit wrapper — run() I/O (coregistration.unit.CoregistrationUnit):

Aspect

Value

inputs

{"l1b": EOProduct}

adfs

none

outputs

{"cor": EOProduct} (co-registered stack + residual QA)

parameters

CoregParams fields (DPM-PRM-COR-01..04)

modes

"nominal"

Computing-model JSON (models/msi_coregistration_1.0.0.json): inputs:[l1b], adfs:[], outputs:[cor], parameters:{reference_band, match_fraction, ransac_tau, min_keypoints, …}, modes:["nominal"].

Error/exception handling. Insufficient keypoints/matches or residual outside acceptance ⇒ CoregistrationError, the affected band flagged COREG_FAIL, and fail-stop (REQ-F-COR-03, REQ-F-DEP-01) — no misregistered product is emitted. RANSAC uses seed for reproducibility (REQ-F-DEP-02).

<5.4.7> C-PU-GEO — georeference (DPM-M-GEO; ALG-GEO-ORBIT/GSD/GCP/ORTHO/RESAMP)#

Identifier/type. msi_processor.computing.georeference; PU; PROCESSOR_LEVEL="L1C". Purpose & trace. Viewing-model geolocation + GSD, optional GCP refinement, DEM orthorectification, resample to the profile CRS/grid; emit L1C. Trace: REQ-F-GEO-01..04; DPM-M-GEO; ALG-GEO-; ICD-IF-OUT-.

Pure-core — signatures & data structures (georeference.core):

@dataclass(frozen=True)
class PlatformState:                 # ALG-GEO-ORBIT
    lat: float; lon: float; altitude_m: float; ground_velocity_ms: float
@dataclass(frozen=True)
class Geotransform:                  # GDAL affine [ulx, xres, 0, uly, 0, -yres]
    ulx: float; xres: float; uly: float; yres: float; crs_wkt: str

def compute_gsd(altitude_m: float, pixel_pitch_m: float, focal_length_m: float) -> float:
    """ALG-GEO-GSD (heritage). GSD = altitude·pitch/focal."""

def orbit_state(viewing_model: "ViewingModel", acq_time: "datetime") -> PlatformState:
    """ALG-GEO-ORBIT: propagate orbit (SGP4/ephemeris) to acquisition; sub-point + altitude.
    Engine choice (pyorbital/skyfield) [impl]."""

def geolocate(shape: tuple[int, int], state: PlatformState,
              viewing_model: "ViewingModel", dem: np.ndarray) -> np.ndarray:
    """ALG-GEO-ORTHO. Rigorous collinearity (line-of-sight ∩ ellipsoid+DEM) per pixel.
    CDR-target body -> [impl]; PDR heritage = reference-image homography fallback."""

def refine_with_gcp(image: np.ndarray, reference: np.ndarray, params: CoregParams) -> np.ndarray:
    """ALG-GEO-GCP: reuse coregistration.core machinery against a geolocated reference."""

def resample_to_grid(image: np.ndarray, src_geo, dst: Geotransform,
                     resampling: str) -> tuple[np.ndarray, Geotransform]:
    """ALG-GEO-RESAMP: warp to the profile CRS/grid/resolution (GDAL/rasterio + PROJ/osr)."""

EOProcessingUnit wrapper — run() I/O (georeference.unit.GeoreferenceUnit):

Aspect

Value

inputs

{"cor": EOProduct} (+ orbit/attitude from the L1A conditions)

adfs

{"viewing_model"}, {"dem"} (mandatory), {"gcp"} (optional) — ICD <5.3.2>A

outputs

{"l1c": EOProduct} — gridded /measurements/reflectance/<band>, /conditions/geolocation/{x,y,spatial_ref}, QA, provenance

parameters

crs, grid, resolution, resampling, use_gcp (DPM-PRM-GEO-01..03); interior geometry p,f from viewing_model

modes

"nominal"

Computing-model JSON (models/msi_georeference_1.0.0.json): inputs:[cor], adfs:[{viewing_model,true},{dem,true},{gcp,false}], outputs:[l1c], parameters:{crs, grid, resolution, resampling, use_gcp}, modes:["nominal"].

Error/exception handling. Missing DEM/viewing-model coverage for the footprint/epoch ⇒ GeolocationError, fail-stop. Geolocation error verified locally vs GEO_CE90 (REQ-F-GEO-03, REQ-Q-03). Open point (ATBD <5.7> open point 2): the rigorous collinearity body replaces the heritage homography at implementation [impl].

<5.4.8> C-PU-PAN — pansharpen (optional) (DPM-M-PAN; ALG-PAN-ALIGN/FUSE)#

Identifier/type. msi_processor.computing.pansharpen; PU (opt, terminal post-L2A derivative). Purpose & trace. MS↔PAN fusion to PAN resolution as a terminal, optional, default-off post-L2A derivative product running after atmospheric correction (C-PU-ATM): its input is the L2A BOA (surface) reflectance product, and it emits a separate pan-sharpened L2A derivative with spectral-fidelity QA. Trace: REQ-F-PAN-01/02; DPM-M-PAN; ALG-PAN-*.

Change note (CR-4). Pan-sharpening is moved out of the L1B→L1C transition and re-placed as an optional terminal step after atmospheric correction. Rationale: pan-sharpening trades spectral/radiometric fidelity for spatial sharpness, so it is a visual/derivative product, not a science-grade input to quantitative retrieval/indices; and atmospheric correction must precede fusion so it operates on physically-meaningful BOA (surface) reflectance rather than TOA values (empirical guidance: Lin et al. 2015, WorldView-2 AC×pan-sharpen study). L1C therefore stays band-co-registered orthorectified TOA reflectance with no pan-sharpening; the AC→pan-sharpen order is settled.

Pure-core — signatures & data structures (pansharpen.core):

FusionMethod = Literal["simple_mean", "brovey", "gs", "ihs", "atrous"]   # heritage = simple_mean

def align_ms_to_pan(ms_stack: Mapping[str, np.ndarray], pan: np.ndarray,
                    params: CoregParams) -> dict[str, np.ndarray]:
    """ALG-PAN-ALIGN: reuse coregistration.core (CLAHE→SIFT→FLANN→RANSAC→warp) onto the PAN grid."""

def fuse(ms_aligned: Mapping[str, np.ndarray], pan: np.ndarray,
         method: FusionMethod = "simple_mean") -> dict[str, np.ndarray]:
    """ALG-PAN-FUSE. simple_mean: ½(MS_b + PAN), clipped. Other methods [impl] (ATBD <5.9> open point 6)."""

EOProcessingUnit wrapper — run() I/O (pansharpen.unit.PansharpenUnit):

Aspect

Value

inputs

{"l2a": EOProduct} (BOA-MS stack + PAN band) — the atmospherically-corrected L2A product (post-AC)

adfs

none

outputs

{"pan": EOProduct} (pan-sharpened L2A derivative: fused BOA-MS + spectral-fidelity QA)

parameters

enabled, method, pan_band (DPM-PRM-PAN-01)

modes

"nominal"; skipped when optional_stages.pansharpen=false

Computing-model JSON (models/msi_pansharpen_1.0.0.json): inputs:[l2a], adfs:[], outputs:[pan], parameters:{enabled, method, pan_band}, modes:["nominal"].

Open point (PAN reflectance handling) — [impl]/profile. The AC→pan-sharpen order is settled, but the PAN-band reflectance to fuse against BOA-MS is not: rigorous AC is band-specific and the broadband PAN band is too broad for a well-defined atmospheric correction. “After AC” therefore requires either (a) producing a BOA-PAN approximation, or (b) fusing BOA-MS with TOA-PAN — a TOA/BOA domain mismatch that weakens component-substitution methods (Brovey/GS/PCA). The choice between (a) and (b) is an open [impl]/profile decision and is not resolved here.

Error/exception handling. Optional/default-off. Alignment failure ⇒ flag + skip fusion (the L2A product remains valid at MS resolution) rather than fail-stop. Spectral fidelity reported vs the per-profile budget (REQ-F-PAN-02); if unmet, a spectral-preserving method is selected by profile.

<5.4.9> C-PU-ATM — atmospheric (new) (DPM-M-ATM; ALG-ATM-PAR/RT/SCM)#

Identifier/type. msi_processor.computing.atmospheric; PU; PROCESSOR_LEVEL="L2A". Purpose & trace. AOT/WV ingest or retrieval, TOA→BOA inversion, scene classification + cloud/shadow masks; emit L2A. Trace: REQ-F-ATM-01..04; DPM-M-ATM; ALG-ATM-*. No prior-work heritage — interface-first new development; RT engine and classifier are down-selected in the DPM/[impl].

Pure-core — signatures & data structures (atmospheric.core):

@dataclass(frozen=True)
class AtmParams:
    aot: np.ndarray | float; water_vapour: np.ndarray | float; ozone: float | None = None

def get_atmospheric_parameters(toa_refl: Mapping[str, np.ndarray], mode: Literal["ingest","retrieve"],
                               aux: "AtmAux", params: Mapping[str, object]) -> AtmParams:
    """ALG-ATM-PAR: ingest auxiliary AOT/WV or retrieve from imagery (DDV / band-ratio). [impl]/DPM."""

def toa_to_boa(toa_refl: Mapping[str, np.ndarray], atm: AtmParams, geometry: "SceneGeometry",
               dem: np.ndarray, rt_lut: "RTLut") -> dict[str, np.ndarray]:
    """ALG-ATM-RT: invert 6S surface-reflectance eqn per pixel via RT-LUT interpolation
    (ATBD <5.8>). Engine (Py6S/Sen2Cor-LUT/ACOLITE) down-selected -> [impl]. Returns BOA in [0,1]."""

def classify_scene(boa: Mapping[str, np.ndarray], params: Mapping[str, object]
                   ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
    """ALG-ATM-SCM: returns (scene_class, cloud_mask, cloud_shadow_mask). Classifier -> [impl]."""

EOProcessingUnit wrapper — run() I/O (atmospheric.unit.AtmosphericUnit):

Aspect

Value

inputs

{"l1c": EOProduct} (TOA reflectance)

adfs

{"atmospheric"} (AOT/WV/RT-LUT, ingest mode), {"dem"} — ICD <5.3.2>A

outputs

{"l2a": EOProduct}/measurements/reflectance/<band> (BOA), /quality/scene_classification, masks, QA, provenance

parameters

mode (retrieve|ingest), model/LUT id, classification options (DPM-PRM-ATM-01/02)

modes

"nominal"

Computing-model JSON (models/msi_atmospheric_1.0.0.json): inputs:[l1c], adfs:[{atmospheric,false},{dem,true}], outputs:[l2a], parameters:{mode:{enum:[retrieve,ingest]}, model, classification_opts}, modes:["nominal"].

Error/exception handling. Missing atmospheric/DEM coverage for the footprint/epoch ⇒ AtmosphericError, fail-stop. Cloud / cloud-shadow pixels flagged CLOUD/CLOUD_SHADOW (REQ-F-ATM-03). Open point (ATBD <5.8> open point 1): parameter-retrieval method, RT engine and classifier are to be defined and validated in the DPM before/at CDR; this SDD fixes the interface and the candidate algorithm, the bodies are [impl].

<5.4.10> C-PU-QA — qa (cross-cutting) (DPM-M-QA; ALG-QA-*)#

Identifier/type. msi_processor.computing.qa; PU/library invoked by every measurement PU. Purpose & trace. Compute per-band metrics and merge/propagate the per-pixel flag layer. Trace: REQ-F-QA-01/02; DPM-M-QA; ALG-QA-*.

Pure-core — signatures & data structures (qa.core):

def compute_metrics(test: np.ndarray, reference: np.ndarray | None) -> MetricSet:
    """ALG-QA-* (heritage metrics_ips). SNR=20log10(mean/std); referential RMSE/MSE/PSNR vs
    reference (aligned to common extent); variance. Returns MetricSet (∞ PSNR when MSE=0)."""

def align_extent(a: np.ndarray, b: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
    """Crop both to the common extent before referential comparison (heritage run_validation)."""

def merge_flags(*masks: np.ndarray) -> np.ndarray:
    """QA bit OR-accumulation (monotone, REQ-F-QA-02). Same as C-COM-QAFLAG.merge."""

EOProcessingUnit wrapper — run() I/O (qa.unit.QaUnit). Usable standalone (inputs:{"test", "reference"?}outputs:{"metrics"}) and as a library called inline by the other PUs (step 5 of the wrapper template). modes:["nominal"]. Computing-model JSON models/msi_qa_1.0.0.json: inputs:[{test,true},{reference,false}], outputs:[metrics], parameters:{metric_set, reference_sel}.

Error/exception handling. Metrics are observational and never abort the chain; a metric outside the configured tolerance is a warning in the report (REQ-O-02). Metrics carry no private data (REQ-HF-02).

<5.4.11> common services (C-COM-*) — detailed design#

Library/executable components; the data aspect dominates (Annex F <5.4.11>). The public contracts:

# C-COM-PRODUCT (msi_processor.common.product) — REQ-F-PRD-01, REQ-DAT-01, REQ-D-09
def build_eoproduct(measurements: Mapping[str, BandImage], conditions: Mapping, quality: Mapping,
                    attrs: Mapping) -> EOProduct: ...        # DataTree per ICD <5.3.3>A
def write_product(product: EOProduct, uri: str, store_params: Mapping, mode: str) -> None: ...
def read_product(uri: str, store_type: str = "zarr") -> EOProduct: ...   # via EOZarrStore; raises ProductWriteError
def extract_band_stack(product: EOProduct, bands: list[str]) -> BandStack: ...

# C-COM-IO (…common.io) — REQ-I-06, REQ-PORT-02/03
def resolve_uri(uri: str) -> "StoreHandle": ...             # local FS | posix | s3:: (no container/Dask/S3 needed for local)
def open_store(uri: str, mode: str, store_params: Mapping | None = None) -> EOZarrStore: ...

# C-COM-ADF (…common.adf) — REQ-F-RAD-01, REQ-DAT-02, REQ-S-04
def resolve_adf(binding: "AdfBinding", acq_time, sensor_id, profile_id) -> AuxiliaryDataFile: ...
def load_adf(adf: AuxiliaryDataFile) -> "AdfData": ...      # read-only
def check_validity(adf: AuxiliaryDataFile, acq_time, sensor_id) -> None: ...  # raises AdfResolutionError

# C-COM-PROFILE (…common.profile) — REQ-AD-01/02, REQ-DAT-03
@dataclass(frozen=True)
class Profile: ...                                          # typed mirror of ICD <5.3.6> schema
def load_profile(uri: str) -> Profile: ...
def validate_profile(raw: Mapping, schema_version: str) -> Profile: ...       # JSON-Schema; raises ProfileValidationError

# C-COM-PROV (…common.provenance) — REQ-F-PRD-02, REQ-S-05
def build_provenance(run_ctx: "RunContext") -> dict: ...    # ids/versions/params/timestamp; NO coefficients

# C-COM-QAFLAG (…common.qaflags) — REQ-F-QA-02
class QAFlag(enum.IntFlag): ...                             # the registry of <5.4.1>
def set_flag(mask: np.ndarray, condition: np.ndarray, flag: QAFlag) -> np.ndarray: ...
def merge(a: np.ndarray, b: np.ndarray) -> np.ndarray: ...  # bitwise OR (monotone)

# C-COM-CHUNK (…common.chunking) — REQ-F-ORC-02, REQ-R-04, REQ-P-05
@dataclass(frozen=True)
class ChunkPlan: chunks: tuple[int, ...]; halo: int
def plan_chunks(shape, chunk_size, halo: int = 0) -> ChunkPlan: ...
def map_over_blocks(func, stack: BandStack, plan: ChunkPlan, use_dask: bool) -> BandStack: ...

# C-COM-CONFIG (…common.config) — REQ-AD-04, REQ-O-01
@dataclass(frozen=True)
class RunContext: profile: Profile; adf_ids: dict; params: dict; input_ids: list; processor_version: str
def resolve_config(payload: Mapping, profile: Profile) -> RunContext: ...

# C-COM-ORC (…common.orchestration) — REQ-F-ORC-01, REQ-F-DEP-01, REQ-I-05
@dataclass(frozen=True)
class RunResult: status: int; report: dict; outputs: dict[str, str]
def run_chain(payload: Mapping) -> RunResult: ...          # parse → resolve → validate → execute PUs → publish
                                                           # FAIL_FAST: any MsiProcessorError → status!=0, no publish

# C-COM-CLI (…common.cli) — REQ-I-02, REQ-O-01, REQ-HF-01
def main(argv: list[str]) -> int: ...                      # `msi-processor <payload.{yaml,json}>`; exit code = RunResult.status

Per-component element-level data (dtypes/ranges/initial values) for the product DataTree, the ADF content and the profile schema are governed by the ICD (<5.3.3>/<5.3.2>/<5.3.6>) and reproduced as the internal data structures of <5.5>; they are not duplicated field-by-field here (Annex F <5.4.10> reference rule).

  • C-COM-PRODUCT. Type: library over CPM EOProduct/EOGroup/EOZarrStore. Data: the output DataTree (measurements/conditions/quality + root provenance/STAC attrs), chunking and CRS encoding per ICD <5.3.3>A/C/D. Errors: ProductWriteError on a store failure (fail-stop).

  • C-COM-IO. Type: library. Function: URI→store handle for local FS / POSIX / S3; guarantees a local-FS path with no container/Dask/S3 for CI (REQ-PORT-03). Errors: unresolvable URI ⇒ InputValidationError.

  • C-COM-ADF. Type: library. Function: select the validity-/version-matched ADF, open read-only, load content. Errors: AdfResolutionError on missing/mismatched ADF (REQ-S-04).

  • C-COM-PROFILE. Type: library. Function: load + JSON-Schema-validate the versioned profile to a typed Profile; an invalid/incomplete profile ⇒ ProfileValidationError before processing (REQ-DAT-03). Data: the profile schema is the in-model id space (paths = item ids, ICD <5.1>b).

  • C-COM-QAFLAG. Type: library. Data: the fixed QAFlag bitfield (ICD <5.3.3>E); the merge is bitwise-OR (monotone accumulation).

  • C-COM-CHUNK. Type: library over Dask. Function: compute the block grid + halo per stage (<4.6>) and map the Core over blocks sequentially or via Dask; the halo guarantees spatially-coupled stages (ENH/COR/GEO/PAN) see sufficient context.

  • C-COM-CONFIG / C-COM-ORC / C-COM-CLI. Type: config library + executables. Function: turn the triggering payload (ICD <5.3.5>) into a RunContext, topologically order and execute the active PUs honouring breakpoints, enforce FAIL_FAST, publish on success, return exit status + report (ICD-IF-DIAG). Interfaces (Annex F <5.4.8>, executable): control in = main(argv) / run_chain, terminate = exit code; data in = payload + config/profile files, data out = exit status + report + EOProduct(s).

<5.4.12> C-SENSORS — sensor adaptation layer (data + schema)#

Identifier/type. msi_processor.sensors; logical: data + schema package; physical: a JSON-Schema file (sensors/profile.schema.json), per-sensor profile JSON instances (sensors/<profile_id>.json) and their ADF binding tables. Non-executable — no algorithm code (REQ-D-04). Purpose & trace. Externalise all sensor-specific data so the generic chain is sensor-agnostic; a new sensor = a new profile (+ private ADFs), no core change. Trace: REQ-AD-01..04, REQ-DAT-03, REQ-D-07; ICD-IF-PROF-*; DPM <7.4>. Data (Annex F <5.4.11>). The profile object structure is the ICD <5.3.6> schema (id/version/sensor_id, bands[], focal_plane, adf_bindings, per-stage parameter blocks, output, optional_stages, breakpoints); element-level field ids, types, ranges and legality are controlled in the ICD and validated by C-COM-PROFILE. The first instantiated profile is the project owner’s sensor (REQ-AD-02); its numeric content is private (data policy). Subordinates. the schema file (parent) ‘composes’ the per-sensor profile instances and the ADF-binding tables.

<5.5> Internal interface design#

Per Annex F <5.5>a/b the internal interfaces among the identified components, organised as the interfaces map; per Annex F <5.5>c/e each interface lists its data elements (name, type, dimension, range, initial value). The previously-postponed file-level data structures (Annex F <5.5>d — the on-disk EOProduct/Zarr layout and the profile-file schema) are now controlled in the ICD (RD-5 <5.3.2/3/6>) and referenced here.

Interface inventory (component ↔ component).

If id

From → To

Mechanism

Data (element level)

IF-CHAIN-01

C-COM-ORC → C-PU-*.unit

run(inputs, adfs, mode, **params) call

inputs: Mapping[str,DataType], adfs: Mapping[str,AuxiliaryDataFile], params: dict; return Mapping[str,DataType]

IF-PROD-01

C-PU-L0 → C-PU-RAD

in-memory EOProduct (L1A)

/measurements/detector/<band> uint16 (line,detector) [0,2^B−1]; telemetry; /quality/l0_flags uint8

IF-PROD-02

C-PU-RAD → C-PU-ENH → C-PU-TOA

EOProduct (intermediate)

corrected DN float32→uint16; quality/mask/<band> uint16 (QAFlag)

IF-PROD-03

C-PU-TOA → C-PU-COR (L1B)

EOProduct + Zarr breakpoint DPM-BKP-L1B

/measurements/radiance/<band> (+reflectance), instrument geometry, QA, provenance

IF-PROD-04

C-PU-COR → C-PU-GEO (L1C)

EOProduct + Zarr breakpoint DPM-BKP-L1C

co-registered → orthorectified TOA reflectance (no pan-sharpening), conditions/geolocation/{x,y,spatial_ref}, QA

IF-PROD-05

C-PU-GEO → C-PU-ATM (L2A)

EOProduct + Zarr breakpoint DPM-BKP-L2A

BOA reflectance, quality/scene_classification, masks, QA, provenance

IF-PROD-06

C-PU-ATM → C-PU-PAN (opt, terminal)

EOProduct (pan-sharpened L2A derivative)

BOA-MS fused with PAN to PAN resolution, spectral-fidelity QA; default-off (PAN-reflectance handling [impl]/profile, <5.4.8>)

IF-CORE-01

C-PU-.unit → C-PU-.core

pure function call

BandStack in / (BandStack | arrays + MetricSet/residuals) out; no CPM/IO (<5.4.1>)

IF-SVC-01

C-PU-*.unit → C-COM-PRODUCT

function call

build_eoproduct/read/write/extract_band_stack

IF-SVC-02

C-PU-*.unit → C-COM-ADF

function call (URI)

AuxiliaryDataFileAdfData (dark, PRNU, gain/offset, BPM, PSF/MTF kernel (DPM-ADF-PSF), viewing model, DEM, AOT/WV)

IF-SVC-03

C-PU-*.unit → C-COM-PROFILE

function call

Profile (per-stage parameter block)

IF-SVC-04

C-PU-*.unit → C-COM-QAFLAG

function call

np.ndarray uint16 QA layer (set_flag/merge)

IF-SVC-05

C-PU-*.unit → C-COM-PROV

function call

provenance dict (ids/versions/params/timestamp)

IF-SVC-06

C-COM-ORC/PRODUCT → C-COM-IO

function call

URI→EOZarrStore handle (local FS / POSIX / S3)

IF-SVC-07

C-PU-*.unit → C-COM-CHUNK

function call

ChunkPlan; map_over_blocks(core, stack, plan, use_dask)

IF-TRIG-01

E4 → C-COM-CLI/ORC

triggering JSON (ICD <5.3.5>)

workflow[], io, breakpoints, dask_context, params

Element-level data structures of the dominant interfaces (Annex F <5.5>c/e):

  • Inter-PU EOProduct (IF-PROD-01..05) — the on-disk/in-memory DataTree, dtypes, dims, chunking, CRS encoding, QA bitfield, provenance fields, ranges and fill values are defined element-by-element in ICD <5.3.3> tables A–E and are the normative file-level structure (Annex F <5.5>d). Measurement variables: uint16 packed (scale_factor/add_offset) or float32 reflectance [0.0,1.0], _FillValue = profile sentinel, initial value n/a (computed); QA quality/mask/<band> uint16 bitmask, initial value 0; provenance root attrs (ids/versions/params/timestamp), no coefficients.

  • BandStack / BandImage (IF-CORE-01) — the pure-core payload of <5.4.1>: data float32 (working) / uint16 (DN), dims (line|y, detector|x), range [0,2^B−1] or [0,1]; qa uint16 initial 0; name, geom. This is deliberately CPM-free so Cores are testable off-platform (REQ-D-03, REQ-PORT-03).

  • Profile (IF-SVC-03) — the typed Profile mirrors the ICD <5.3.6> schema; the JSON-Schema file is the element-level definition (field ids = dotted paths, ICD <5.1>b).

Interface-map notes. The level-breakpoint EOProduct (IF-PROD-03/04/05, DPM-BKP-*) is the dominant internal interface and the point of optional Zarr persistence (resume semantics, DPM <9>). The Core↔Wrapper interface (IF-CORE-01) is uniform across PUs, which is what lets a new stage or a new profile slot in without touching the others (REQ-M-04, REQ-D-07). The cross-cutting service interfaces (IF-SVC-*) are uniform across PUs (the six wrapper steps of <5.4.1>).


<6> Requirements to design components traceability#

Per Annex F <6>a this clause gives the forward (requirement→component) and backward (component→requirement) traceability. The authoritative, tool-maintained matrix — including the downward trace to unit/integration test cases and the upward link to DPM DPM-M-* and ATBD ALG-* — is RD-11; per Annex F <6>b this clause is its design-level summary and the DJF reference.

<6.1> Forward trace — SRS REQ-* → design component (+ DPM/ATBD)#

SRS requirement group

Design component(s)

DPM / ATBD

REQ-F-L0-01..05

C-PU-L0 (+ C-COM-PRODUCT, C-COM-PROFILE, C-COM-ADF, C-COM-QAFLAG)

DPM-M-L0 / ALG-L0-*

REQ-F-RAD-01..05

C-PU-RAD (+ C-COM-ADF, C-COM-QAFLAG)

DPM-M-RAD / ALG-RAD-*

REQ-F-ENH-01..03

C-PU-ENH (+ C-PU-QA)

DPM-M-ENH / ALG-ENH-*

REQ-F-TOA-01..03

C-PU-TOA (+ C-COM-ADF, C-COM-PRODUCT, C-COM-PROV)

DPM-M-TOA / ALG-TOA-*

REQ-F-COR-01..03

C-PU-COR (+ C-COM-QAFLAG, fail-stop via C-COM-ORC)

DPM-M-COR / ALG-COR-*

REQ-F-GEO-01..04

C-PU-GEO (+ C-COM-ADF, C-COM-PRODUCT)

DPM-M-GEO / ALG-GEO-*

REQ-F-PAN-01/02

C-PU-PAN (+ C-PU-QA)

DPM-M-PAN / ALG-PAN-*

REQ-F-ATM-01..04

C-PU-ATM (+ C-COM-ADF, C-COM-PRODUCT)

DPM-M-ATM / ALG-ATM-*

REQ-F-QA-01/02

C-PU-QA, C-COM-QAFLAG

DPM-M-QA / ALG-QA-*

REQ-F-PRD-01/02

C-COM-PRODUCT, C-COM-PROV

DPM-M-PRD

REQ-F-ORC-01/02

C-COM-ORC, C-COM-CHUNK

DPM-M-PRD / DPM-BKP-*

REQ-F-DEP-01/02

C-COM-ORC (fail-stop), all Cores (determinism, seeds; REQ-D-05)

DPM-M-PRD / ATBD <6>

REQ-P-01..05

C-PU-RAD/TOA (RAD_ACC), C-PU-COR/GEO (GEO_CE90/BAND_COREG), C-PU-ATM (BOA_ACC), C-COM-CHUNK (THRU/MEM)

DPM <7.4>

REQ-I-01..07

C-COM-CLI, C-COM-ORC, C-COM-IO, C-COM-PRODUCT (ICD-bound)

ICD-IF-*

REQ-O-01..04

C-COM-CLI, C-COM-ORC (logs/report/status/modes)

ICD-IF-DIAG

REQ-R-01..05

architecture-wide (pure Python/CPU; C-COM-CHUNK sizing; REQ-R-05 N/A)

REQ-D-01..09

the architecture itself (PU pattern, profile layer, reuse, numerics, Zarr) + the typed exception/seed design (<5.4.1>)

REQ-S-01..05

C-COM-ADF (private by URI), C-COM-PROV (no calib in output), C-COM-IO

ICD-IF-ADF/OUT

REQ-PORT-01..03

C-COM-IO, Core/Wrapper split, C-COM-CHUNK (local-FS path)

ICD-IF-TRIG-03

REQ-AD-01..05

C-SENSORS, C-COM-PROFILE, C-COM-CONFIG

ICD-IF-PROF

REQ-DAT-01..03

C-COM-PRODUCT, C-COM-ADF, C-COM-PROFILE

ICD <5.3>

REQ-Q/REL/M/SAF/DEL/HF-*

toolchain + C-COM-PROFILE/PROV/QAFLAG + C-SENSORS + <4.5>/<4.7>

<6.2> Backward trace — component → SRS REQ-*#

The backward trace is the inverse of the <5.3> allocation table (each component’s Allocated REQ-* column) and of <6.1>; it is consolidated and kept current in RD-11. Every design component traces to at least one REQ-* (no orphan components), every REQ-* is allocated to at least one component (no unimplemented requirement), and every component additionally links to its DPM module / ATBD algorithm (the DPM/ATBD column of <5.3> and <6.1>). The closure to unit/integration test cases is held in RD-11 and exercised per the V&V plan (RD-8) and the SRS/ICD validation matrices.

<6.3> Measures for critical software components (Annex F <6>c)#

msi-processor is Category C (degraded/incorrect data only; reprocessable; SDP §5.6). The residual hazard class is product-data integrity (REQ-SAF-01). The design measures, per ECSS-Q-ST-80 6.2.2.4 (minimising critical components), now realised at element level:

  • No hard-coded instrument constants in the core — all externalised to the profile/ADFs (REQ-D-04, <4.1>c, <5.4.1>/<5.4.12>); the Cores take only arrays + a typed parameter object, so a calibration error is a data fix, not a code fix.

  • Pure-core isolation behind a tested interface — the core/unit split and the CPM-free BandStack contract (IF-CORE-01) make numerical kernels unit-verifiable in isolation and off-platform (REQ-D-03), with per-stage tolerances documented in the DPM/ATBD (REQ-D-05).

  • Bounded cyclomatic complexity (xenon thresholds) per core/unit module; the thin-wrapper template keeps the CPM-facing code trivial (REQ-D-04, REQ-Q-04).

  • Typed fault containment — the MsiProcessorError hierarchy (<5.4.1>) makes every failure a typed, flagged, reported event at the PU boundary; no silent cross-DAG propagation.

  • Determinism/reproducibility for a fixed input/ADF/profile/processor quadruple, including profile-fixed RANSAC seeds (REQ-F-DEP-02, REQ-D-05).

  • Fail-stop + QA flags + provenance as the integrity triad (REQ-F-DEP-01, REQ-F-QA-02, REQ-F-PRD-02); no partial product is ever published as complete.

No software-criticality-driven special design measures beyond the above (e.g. redundancy, watchdog, N-version) are levied at Category C; this is recorded to close Annex F <6>c. The detailed justification is carried in the DJF.


End of SDD (detailed / CDR issue). Authored per ECSS-E-ST-40C Rev.1 Annex F. This issue baselines the element-level per-component design (<5.4>) and internal-interface data (<5.5>) at CDR, superseding the PDR architectural issue. Requirements are in the SRS (RD-4); algorithm basis in the DPM (RD-6) / ATBD (RD-7); concrete interfaces in the ICD (RD-5); reuse heritage in the SRF (RD-10); the maintained traceability matrix is RD-11. Code internals marked [impl] are completed in implementation (SDP WP-5), which starts only after CDR.