Skip to content

layerFracs returns its two interpolation weights in the wrong order #121

Description

@ctessum-claude

Summary

sr.Reader.layerFracs returns its two interpolation weights in the opposite order
to the two layers they belong to. A plume sitting just above the lower SR layer is
given nearly all of its weight on the upper layer, and vice versa.

The weights are correct in magnitude and they sum to 1, so nothing about this is
visible in a mass balance — but emissions are systematically placed on the wrong
side of every split.

Where

inmap/sr/srreader.go

Lines 421 to 428 in 52b7c71

for i := 0; i < len(sr.layers)-1; i++ {
if sr.layers[i] < c.Layer && sr.layers[i+1] > c.Layer {
below := layerHeights[sr.layers[i]]
above := layerHeights[sr.layers[i+1]]
frac := (plumeHeight - below) / (above - below)
return []int{i, i + 1}, []float64{frac, 1 - frac}, nil
}
}

for i := 0; i < len(sr.layers)-1; i++ {
    if sr.layers[i] < c.Layer && sr.layers[i+1] > c.Layer {
        below := layerHeights[sr.layers[i]]
        above := layerHeights[sr.layers[i+1]]
        frac := (plumeHeight - below) / (above - below)
        return []int{i, i + 1}, []float64{frac, 1 - frac}, nil
    }
}

frac is the normalised distance above below, and it is returned paired with
i, the lower layer.

Checking the two limits:

  • plumeHeight → below: frac → 0, so the weights are {0, 1} — the plume is
    sitting on the lower reference layer and 100% of its mass is charged to the
    upper one.
  • plumeHeight → above: frac → 1, weights {1, 0} — the plume is at the upper
    layer and all of its mass is charged to the lower one.

Both are backwards. The intended linear interpolation is

return []int{i, i + 1}, []float64{1 - frac, frac}, nil

Everything else in the function looks right to me — the reference heights come from
sr.d.VerticalProfile, i.e. cell centres, which matches how sr/sr.go places
sources, and the exact-match and AboveTopErr branches are fine. It is only the
order of the two returned weights.

How much it matters

More than it looks, because the split branch is not a rare path. Measured on the
2016 EGU point-source inventory used by the source-receptor
tutorial
— 43,650 records, 1,520 emitting
cells, ISRM v1.2.1:

  • about 61% of emitted mass goes through the split branch (29.6% of records land
    in model layers 1–2, 31.7% in layers 4–5, between the [0, 3, 6] entries of
    sr.layers)
  • correcting the order relocates 192,918 t/yr — 6.25% of total emitted mass

Per pathway, tons/yr into SR layers 0/1/2:

as-is corrected
NOx 294,603 / 702,958 / 316,902 252,274 / 792,960 / 269,229
SOx 199,895 / 804,220 / 567,102 192,744 / 896,195 / 482,279
PM2.5 30,869 / 73,018 / 36,936 26,489 / 81,100 / 33,233
NH3 9,229 / 13,694 / 2,089 8,061 / 15,529 / 1,422
VOC 8,727 / 18,206 / 6,520 9,342 / 18,615 / 5,496

The direction is worth stating explicitly, because "inverted" suggests mass goes too
high and that isn't what happens. Weighting the layer farther from the plume pushes
mass outward from the middle layer in both directions; correcting it pulls mass
back in (L1 +192 kt, L0 −54 kt, L2 −138 kt).

Downstream, national premature deaths from these emissions come out about 1.25%
low
: 6936.1 → 7022.7 (Krewski) and 15640.1 → 15836.0 (Lepeule), i.e. +86.6 and
+195.9 deaths/yr.

So fixing this will change the numbers published in the SR tutorial, which are
6928.959583 and 15623.924632. That seems worth flagging before anyone merges a
one-line change.

How this was found

Not by reading the code — by reimplementing the SR reader's emission-layer
assignment independently (NumPy, from ctessum/atmos/plumerise ASMEPrecomputed
plus this function) and reconciling it against InMAP itself. Reproducing the current
behaviour bug-for-bug matches the live inmap cloud service to 8.9e-9 on the
first 200 records of that inventory, which is what makes the numbers above
attributable to this one line rather than to a difference in my reimplementation.

Happy to open a PR with the one-line fix if that's useful, though the tutorial-number
question probably wants deciding first.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions