# gdir size regression in v1.7a vs v1.6.3

## Context

Comparing OGGM output directories from three runs on the same machine (RGI62,
border 160, per_glacier_spinup / elev_bands / ERA5), for the ~10 RGI regions
that all three runs happen to share (06–12, 16, 18, 19; the full runs stopped
early on quota):

- `oggm_v163` — 1.6.2.dev checkout
- `oggm_v17a` — 1.6.2-28-g367da93 (the "1.7a" dev branch)
- `oggm_v17a_snow` — same branch + SnowModel (separate issue, see bottom)

Comparing v1.7a against v1.6.3 at every shared level (`L3`, `L4`, `L5`) for
the same glaciers (glacier counts verified identical between runs, so this
isn't a partial-run artifact):

| Region | Glaciers | v1.6.3 | v1.7a | ×    |
|---|---|---|---|---|
| RGI60-11 Central Europe | 3,927 | 8.41 GiB | 10.69 GiB | 1.27× |
| RGI60-12 Caucasus/Middle East | 1,339 | 3.83 GiB | 3.83 GiB | 1.00× |
| RGI60-09 Russian Arctic | 331 | 2.25 GiB | 18.15 GiB | 8.08× |
| RGI60-19 Antarctic & Subantarctic | 2,752 | 3.40 GiB | 38.96 GiB | **11.46×** |
| **Total (10 regions)** | 25,238 | **57.45 GiB** | **149.40 GiB** | **2.60×** |

The multiplier tracks region complexity, not glacier count or region size —
that ratio ends up being the clue (see root cause below).

## Root cause

`Flowline.__init__` in `oggm/core/flowline.py` (~line 104–106, and duplicated
at ~685–686) does:

```python
if gdir is not None:
    gdir.settings_filesuffix = settings_filesuffix
    self.settings = gdir.settings
    self.map_trafo = partial(gdir.grid.ij_to_crs, crs=salem.wgs84)
else:
    self.settings = cfg.PARAMS.copy()
```

So every `Flowline`/`MixedBedFlowline` instance now carries a `.settings`
attribute pointing at a `ModelSettings` object
(`oggm/utils/_workflow.py:5333`). `ModelSettings.__init__` does, at line 5365:

```python
if self.data['parent_filesuffix'] == 'cfg.PARAMS':
    self.defaults = cfg.PARAMS.copy()
```

`cfg.PARAMS.copy()` is a **shallow** copy — so `.defaults` doesn't just carry
scalar model parameters, it carries a live reference to whatever object
`cfg.PARAMS['intersects_gdf']` points to. And `intersects_gdf` isn't a scalar:
`oggm/cfg.py:783` sets it to a full `geopandas.GeoDataFrame` — every
glacier-polygon intersection record for the *entire RGI region*, loaded once
per run:

```python
PARAMS['intersects_gdf'] = gpd.read_file(path_or_gdf)   # cfg.py:783
```

`ModelSettings` also stores `self.gdir = gdir` (the parent `GlacierDirectory`
object itself — confirmed negligible in size, ~1.3 KB pickled, not part of
the problem).

None of `Flowline`, `MixedBedFlowline`, or `ModelSettings` define
`__getstate__`/`__reduce__`, so when a flowline list is pickled to
`model_flowlines.pkl` (and `model_flowlines_dyn_melt_f_calib.pkl`), the
default pickle protocol walks the whole object graph and serializes the
**entire region-wide `intersects_gdf` table, once per glacier, with no
deduplication across files**.

### Measured impact, per glacier

| Glacier | Region | `intersects_gdf` rows | Pickled alone | `model_flowlines.pkl` (v1.6.3 → v1.7a) |
|---|---|---|---|---|
| RGI60-11.03300 | Central Europe | 1,149 | 361 KB | 3.6 KB → 176 KB (**×48**) |
| RGI60-19.00001 | Antarctic & Subantarctic | 4,988 | 5.34 MB | 3.7 KB → 3.4 MB (**×920**) |

`intersects_gdf` size scales with how many glacier margins touch/overlap in
a region — not glacier count — which is exactly why RGI60-12 (few adjoining
glaciers) shows ~1.00× and RGI60-19 (dense Antarctic ice-cap margins) shows
11.46×.

It shows up **up to 5 times per glacier** across the pipeline: once in L3's
`model_flowlines.pkl`, twice in L4 (`model_flowlines.pkl` +
`_dyn_melt_f_calib.pkl`), twice again in L5. L5 shows the worst *relative*
damage of the three tiers, because L5 strips the bulky `dem.tif` /
`gridded_data.nc` — so the redundant pickle ends up being the majority of
what's left in the smallest, distribution-facing tier.

(Side note, not part of the regression: v1.7a's `dem.tif` is written
deflate-compressed vs. uncompressed in v1.6.3, ~4× smaller. That's a real
win that happens to mask the regression at L3 — it doesn't survive to L5
where the DEM is dropped entirely.)

## Suggested fix — two separable things

1. **Don't let a `GeoDataFrame` — or anything non-trivial — end up in
   `cfg.PARAMS` in a way that gets silently copied around as a "parameter".**
   `intersects_gdf` being a first-class value in `cfg.PARAMS` next to scalars
   like `glen_a` or `melt_f_min` is what let this happen invisibly:
   `ModelSettings.defaults = cfg.PARAMS.copy()` (or anything else that
   snapshots/serializes PARAMS) had no way to know it was dragging a
   multi-MB region-wide table along. Either keep `intersects_gdf` out of
   `cfg.PARAMS` entirely (store it somewhere not treated as a generic
   parameter dict), or have `ModelSettings`/`Flowline.settings` explicitly
   exclude non-parameter/heavy values (e.g. filter to JSON/YAML-serializable
   scalars only — which is presumably the actual intent, given `ModelSettings`
   is a `YAMLFileObject` and the settings are also written out to
   `settings.yml`, which does *not* contain `intersects_gdf`, so the pickled
   in-memory copy is already inconsistent with what's on disk).

2. **`Flowline.settings` doesn't need a live `gdir` back-reference at all**
   for this to work — you flagged this one as the easy part. `ModelSettings`
   already knows its own file `path`; there's no need to carry the full
   `GlacierDirectory` object through pickling to get back to it. This one's
   small in isolation (~1.3 KB/glacier) but it's the same pattern and worth
   closing at the same time — a future attribute added to `GlacierDirectory`
   would silently ride along the same way `intersects_gdf` did.

Belt-and-suspenders: since neither `Flowline` nor `ModelSettings` currently
defines `__getstate__`, a targeted `__getstate__` on `Flowline` that drops or
trims `.settings` before pickling (keeping only what a rehydrated model
actually reads via `self.settings[...]`) would catch this class of bug
generally, not just for `intersects_gdf` specifically — worth doing even if
(1) and (2) are fixed, as defense against the next thing that lands in
`cfg.PARAMS` unnoticed.

Either (1) or (2) alone should collapse L5 back to roughly v1.6.3 size in the
regions audited, since L5's entire size increase there is attributable to
this one object graph.

## Not investigated here: SnowModel L4 overhead (separate issue)

Flagged only for later — not root-caused with the same rigor as the above.
v1.7a-Snow adds two new files per glacier at L4,
`mb_diagnostics_historical.nc` and `mb_diagnostics_spinup_historical.nc`, at
**3.14 MB each** — versus ~8.9 MB for the entire rest of the directory on the
same Alpine glacier used above. That's the whole story behind Snow's L4
roughly doubling plain v1.7a's, independent of the `intersects_gdf` issue
above (which affects the Snow config equally, on top of this).

## How this was verified

- Sizes are `du -sb` on-disk totals per `L{3,4,5}/RGI60-NN` directory.
- Glacier counts cross-checked by listing every `.tar.gz` member inside each
  region's batch tars — identical between v1.6.3 and v1.7a per region — to
  rule out a partial/quota-truncated run as the explanation.
- Pickle inspection used each run's actual environment (`oggm.__version__`
  and `oggm.__file__` checked to confirm it matched the run that produced
  the file) rather than guessing at class layout.
- File/line references above are from the installed v1.7a package at
  `oggm/core/flowline.py` and `oggm/utils/_workflow.py`, `oggm/cfg.py` in
  that environment — worth confirming they still match current `master`
  before patching, in case of drift since these runs were produced.
