---
title: "Sentinel-2 MSI L1B→L1A→L0plus→Synthetic L0 Reverse Reconstruction — Algorithm Theoretical Basis Document"
document_number: "S2MSI-E2ES-ATBD-0001"
version: "1.0"
date: "2026-06-30"
status: issued
confidentiality: Internal
---

# Algorithm Theoretical Basis Document (ATBD)

> **Issued.** This ATBD defines the algorithm basis by which `s2_msi_raw_generator` runs an
> Sentinel-2B **L1B** backwards through the **exact inverse of the operational L0→L1B radiometric
> chain** (invert offset, relative-response/PRNU G⁻¹, on-board equalization, dark, un-bin, SWIR
> re-stage, defective, crosstalk) to reconstruct **L1A → L0plus (CCSDS-122 ISP) → Synthetic L0**.
> **MTF-deconvolution is OFF**, so PSF and noise are **NOT** re-applied. Success is the reconstructed
> L0 measured against the **reference ESA L0 `img`** (10/20 m bands ≤~4 DN). Operational numerical values are
> populated: per-band gains/TDI/timing from products, the SRF spectral characterisation, and the
> **operational S2A GIPP** (per-pixel dark + relative response), with **CCSDS-122 lossless** codec for
> the L0plus. The calibration sub-set derives the coefficients from CSM-diffuser + dark acquisitions
> to build a **cal-DB** (inverse-crime cure). Implemented from the public L1 ATBD + GIPP data only.

---

# 1. INTRODUCTION

## 1.1 Project description

`s2_msi_raw_generator` (a **reverse reconstructor**) **inverts** the operational L0→L1B radiometric
chain of the `msi-processor` (a generic high-resolution push-broom MSI processor, EOPF CPM 2.8.1,
L0c→L2A, 8 units, CI-green). Where the processor *applies* the corrections that carry L0 up to L1B
(radiometric calibration, PSF deconvolution, co-registration, orthorectification, atmospheric
correction), this tool runs the **exact inverse** of each radiometric correction: it takes a
Sentinel-2 **L1B** product (already in per-detector sensor geometry) and **reconstructs** its
**L1A → L0plus → Synthetic L0** products (focal-plane digital numbers, 12 staggered detectors × 13 bands).
Because L1B is already sensor-geometry, the chain is **radiometric-only** — there is no geometry
inversion (orthorectification undo) to perform (Issue #17). An L1C entry + geometry-reverse module was
considered and **cancelled**: with an L1B entry there is nothing to de-orthorectify.

The reconstruction is validated **end-to-end against ground truth**: the Synthetic L0 DN is
compared with the **reference ESA L0 `img`** (10/20 m bands ≤~4 DN). As a supporting check, the L0plus
CCSDS-122 codec round-trip is bit-exact — `decode(L0plus) == L1A`. Implemented from the public
L1 ATBD + operational GIPP data only — no external processor.

## 1.2 Purpose of document

Present the L1B→L1A→L0plus→Synthetic L0 reverse-reconstruction data flow and the physical/mathematical
treatment at each stage — reconstructing the L1A/L0plus/Synthetic L0 products by **inverting** each operational
forward correction, with explicit traceability to the `msi-processor` forward function each reverse
step conjugates.

## 1.3 Scope

Algorithm-theoretical basis only; software design, ICD, and V&V plan
are separate DRDs. Reverse entry level is **L1A/L1B at-sensor radiance** (Issue #17); the L1C-entry
+ geometry-reverse module is **cancelled** (not applicable to an L1A/L1B entry).

## 1.4 References

### 1.4.1 Applicable documents
| | |
|---|---|
| AD 1 | `e2es-coupling-decision.md` — org/interface ADR (two groups; processor pinned wheel; sensor-model ADF bridge) |
| AD 2 | `msi-processor` ICD `<5.3.x>` (Synthetic L0 RAW container, processor-owned) |
| AD 3 | `msi-processor` ATBD `<5.7>` (forward algorithm definitions, the conjugates) |
| AD 4 | ECSS-E-ST-40C; ECSS-E-ST-10-02C (Verification) |

### 1.4.2 Reference documents
| | |
|---|---|
| RD 2 | ESA Sentinel-2 Spectral Response Functions (S2-SRF), doc COPE-GSEG-EOPG-TN-15-0007 v4.0 (2024) |
| RD 3 | SentiWiki Sentinel-2 MSI — instrument/mission parameters |
| RD 4 | PyRawS — Sentinel-2 raw (L0-like) granule structure & per-detector layout |
| RD 5 | S2 datastrip metadata — `SOLAR_IRRADIANCE` (ESUN) per band |
| RD 6 | Sentinel-2 Products Specification Document (PSD) — L1C/Synthetic L0 format |

## 1.5 Glossary

### 1.5.1 Acronyms
ADF (Auxiliary Data File), BOA (Bottom Of Atmosphere), DN (Digital Number), DSNU (Dark Signal
Non-Uniformity), E2ES (End-to-End performance Simulator), ESUN (mean exo-atmospheric solar
irradiance), GSD (Ground Sampling Distance), MSI (MultiSpectral Instrument), MTF (Modulation
Transfer Function), NUC (Non-Uniformity Correction), PRNU (Photo-Response Non-Uniformity),
PSF (Point Spread Function), RT (Radiative Transfer), SRF (Spectral Response Function),
TOA (Top Of Atmosphere), TDI (Time-Delay Integration), CSM (Calibration & Shutter
Mechanism), GRI (Global Reference Image), SSO (Sun-Synchronous Orbit), MGRS (Military Grid
Reference System). `[extend as needed]`

### 1.5.2 Definitions
- **Reverse step (R#):** an operation that inverts one `msi-processor` forward correction.
- **Conjugate:** the processor forward function a reverse step inverts.
- **Invert (exact inverse):** apply the analytic inverse of the operational forward correction to
  reconstruct the down-level (L1A/L0plus/Synthetic L0) product from the level above.

### 1.5.3 Definition of quantities
| Symbol | Name | Units |
|---|---|---|
| L | at-sensor band radiance (**L1B input**) | W·m⁻²·sr⁻¹·µm⁻¹ |
| DN | digital number (Synthetic L0 RAW output) | none, [0, 2¹²−1] |
| g_phys | per-band physical gain (DN↔radiance, value Annex A.11) | — |
| g_nuc, o_nuc, k_dark | per-detector PRNU gain/offset, dark offset | — |
| PSF_b | per-band point spread function kernel (DC=1) | none |
| ρ_TOA | TOA reflectance (*cancelled L1C-entry module — unused*) | none, [0,1] |
| ESUN_b, θ_s, d | solar irradiance (from SRF), solar zenith, Earth–Sun dist (*cancelled L1C-entry module — unused*) | mixed |

### 1.5.4 Note on coordinates
Three coordinate sets: **detector** (focal-plane pixel, the Synthetic L0 RAW frame),
**image/granule** (native acquisition grid), **map** (UTM/WGS-84, MGRS-tiled — *cancelled L1C-entry module*).
The v1 reverse operates entirely in **detector geometry** (input L1A/L1B is already there). Detector
array: **2592 px across-track
per module @ 10 m, 1296 px @ 20 m, ≈432 px @ 60 m**; 12 staggered modules per focal plane,
odd/even two-row stagger (Annex A.5). `[TBD: exact per-band sub-window origins / stagger
offsets — from ASGARD/GRI or PyRawS]`.

---

# 2. OVERVIEW

The Sentinel-2 MSI is a solar-reflective push-broom instrument: **13 spectral bands**
(B01 443 nm → B12 2202 nm; **no panchromatic band**), GSDs of **10/20/60 m**, two focal planes
(**VNIR** Si-CMOS + **SWIR** MCT) each with **12 staggered detector modules** across a 20.6° FOV,
on-board **sun-diffuser** (CSM) radiometric calibration, **12-bit** quantization (DN 0–4095).
**TDI:** the product `tdi_configuration_list` shows TDI **APPLIED on B03, B04, B11, B12** (2 VNIR +
2 SWIR); model as a per-band config. Full sourced values in **Annex A**.

**Reverse-reconstruction data-flow — S2 L1B → L1A → L0plus → Synthetic L0, radiometric-only:**
```mermaid
flowchart TD
    IN["S2B L1B<br/>(downlink DN, per-detector geometry)"]
    A["+ offset (R2PARA)"]
    B["G⁻¹ invert relative response / PRNU (R2EQOG)"]
    C["invert on-board equalization non-linearity (REOB2)"]
    D["+ L0 dark pedestal (L0_DARK_LSB × COEFF_D shape)"]
    E["×3 un-bin 60 m (B01/B09/B10)"]
    F["SWIR re-stage (RSWIR)"]
    G["re-stamp defective / re-insert blind (R2DEPI / BLINDP)"]
    H["+ crosstalk (RCRCO)"]
    QZ["quantize → uint16 (12-bit)"]
    L1A["L1A (reconstructed)"]
    ISP["CCSDS-122 lossless + ISP packetize"]
    L0P["L0plus (CCSDS ISP; decode == L1A, bit-exact)"]
    L0["L0 (156 frames + telemetry + STAC)"]
    OFF["MTF-deconvolution OFF ⇒ PSF &amp; noise NOT re-applied"]
    IN --> A --> B --> C --> D --> E --> F --> G --> H --> QZ --> L1A --> ISP --> Synthetic L0P --> Synthetic L0
    OFF -.-> B
```
No geometry inversion (L1B already sensor geometry).

---

# 3. DATA PRODUCTS

| Role | Short name | Description |
|---|---|---|
| **Input** | S2 **L1A/L1B** | At-sensor **radiance** (L1B float32; L1A rawer), **per-detector** geometry (`measurements/d{DD}/b{BB}/img`, e.g. [9216,2552] @10 m), 13 bands. Already sensor-geometry ⇒ **no geometry inversion** (Issue #17). |
| **Output** | Synthetic L0 RAW | Focal-plane uint16 DN per detector, 12 detectors × 13 bands, native geometry, quality annotations, STAC metadata. |

Output container (EOPF L0 Zarr = the normative **ICD-IF-L0**; Annex A.9): `measurements/d{DD}/b{BB}/band{BB}`
uint16 DN (156 arrays); `conditions/anc_data/s{APID}/isp` (CCSDS ISP/SAD telemetry); `quality/d{DD}/b{BB}/mask`
uint8; root STAC + sensor config (`tdi_configuration_list`, `spectral_band_info` incl. `physical_gains`,
`line_period`, `nuc_table_id`, `active_detectors_list`).

---

# 4. REVERSE PROCESSING FLOW

The processor's operational L0→L1B forward chain, inverted. Two sub-flows:

**Main reverse-reconstruction chain (`reverse_l1b_to_l0`, §5.R):** the S2 L1B is run backwards
through the inverse of the full operational L0→L1B radiometric chain, in reverse order —
`+offset → G⁻¹ relative-response → invert on-board-eq → +dark → un-bin(60 m) → SWIR re-stage →
defective/blind → crosstalk → quantize → CCSDS-122 + ISP → Synthetic L0` — reconstructing L1A → L0plus → Synthetic L0.
MTF-deconvolution is OFF, so **PSF re-blur and noise are NOT re-applied**. The step-level physics is
detailed in §5; §5.R is the authoritative S2 L1B flow.

**Calibration sub-set (`s2_msi_raw_generator/calibration.py`):** the S2 **two-reference** radiometric
calibration in the reflective domain, which derives the **cal-DB**. The high-signal reference is the
on-board **CSM sun-diffuser** (uniform full-field), the zero reference is a **dark** (CSM closed /
night). The sub-set synthesises both L0 acquisitions from the *true* ADF, then **derives** the
coefficients back (L1 ATBD §4.1.1.2.2):
$D(j) = \langle X_\mathrm{dark} \rangle_i$, $g(j) = A \cdot \langle L_\mathrm{diff} \rangle / \langle X_\mathrm{diff} - D \rangle_i$ with $\langle g(j) \rangle_j = 1$ → fixes `A`. The processor then
uses the **derived** coefficients (`estimated_adf`), not the truth used to build the references —
closing the loop **breaks inverse crime** (verified: derived dark recovers truth to <0.05 DN,
relative response correlation >0.99, `A≈cal_gain`; the small residual is the calibration
uncertainty). `ADF_REQOG`.

---

# 5. PROCESSING-BLOCK DESCRIPTIONS (the operational corrections being inverted)

Each block below states one operational L0→L1B forward correction, its ADF, and the `msi-processor`
conjugate. The reverse reconstructor applies the **analytic inverse** of each. The **authoritative
S2 L1B flow — how these inverses are actually composed on a S2B L1B — is §5.R**; the entry is
the downlink **DN** domain (a S2 L1B is already digital counts), not synthetic radiance. Real
per-band values in **Annex A.6/A.11**.

## 5.S1 Radiometric gain/offset (the DN-domain equation inverted)

**Model.** The official L1 ATBD raw equation (S2-PDGS-MPC-ATBD-L1 §4.1.1) is

$$X_k = A_k \cdot G_k(j,L) \cdot L_k + D_k$$

whose terms the reverse chain **inverts** in the DN domain:

- the absolute-calibration term $A$ (`Band.cal_gain`);
- the relative sensitivity $G$ (S7, inverted as $G^{-1}$);
- the dark signal $D$ (S11).

**Domain note.** A S2 L1B is already digital counts, so the reverse chain does **not** perform a synthetic
radiance→DN entry; it enters in the downlink DN domain and inverts the operational gain/offset directly
(see §5.R). The product's `physical_gains` (Annex A.11) are carried for metadata; the DN-domain
inversion of offset and relative response is applied per §5.S4/§5.S7.

**ADF:** `ADF_RABCA`. **Conjugate:** `toa.dn_to_radiance` ($L = \mathrm{DN}/A$).

## 5.S3 Undo framing & round/clamp

**Forward.** Extend to a continuous detector strip; restore sub-pixel precision.

**ADF:** `ADF_PRDLO`, `ADF_RPARA`. **Conjugate:** `l0_decode` framing.

## 5.S4 Undo radiometric offset

**Forward.** `DN −= offset`, with `radio_add_offset = −100` (L1B; −1000 for L1C, PB04.00).

**ADF:** `ADF_RPARA`. **Conjugate:** —

## 5.S5 Undo 60 m binning (B01/B09/B10)

**Forward.** De-bin to detector level (the 20 m → 60 m forward bin, reversed).

**ADF:** `ADF_RBINN`. **Conjugate:** `georeference.resample_to_grid`.

## 5.S7 Invert relative response (G⁻¹)

**Reverse.** Invert the on-ground equalization $Y = G(Z)$ — VNIR cubic $A\,Z^3 + B\,Z^2 + C\,Z$ /
SWIR bilinear (knee at `Zs`) — applying the analytic inverse $G^{-1}$ per pixel to reconstruct the
un-equalized DN.

**Real values.** The per-pixel gains come straight from the **operational S2A GIPP `R2EQOG`**
(`COEFF_A/B/C` cubic / `COEFF_A1/A2/Zs` bilinear; $C \approx 1.0$–$1.2$ dominant), parsed by
`s2_msi_raw_generator.gipp` and applied via the analytic inverse $G^{-1}$ in
`forward_radiometric_atbd.inverse_equalize` (`BandADF.from_gipp`).

**ADF:** `ADF_REQOG`. **Conjugate:** `radiometric.apply_nuc`.

## 5.S8 Re-insert SWIR arrangement (B10/B11/B12)

**Forward.** Restore the staggered SWIR readout layout (the TDI "rearrangement"); TDI is **APPLIED on
B03, B04, B11, B12** (`tdi_configuration_list`).

**Method.** PyRawS `shift_lut.csv` deterministic per-(satellite, detector, band-pair) shifts (Annex A.9).

**ADF:** `ADF_RSWIR`.

## 5.S9 Re-apply crosstalk

**Forward.** Inject optical + electrical channel crosstalk,

$$\mathrm{DN}_i \mathrel{+}= \sum_j \mathrm{xtalk}[i,j] \cdot \mathrm{DN}_j$$

of magnitude <0.5 % channel-to-channel. The matrix is the GIPP `R2CRCO` (per-band
OPTICAL+ELECTRICAL row; ≈0 for S2A → identity).

**ADF:** `ADF_RCRCO`. **Conjugate:** *none* (S2-specific; a named residual).

## 5.S10 Re-insert blind/defective pixels

**Forward.** Insert masked blind columns and inject defective pixels (**3 in B11, 1 in B12**, per S2C Cal/Val).

**ADF:** `ADF_BLIND` (blind), `ADF_RDEPI` (defective). **Conjugate:** `radiometric.replace_bad_pixels`.

## 5.S11 Reconstruct the dark pedestal

**Reverse.** `DN += dark[pixel]` — reconstruct the L0 dark pedestal that the operational chain
subtracted (see the §5.R downlink-dark domain note).

**Real values.** The **per-pixel** dark signal $D(j)$ comes from the operational S2A GIPP
**`R2EQOG` `COEFF_D`** (`s2_msi_raw_generator.gipp`, `BandADF.from_gipp`) — mean 440–522 LSB per band,
matching the DQR (OMPC.CS.DQR.01.02-2023) range but now resolved per pixel. Fallback when no GIPP:
`DARK_PEDESTAL_LSB` 440–520 + per-pixel DSNU $< 0.5/1.0$ LSB (`Band.dark_dsnu`).

**ADF:** `ADF_REOB2` / `ADF_REQOG`.

## 5.S12 Re-apply onboard equalization

**Forward.** Invert the R2EQOG equalization — multiplicative (cubic VNIR $Z = \sum_n G_n \cdot Y^n$ /
bilinear SWIR) on the dark-subtracted signal (Clerc et al. 2026, S2C cal/val; `equalization_mode = true`,
`nuc_table_id = 3`). Linearized here as $\mathrm{DN}_\mathrm{raw} = \mathrm{DN}_\mathrm{eq}/\mathrm{gain}_\mathrm{ob}$,
with the **per-detector gain stability 0.05 % 1σ** (paper Table 3, Ra factor; `sensor.EQ_GAIN_STD`) and
**no offset** (the dark is the S11 pedestal).

**ADF:** `ADF_REOB2`. **Conjugate:** `radiometric.estimate_nuc`.

## 5.S14 Quantize

**Forward.** `clip(round(DN), 0, 4095)` → uint16 (12-bit at sensor); no-data 0, saturated 65535.

**ADF:** `ADF_CONVE`. **Conjugate:** `radiometric.flag_saturation`.

## 5.S15 Generate ISP packets & telemetry

**Forward.** Two sub-steps mirroring the onboard chain:

1. **Onboard image compression** — the Sentinel-2 compresses MSI video data onboard with
   the proprietary **MRCPB** wavelet scheme (bit-plane coding, per-band tuned rates ≈ 2.4–2.97,
   CoReCi ASIC); a CCSDS-compression ASIC is the documented *alternative*. This generator
   implements that alternative: **CCSDS 122.0-B, lossless profile**
   (`s2_msi_raw_generator/ccsds122.py`) — 3-level integer DWT 9/7-M (§3.3 lifting with
   `floor(·+1/2)` rounding, whole-sample symmetric extension), 8×8 block/family structure and
   16-block gaggles (§4.1), self-describing segment headers carrying the Part-1A/3/4 content
   (§4.2), DC and per-block `BitDepthAC` DPCM + per-gaggle Rice coding (§4.3/§4.4). *Documented
   divergence:* the §4.5 AC stages keep their semantics and scan order but the per-stage bits are
   packed raw instead of §4.5.3 word-mapped VLCs — bit-exact lossless, structurally 122-shaped,
   not interoperable with reference decoders (the matching decoder ships in the module). Segments
   default to one block row = **8 image lines**, giving line-accurate packet datation.
2. **Packetization** — the compressed stream is carried in CCSDS space packets; per-line
   timestamps from `line_period = 1.5658736 ms`; SAD packets per APID.

Note the level split of the S2 L1B chain: **L0plus stores the compressed, annotated ISPs; the ground
L1A step decompresses** (SentiWiki S2 Products). Choosing the *lossless* profile (operational MRCPB is
lossy) keeps the L0plus codec round-trip **bit-exact** — `decode(L0plus) == L1A` — a supporting check
on the reconstruction (distinct from the end-to-end Synthetic L0 vs reference ESA L0 validation, §5.R). The L0plus
achieves a lossless compression ratio of **~3.66×** with bit-exact ground decode.

**Output.** The 156-frame L0 (Annex A.9).

**ADF:** `ADF_SADMP`, `ADF_DATAT`. **Conjugate:** ground decompression + `l0_decode`.

**Calibration sub-set (inverse-crime cure — implemented `s2_msi_raw_generator/calibration.py`):** synthetic
CSM sun-diffuser + dark acquisitions → derive `D`, `g`, `A` (L1 ATBD §4.1.1.2.2) → *estimated* ADF
(`estimated_adf`, the **cal-DB**) handed to the processor, not the truth used to build the references.

**Cancelled L1C-entry module (Issue #17):** an L1C entry would have required prepending
de-orthorectification (ground→detector via the S2 viewing model — **ASGARD**,
`ADF_VDIRP/SPAMO/GPARA/RESAM/TILEP/DEM`) + reflectance→radiance. This module is **dropped**: the
adopted L1A/L1B entry is already in native detector geometry, so there is nothing to de-orthorectify.
(L1C orthorectification destroys the native detector alignment the radiometric reverse relies on —
the very reason an L1C entry was rejected; PRNU paper.)

---

# 5.R REAL L1B → L1A → L0plus → Synthetic L0 FULL REVERSE — AUTHORITATIVE FLOW (`reverse_l1b_to_l0`)

**This is the central reverse-reconstruction flow; §5.S1–S14 give the per-step physics it composes.**
A S2B EOPF **L1B is already digital counts**, so the reverse enters in the **downlink DN domain**
and inverts the *full* operational L0→L1B radiometric chain (EOPF `AllRadiometricCorrectionL1B` — every
`feature_flag_* = True` **except** deconvolution/denoising) in reverse order to reconstruct
**L1A → L0plus → Synthetic L0**:

`+offset (R2PARA) → G⁻¹ relative response (R2EQOG) → on-board-eq non-linearity (REOB2) → +L0 dark
(L0_DARK_LSB × R2EQOG COEFF_D shape) → ×3 un-bin (60 m) → SWIR re-arrangement (RSWIR) → re-stamp
defective (R2DEPI)`. Crosstalk (RCRCO) is added back phase-level across same-resolution bands; blind
columns are re-inserted from BLINDP; CCSDS-122 + ISP then package the L0 (S15).

**MTF restoration / deconvolution (forward step 8) is deliberately skipped** — it is off in the
operational chain (`feature_flag_with_deconvolution = False`, ADF_RPARA `restoration`; SentiWiki:
*"restoration disabled by default — instrument MTF already high"*). L1B therefore still carries the full
instrument PSF, so **S6 PSF re-blur and S13 noise are NOT re-applied** — they would double-count and are
non-invertible anyway (deconvolution is lossy; the exact noise realization is unrecoverable).

**Domain note.** The added dark is the *downlink* L0 dark (`L0_DARK_LSB` ≈ 51), not the raw-detector
COEFF_D (≈ 440): REOB2 has near-unity slope (S2B `a1 ≈ 1.005, a2 ≈ 0.995`) and its dark `d ≈ 455`
cancels COEFF_D, so REOB2 contributes only its `a1/a2` non-linearity in the downlink domain.

**ADF sources** (EOPF json, auto-found next to `ADF_REQOG`): `ADF_RSWIR`
(`swir_band_list/swir_band/detector` per-column ±1 shift + B10 3-tap kernel), `ADF_REOB2`
(`new_table/coeff_{a1,a2,zs,d}`), `ADF_RCRCO` (13×13 optical+electrical). Parsers: `gipp.read_rswir_eopf`
/ `read_reob2_eopf` / `read_rcrco_eopf`; ops: `forward_radiometric_atbd.restage_swir_lines` /
`reapply_onboard_eq`.

**Validation** (2024-04-08 S2B PPB, d05, CCSDS round-trip): the **Synthetic L0** vs the
**reference ESA L0 `img`** RMSE **≤ 3 DN on all 13 bands** (B11/B12 fixed by S8 from ~50 → ~3 DN;
success criterion ≤~4 DN on the 10/20 m bands), framing-aligned via ADF_PRDLO
with zero line-drift — see the [S2 L1B-E2E run report](../vv/s2_l1b_e2e_run_report.md) and the
[DPM parameter/data list](../dpm/parameters-data-list.md).

---

# 6. SENSOR-MODEL ADF v0 (the conjugate bridge)

Jointly change-controlled with the processor (per AD 1). **Conjugate subset** (both halves read):
- `spectral`: per-band S2 SRF samples (central λ Annex A.1); **ESUN_b derived from the same SRF**,
  **matched to the product's satellite** (S2A/B/C; Risk 2) — per-unit centre/bandwidth/equivalent
  wavelength from the SRF doc are in `sensor.py`.
- `radiometric`: per-band gain — **`physical_gains` from the product metadata** (Annex A.11,
  no credentialed ADF needed); `radio_add_offset = −100` (L1B). Source ADF: `ADF_RABCA`.
- `nuc`/`dark`/`badpixel`: per-detector PRNU (**1D per-detector model**; residuals from Zenodo
  `records/18433006`), DSNU + dark (`ADF_REOB2`, nighttime-ocean), defect/blind (`ADF_BLIND`/`ADF_RDEPI`,
  3×B11 + 1×B12), crosstalk `ADF_RCRCO` (<0.5 %). `nuc_table_id = 3`, equalization on.
- `psf`: per-band, per-unit kernel (DC=1) — the **official ESA PSF matrices** (SentiWiki
  `S2{A,B,C}_PSF`, Annex A.4), integrated from the 33×33 oversampled matrix to the detector grid.
- `viewing_model`: focal length 600 mm / F4 (TMA, 150 mm pupil), pixel pitch 7.5/15 µm, 12-detector
  stagger, per-band GSD (Annex A.2), `line_period` 1.5658736 ms. *(Was only needed by the cancelled L1C-entry module.)*

**E2ES-only block** (processor never reads): noise model (`a,b` for $\sigma=\sqrt{a+b\cdot\mathrm{DN}}$, `ADF_RNOMO`),
crosstalk kernel, temporal gain drift (VNIR 0.1–0.35 %/months, SWIR faster).

**ADF access — all now.** Every radiometric ADF the chain needs is ESA-sourced: gain/TDI/timing/offset
from the products; **PSF** = the official ESA matrices (SentiWiki); **spectral** = the **SRF**
(COPE-GSEG-EOPG-TN-15-0007); **noise** = the per-band α, β from the L1A product
(`quality_indicators_info/.../noise_model`), $\sigma=\sqrt{\alpha^2+\beta\cdot\mathrm{DN}}$ (S2-RUT, reproduces SNR@Lref). The
previously-modelled **per-pixel PRNU + dark** are now the **operational S2A GIPP**: `R2EQOG`
carries, per detector and per across-track pixel, the dark signal `COEFF_D` (≈440–522 LSB, matching
the DQR) and the relative-response gains (VNIR cubic `A/B/C`, SWIR bilinear `A1/A2/Zs`); `R2DEPI` the
defective/blind columns; `R2PARA` the −100/−1000 offsets; `R2CRCO`≈0. These GIPP **data** files are
parsed by `s2_msi_raw_generator.gipp` (an original reader) into per-pixel arrays (`BandADF.from_gipp`).

---

# 7. ERROR ANALYSIS

Per-stage error-budget table, **reflective-domain terms**. Populate numerically via sensitivity sweeps.

| Step | Dominant error term | Driver | Budget |
|---|---|---|---|
| S1/S4 | gain / offset inversion residual | radiometric | abs. <5 % (goal 3 %); per-unit SRF up to ~15 % if mismatched |
| S7/S12 | relative-response G⁻¹ + on-board-eq inversion residual | radiometric | inter-band rel. 3 %; linearity 1 % |
| S5 | un-bin (60 m) reconstruction residual | resample | sub-DN on uniform fields |
| S8 | SWIR re-stage mis-alignment | detector | B11/B12 dominant (fixed ~50 → ~3 DN) |
| S9 | crosstalk residual (no processor inverse) | detector | <0.5 % channel-to-channel |
| S14 | quantization | 12-bit (0–4095) | ≈ Lref/SNR LSB |
| end-to-end | **Synthetic L0 vs reference ESA L0 `img` RMSE** per band | composite | **≤~4 DN (10/20 m bands)**; multi-temporal rel. ≤1 % |

**Method note:** re-derive the dominant terms for the reflective domain. The end-to-end claim is
*absolute verification against ground truth* — the Synthetic L0 DN measured directly against the
reference ESA L0 `img`, not a self-consistency round-trip.

---

# 8. OPEN POINTS

- Reverse entry level — **RESOLVED: L1B** (Issue #17); the L1C-entry + geometry-reverse module is **cancelled**.
- L0 ICD: adopt the EOPF L0 Zarr structure (Annex A.9) as ICD-IF-Synthetic L0.
- SRF: **DONE** — per-unit band centre/bandwidth/equivalent wavelength from the official SRF
  doc (COPE-GSEG-EOPG-TN-15-0007) are in `sensor.py`.
- PRNU/dark per-pixel GIPP — **RESOLVED** (supersedes the early "credentialed GIPP" blocker #36):
  the operational `S2A_OPER_GIP_R2EQOG` (per-pixel dark `COEFF_D` + relative response) is read
  directly by `gipp.py` / `BandADF.from_gipp` and is publicly fetchable (bucket `GIP_*` TGZs are
  anonymous-GET; verified 2026-07-02). The reverse reconstruction runs on it and validates against the
  **reference ESA L0 `img`** at RMSE ≤~4 DN (10/20 m bands; see §5.R). Still open in the narrow sense: a
  **dark-calibration granule** (night-over-ocean) is not in this dataset — per-pixel dark
  therefore comes from the GIPP, not from a dark acquisition. L1B-derived PRNU remains available via
  the pipeline's `derive-adf` phase.

---

# Annex A — Sentinel-2 MSI parameter data list (sourced)

Externally sourced, verified values. Primary sources: **SentiWiki (Copernicus)**
`sentiwiki.copernicus.eu`, **ESA SP-1322/2**, **S2 L1 ATBD**, **eoPortal**, **S2 User Handbook**;
cross-checked vs the official S2-SRF table (COPE-GSEG-EOPG-TN-15-0007). See A.8 for provenance and
unverified/derived flags. Values to be migrated into the sensor-model ADF v0 (§6).

## A.1 Spectral bands — central wavelength & bandwidth (FWHM), S2A / S2B
(Central λ cross-verified to ~0.3 nm; bandwidths are S2-SRF-version-sensitive — cite a version.)

| Band | S2A λ (nm) | S2A BW (nm) | S2B λ (nm) | S2B BW (nm) | GSD |
|---|---|---|---|---|---|
| B01 | 442.7 | 21 | 442.2 | 21 | 60 m |
| B02 | 492.4 | 66 | 492.1 | 66 | 10 m |
| B03 | 559.8 | 36 | 558.9 | 36 | 10 m |
| B04 | 664.6 | 31 | 664.9 | 31 | 10 m |
| B05 | 704.1 | 15 | 703.8 | 16 | 20 m |
| B06 | 740.5 | 15 | 739.1 | 15 | 20 m |
| B07 | 782.8 | 20 | 779.7 | 20 | 20 m |
| B08 | 832.8 | 106 | 832.9 | 106 | 10 m |
| B8A | 864.7 | 21 | 864.0 | 22 | 20 m |
| B09 | 945.1 | 20 | 943.2 | 21 | 60 m |
| B10 | 1373.5 | 31 | 1376.9 | 30 | 60 m |
| B11 | 1613.7 | 91 | 1610.4 | 94 | 20 m |
| B12 | 2202.4 | 175 | 2185.7 | 185 | 20 m |

**No panchromatic band.** B08 bandwidth has the largest cross-source divergence (106 vs 115/118 nm).

## A.2 GSD per band
- **10 m:** B02, B03, B04, B08
- **20 m:** B05, B06, B07, B8A, B11, B12
- **60 m:** B01, B09, B10

## A.3 ESUN — extraterrestrial solar irradiance (W·m⁻²·µm⁻¹), Thuillier 2003
(= the `SOLAR_IRRADIANCE` stored per band in L1C metadata; S2A ≠ S2B because of distinct SRFs.)

| Band | ESUN S2A | ESUN S2B | Band | ESUN S2A | ESUN S2B |
|---|---|---|---|---|---|
| B01 | 1884.69 | 1874.30 | B08 | 1041.63 | 1041.28 |
| B02 | 1959.66 | 1959.75 | B8A | 955.32 | 953.93 |
| B03 | 1823.24 | 1824.93 | B09 | 812.92 | 817.58 |
| B04 | 1512.06 | 1512.79 | B10 | 367.15 | 365.41 |
| B05 | 1424.64 | 1425.78 | B11 | 245.59 | 247.08 |
| B06 | 1287.61 | 1291.13 | B12 | 85.25 | 87.75 |
| B07 | 1162.08 | 1175.57 | | | |

## A.4 Optics
- Telescope: **Three-Mirror Anastigmat (TMA)**, off-axis, SiC mirrors + structure. Mirror sizes
  M1 442×190, M2 147×118, M3 556×291 mm. Optics identical on S2A/S2B.
- Entrance pupil / aperture: **150 mm**. Focal length: **600 mm (0.60 m)** — now confirmed
  (Languille 2015 "focal length is 0.60 m"), F-number **F/4**. FOV **20.6°** across-track
  (→ 290 km swath @ 786 km); IFOV ≈ 21° × 3.5°.
- Distortion: line-of-sight of edge detector (#12) reaches **2.8° tilt** vs a distortion-free
  telescope; distortion pattern varies with across-track position.
- **MTF at Nyquist** (combined system spec — telescope + detector + smear): 10 m & 20 m →
  **>0.15 and <0.30**; 60 m → **<0.45**. (Phrasing discrepancy: SentiWiki/eoPortal place the
  20 m bands in the <0.45 bracket; the 10/20 m → 0.15–0.30 framing matches Drusch 2012.)
- **Official PSF matrices ARE published** (SentiWiki `S2{A,B,C}_PSF.zip`, packaged in
  `s2_msi_raw_generator/data/psf/`): per-band, per-unit **33×33** matrices, **oversampling 5**, centre at
  (17, 17), normalised ($\Sigma = 1$), for **L1B focal-plane geometry (after binning)**. Computed from
  measured Nyquist MTF (along-track + across-track), Gaussian-modelled — S2A/S2C from 2024, S2B from
  2023 — for all bands **except B10** (water-vapour, does not see the ground). Kept as **reference
  data** (`s2_msi_raw_generator/data/psf/`) for sensor characterisation; because MTF-deconvolution is
  OFF in the operational chain, the reverse reconstruction does **not** re-apply the PSF (no re-blur
  step — see §5.R).

## A.5 Detector / focal plane
- Two focal planes (dichroic split): **VNIR = monolithic Si CMOS (0.35 µm)** @ ~293 K, 10 bands;
  **SWIR = MCT/HgCdTe** hybridised on CMOS ROIC @ **195 ± 0.2 K**, 3 bands (B10, B11, B12).
  Per-band separation by stripe filters.
- **12 detector modules per focal plane**, two staggered rows across the 20.6° FOV.
- Pixel pitch: VNIR **7.5 µm** (10 m bands) / **15 µm** (20 m bands); SWIR **15 µm**. Total
  across-track px: **31,152 @ 10 m**, **15,576 @ 20 m**.
- **Per-detector RAW image shape [along-track × across-track]** (PyRawS `BANDS_RAW_SHAPE_DICT`):
  10 m bands B02/B03/B04/B08 = **2304 × 2592**; 20 m bands B05/B06/B07/B8A = **1152 × 1296**;
  60 m bands B01/B09 = **384 × 1296**; SWIR B11/B12 (20 m) = **1152 × 1296**; B10 (60 m) = **384 × 1296**.
- Inter-detector **across-track overlap ≈ 2 km** (120–200 px for 10 m bands).
- **Cross-detector parallax** (odd/even, same band): **0.022°–0.059°** → sub-km baseline,
  near-simultaneous views. *(The earlier "~46 km inter-detector" figure was ERRONEOUS — corrected.)*
  Cross-band parallax within a detector: B2–B9 = 0.018° (VNIR max), B10–B12 = 0.010° (SWIR max).
- Band-to-band along-track **time delay**: ~20 ms (B6–B11, min) to **2.6 s** (B2–B9, max); detector
  along-track footprint ≈ **34 km**; ground velocity ≈ **6700 m/s**; delay knowledge < 0.15 ms.
- **TDI — UNCERTAIN/conflicting in public sources:** the Martimort 2007 / eoPortal spec table lists
  a **~2-line TDI stage for BOTH VNIR and SWIR** (SWIR adds 2 lines for pixel deselection); one
  search result (SP-1322/2) cited per-band SWIR **B10=3, B11/B12=4 lines** — unresolved. Treat the
  TDI line count as a **configurable per-focal-plane ADF parameter** (default: 2-line stage; SWIR
  per-band counts `[TBD]`). TDI drives the **yaw-steering** requirement (ground velocity ⊥ arrays).
  S2 "SWIR rearrangement" (msi-processor `ADF_RSWIR`) realigns SWIR columns for this readout.

## A.6 Radiometry
- Quantization: **12-bit at acquisition (DN 0–4095)**, stored 16-bit unsigned. On-board wavelet
  compression (~450 Mbit/s); RAW = decompressed L0 + metadata.
- Absolute radiometric uncertainty **< 5 %** (goal 3 %); inter-band relative **3 %**;
  multi-temporal relative **1 %**; linearity **1 %** (residual after γ-correction ~0.4 %);
  channel-to-channel cross-talk **< 0.5 %**; diffuser non-uniformity **1 %**; stray-light bias
  **0.3 %** (VNIR) / **0.15 %** (SWIR); polarisation < 1 % VNIR (B1 = 1.2 %), ~1.7 % SWIR.
- On-board calibration: **CSM** full-field/full-pupil **sun diffuser** (700 × 250 mm²), ~monthly
  (over North Pole). **Dark signal**: updated ~every 2 weeks from night-over-ocean (CSM open),
  averaged ≥ **10.8 s**; stability VNIR < 1 DN, SWIR up to ~5 DN; DS uncertainty ~0.4 %.

**L1C DN ↔ reflectance scaling** [PROC; Processing Baseline **04.00**, from **25 Jan 2022**]:
- `QUANTIFICATION_VALUE` = **10000** (scale 1e-4); `RADIO_ADD_OFFSET` = **−1000 DN** (L1C; L1B = −100;
  L2A `BOA_ADD_OFFSET` = −1000), stored in `<Radiometric_Offset_List>`.
- **Inverse (user / reverse-entry):** `ρ = (DN + RADIO_ADD_OFFSET) / QUANTIFICATION_VALUE = (DN − 1000)/10000`.
- **Forward (product encode):** `DN = ρ·10000 − RADIO_ADD_OFFSET = ρ·10000 + 1000`.
- No-data DN = **0**; saturated DN = **65535**. Pre-PB04.00 products carry **no** offset.
- TOA-reflectance equation (PSD): $\rho = \pi \cdot C_N \cdot d^2 / (A \cdot E_\mathrm{SUN} \cdot \cos\theta_s)$, CN = equalised count after L1B
  offset, `A` = absolute calibration coefficient per band (in GIPP/ADF — **not published numerically**).

**Instrument noise model** (Gorroño & Gascon, S2-RUT): $\mathrm{Noise}(Z) = \sqrt{\alpha_Z^2 + \beta_Z \cdot Z}$, Z = equalised
signal; $\alpha_Z$ = std of dark-signal frames (read + dark + electronic floor), $\beta_Z$ = shot/Poisson
from sun-diffuser. Equivalent generic form $\sigma^2 = a \cdot \mathrm{DN} + b$. Components: dark + readout + electronic
+ shot. **Per-band α_Z/β_Z are NOT public** → fit to the SNR@Lref anchor + dark-noise floor.

**Reference radiance Lref (W·m⁻²·sr⁻¹·µm⁻¹) and required SNR @ Lref** (S2A/S2B spec, identical):

| Band | Lref | SNR | Band | Lref | SNR |
|---|---|---|---|---|---|
| B01 | 129.11 | 129 | B08 | 103.00 | 174 |
| B02 | 128.00 | 154 | B8A | 52.39 | 72 |
| B03 | 128.00 | 168 | B09 | 8.77 | 114 |
| B04 | 108.00 | 142 | B10 | 6.00 | 50 |
| B05 | 74.60 | 117 | B11 | 4.00 | 100 |
| B06 | 68.23 | 89 | B12 | 1.70 | 100 |
| B07 | 66.70 | 105 | | | |

## A.7 Geometry / orbit
- **Sun-synchronous**, near-polar; altitude **786 km** (range ~788–818 km over an orbit);
  semi-major axis **7164.26 km**; eccentricity **0.0011584**; inclination **98.62°** (mission value;
  OCD/Binet 2022 gives 98.49° — discrepancy, see A.10); arg. of perigee 90.74°; period **100.6 min**;
  **LTDN 10:30** (LTAN ≈ 22:30, derived/uncited); repeat **10 d**, **143 orbits/cycle**, 14.3
  orbits/day; revisit **5 d** at equator (2-sat, 180° phased); swath **290 km**; ground-track
  dead-band ±2 km.
- **Tiling:** UTM/WGS-84 MGRS; tile **110 × 110 km** on a 100 km grid step (~10 km overlap).
- **Geolocation:** req. without GCP **20 m (2σ)** (measured ~11–14.5 m); with GRI **12.5 m**
  (CE95 < 12.5 m; block 8 m CE95); multi-temporal registration ≤ 0.3 px (= 3 / 6 / 18 m for
  10/20/60 m); inter-band co-registration ≤ 0.30 coarser-pixel. **GRI** = global cloud-free
  mono-spectral **B04** L1B/C images (2015–2018, ~1000), geolocated by spatio-triangulation,
  uncertainty < 6 m; operational worldwide since Aug 2021.

## A.8 Viewing & datation (line-time) model
- **Viewing model** = datation (per-line start dates) + orbit + attitude + per-pixel viewing
  directions (spacecraft frame); geolocation = viewing-vector ∩ Earth model. COTS library **ASGARD**
  performs direct/inverse location (map ↔ detector), GCP-chip projection, footprint recompute.
- ADFs: **`ADF_VDIRP`** (per-pixel viewing directions), **`ADF_DATAT`** (datation), `ADF_VIEDI`,
  `ADF_SPAMO`, `ADF_G2PRA/G2PRE`. Per-pixel LOS for every band & detector lives in L1B "expertise".
- **Public per-detector viewing grids:** L1C `MTD_TL.xml` → `Viewing_Incidence_Angles_Grids` give
  viewing zenith + azimuth **per band, per detector** on a **5 km grid (23×23 nodes, 5000 m)**;
  overlap nodes carry two values. In-orbit LOS calibration < **0.1 px**; main LOS geocentric;
  **yaw-steering** keeps ground velocity ⊥ detector arrays.
- **De-orthorectify (cancelled L1C-entry module — for reference only):** ortho pixel → ground (ITRF/WGS84)
  → inverse location (ASGARD) → detector (col, line). Push-broom: **line index is linearly related to
  acquisition date**. Not implemented: the adopted L1A/L1B entry is already in detector geometry.
- **Datation / line-time:** nadir ground velocity ≈ **6700 m/s**; line period (DERIVED = GSD/Vground)
  ≈ **1.49 ms** (10 m) / **2.99 ms** (20 m) / **8.96 ms** (60 m); relative dating < 0.15 ms; absolute
  line-date < 2 ms; ephemeris sampling 1 Hz; solar-array attitude perturbation sine at 0.032 Hz
  (~2 ms peak). Inter-band time delays (Binet 2022 Tbl 2): B2–B4 = 1.005 s, B3–B2 = 0.527 s,
  B6–B11 ≈ 20 ms (min), B2–B9 = 2.586 s (max).

## A.9 L0 / RAW format & PyRawS coregistration
- **L0** = downlinked, on-board-compressed; **RAW** = decompressed L0 + metadata.
- Granule = **13 bands × 12 detectors = 156** per-detector focal-plane images, raw geometry, **NOT
  co-registered**, ~3.6 s acquisition each.
- **Focal-plane along-track band order:** B02, B08, B03, B10, B04, B05, B11, B06, B07, B8A, B12, B01,
  B09 — filter sort order **inverted between odd/even detectors**.
- **PyRawS deterministic coregistration** (the known-shift fork): `pyraws/database/shift_lut.csv`,
  keyed by (satellite S2A/S2B, registration_mode up/down-sampling, detector 1–12); each cell =
  **[along-track, across-track] pixel shift** for a band-pair vs a reference band. Along-track up to
  ~240 px (e.g. S2A det-1 B05↔ref = [239, −2]), across-track 0–34 px; **sign flips with detector
  parity** (odd +, even −). Directly usable for **S8 SWIR de-arrangement** (the cancelled L1C de-coregistration would also have used it; no SIFT).
- L1B = radiometrically corrected, per-detector geometry (retains parallax + overlap);
  L1C = orthorectified, band-co-registered, UTM tiles.

## A.10 Provenance & unverified/derived
- **Verified (multi-source):** wavelengths; GSD; orbit; bit depth; radiometric-accuracy specs;
  Lref/SNR; detector tech; MTF@Nyquist; TMA / 150 mm / **600 mm / F4** (Languille 2015); mirror sizes;
  **per-detector raw px shapes** (PyRawS); focal-plane band order; **PyRawS shift LUT**; **DN scaling
  PB04.00** (QUANT 10000, offset −1000); **noise-model form**; ASGARD / viewing grids; line period.
- **Single source:** ESUN (SentiWiki Tbl 1 = L1C `SOLAR_IRRADIANCE`; a TSIS-1 2021 set also exists);
  Lref/SNR (SentiWiki); dark/linearity/crosstalk detail (Gorroño & Gascon S2-RUT).
- **Version-sensitive:** band bandwidths (esp. B08 106 vs 115/118 nm) — fix to a named S2-SRF rev.
- **Derived:** F-number; line periods (from Vground 6700 m/s); LTAN ≈ 22:30; 60 m ACT px count.
- **Conflicting / UNVERIFIED:** **TDI line counts** — Martimort spec ~2-line for VNIR+SWIR vs one
  source citing SWIR B10=3/B11-12=4 (per-band unresolved); **inclination** 98.62° (mission) vs 98.49°
  (OCD/Binet). The earlier "~46 km inter-detector parallax" was **erroneous** (cross-detector
  parallax is sub-km, 0.022°–0.059°).
- **Not public (in ADF/GIPP binaries):** absolute cal coefficient A(b); per-band noise α_Z/β_Z;
  PRNU/DSNU magnitudes; crosstalk matrix; full SRF curves (**user will provide**).
- **Environment caveat:** `sentinel.esa.int` was DNS-unreachable during sourcing; ESA figures were
  taken from SentiWiki / SP-1322 / eoPortal / peer-reviewed mirrors carrying the same official values.

## A.11 REAL values extracted from the product metadata (2026-06-29)

Pulled from the EOPF reference product `S02MSIL1B_20240403T000000_0001_A123_T000.zarr.zip`
(`other_metadata/image_data_info/...`) — **no credentialed ADF needed**. The matched chain
L0P+L1A+L1B @ 20240403 enables empirical derivation; the L0 (`S02MSIL0__`) is the output reference.

**Per-band `physical_gains` (DN↔radiance) + integration time:**

| band | physical_gain | integ (ms) | band | physical_gain | integ (ms) |
|---|---|---|---|---|---|
| B01 | 4.10503 | 7.4474 | B08 | 6.14137 | 1.2705 |
| B02 | 3.75138 | 1.2822 | B8A | 5.11991 | 2.5587 |
| B03 | 4.17678 | 1.3230 | B09 | 8.50206 | 7.5934 |
| B04 | 4.50915 | 1.3873 | B10 | 55.05589 | 5.6990 |
| B05 | 5.19263 | 2.8441 | B11 | 35.29882 | 1.4036 |
| B06 | 4.85731 | 2.7251 | B12 | 106.15880 | 1.5004 |
| B07 | 4.52068 | 2.7489 | | | |

- `radio_add_offset` = **−100** (L1B; L1C = −1000, PB04.00). `compression_rate` per band 2.4–2.97.
- **`tdi_configuration_list` = {B03: APPLIED, B04: APPLIED, B11: APPLIED, B12: APPLIED}** (definitive).
- `line_period` = **1.5658736 ms**; `nuc_table_id` = 3; `compress_mode`/`equalization_mode` = true;
  equalization per band = DSNU + offset proc. `active_detector` = 07 (these test products are
  single-detector subsets — d07).
- Local data: `/media/cando/T7/01_cdk/59_gitlab_repos/Copernicus/raw-data-gen/data/` (5.8 GB, all levels
  L0__/Synthetic L0P/L1A/L1B/L1C/L2A) + `reference/` (l0_product_structure.json, reverse_chain_adf_analysis.json).
