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
|
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.
Summary
sr.Reader.layerFracsreturns its two interpolation weights in the opposite orderto 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
fracis the normalised distance abovebelow, and it is returned paired withi, the lower layer.Checking the two limits:
plumeHeight → below:frac → 0, so the weights are{0, 1}— the plume issitting 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 upperlayer and all of its mass is charged to the lower one.
Both are backwards. The intended linear interpolation is
Everything else in the function looks right to me — the reference heights come from
sr.d.VerticalProfile, i.e. cell centres, which matches howsr/sr.goplacessources, and the exact-match and
AboveTopErrbranches are fine. It is only theorder 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:
in model layers 1–2, 31.7% in layers 4–5, between the
[0, 3, 6]entries ofsr.layers)Per pathway, tons/yr into SR layers 0/1/2:
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/plumeriseASMEPrecomputedplus this function) and reconciling it against InMAP itself. Reproducing the current
behaviour bug-for-bug matches the live
inmap cloudservice to 8.9e-9 on thefirst 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.