Adjusted layered PTES model#2181
Draft
cpschau wants to merge 6 commits into
Draft
Conversation
Replace the single 'ptes' heat source with one COP profile per layer ('ptes layer {i}') when multiple layer temperatures are configured, so each layered-PTES booster sees the COP for lifting its own layer temperature to the forward temperature.
Charging: replace the create-volume charger (+ per-layer vents + bottom-water generator) with mass-conserving volume-trade chargers. Each link raises 1 m3 from a colder source layer to a hotter destination layer, drawing rho*c_p*(T_dest-T_src) of district-heating heat, so heat drawn equals the bottom-referenced stored-energy gain and no volume is created or destroyed. Discharge (replaces the preheater + heat-pump pair): the discharger moves the spent volume to the return-level layer and emits a tracking token on the preheater bus; a direct-utilisation link turns the token into DH heat down to the return level; a cooling-extraction multilink relocates the volume to the reinjection layer and releases its full enthalpy onto an hp-source bus; a heat-exchanger split routes the above-return part straight to DH and the sub-return (evaporator) part to the booster heat pump, which lifts it to forward temperature (electricity on bus1, evaporator on bus2). Every quantity references the discrete deposit layers, so delivered heat minus electricity equals the bottom-referenced store debit exactly (energy created = 0, volume conserved to ~1e-6). The single-tank (num_layers == 1) path keeps the original create-volume charger and preheater+HP. solve_network: rework the aggregate-throughput constraint to sum over the now-multiple chargers per layer (the old 1:1 charger<->discharger name mapping no longer holds). Note: the booster heat pump must keep electricity on bus1 and the evaporator (hp-inlet) on bus2 -- the distribution-grid rewiring keys off bus1 for heat-pump electricity.
…nch warning PtesApproximator: add conservative_return_layer (default off) which floors the return-level and reinjection layers to the warmest layer at/below the target temperature instead of the nearest, so direct/boosted discharge never deposits above the return level and cannot create energy. Add booster_cop(), recomputing the per-layer COP at the real evaporator outlet (the reinjection-layer depth) rather than a fixed shallow source cooling. Add discharge_free_lunch_warnings(), flagging nodes whose return-level layer sits above the actual return temperature. run.py: pass conservative_return_layer, export booster_cop, log the free-lunch warnings, and write the dataset with an explicit time encoding (the recomputed COP otherwise carries a conflicting time encoding that overflows on reopen).
build_sector.smk: pass conservative_return_layer and the central-heating HP-COP approximation params to build_ptes_operations; complete the per-layer COP expansion on build_cop_profiles. config: add sector.district_heating.ptes.conservative_return_layer (default false).
PtesApproximator.cop unconditionally selected heat_source "ptes layer {i}",
but build_cop_profiles only expands to per-layer labels when num_layers > 1.
For a single layer (incl. the default config) the COP keeps the plain "ptes"
source, so to_dataset() -> hp_return_layer -> cop raised
KeyError "ptes layer 0", crashing build_ptes_operations. Fall back to "ptes"
when num_layers == 1.
Also gate the booster-COP computation on num_layers > 1 (the single-tank
discharge path never uses it) and revert three unrelated formatter-only
reformattings (the HeatSystem for-loop, the EGS assert, the heat-utilisation
efficiency ternaries) to keep the diff focused.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
for more information, see https://pre-commit.ci
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Layered-PTES model notes — formulation, wiring, and the comparison with Amos' model
This documents the current, energy-exact layered-PTES model (the toy's
rechargemodel, now also ported into PyPSA-Eur:scripts/prepare_sector_network.py,scripts/build_ptes_operations/,scripts/solve_network.py) and compares it, component by component, with theAmos model from PyPSA-Eur PR
#2129 ("feat: layered ptes
model"), which the port replaces.
The two models share the same idea — a stack of fixed-temperature water layers
(one PyPSA
Storeper layer, volume in m³, energy densitym3_to_mwh[l] = ρc_p(T_l − T_bottom)) charged from and discharged to theurban-central heat bus, with a booster heat pump for sub-forward layers. They
differ in how volume and energy are tracked, and that difference is the whole
story: the current model conserves both exactly, Amos's creates energy on
discharge.
1. Preliminaries — energy reference, temperatures, ΔT and COP
1a. Notation and the "bottom-referenced store debit"
T_ll(exogenous, hottest-first)T_bottomT_ret,T_fwdT_retLT_boosthp_return_layer)e_ll'sStorein m³Δ(a,b)ρc_p(T_a − T_b)— enthalpy per m³ between two temperatures [MWh/m³]A layer
Storeholds volume (m³), not energy. Each layer is given a fixedenergy density measured relative to the bottom layer,
so the energy content of the whole PTES is the bottom-referenced sum
E = Σ_l m3_to_mwh[l]·e_l. The choice of reference is arbitrary because everyoperation only moves volume between layers: moving 1 m³ from layer
atolayer
bchangesEbym3_to_mwh[a] − m3_to_mwh[b] = ρc_p(T_a − T_b) = Δ(a,b),in which the bottom term cancels. We call that change the store debit (when
Efalls, on discharge) or store credit (whenErises, on charge). Thewhole point of the current model is that, on every operation,
i.e. delivered useful energy is backed exactly by a bottom-referenced volume
relocation, with no leftover.
1b. The heat-pump output relation (shared, and correct in both models)
A discharged m³ of layer water at
T_srcgives up heat as it cools. Part isdelivered directly (heating the DH return up to
T_src); the sub-return partis the evaporator heat the booster lifts to
T_fwd:Both models get this relation right; they differ in how the cooling depth ΔT and the COP are
parametrised (§1c) and in how the volume is booked (§2–§4).
1c. Per-layer cooling depth ΔT, reinjection layer, and COP
A booster discharging layer
ncools the water from its source-inlet temperaturedown to a deposit temperature, and that cooling depth fixes both where the
spent volume is reinjected and how efficient the heat pump is.
Cooling depth / reinjection target. The ideal evaporator outlet is
T_target = T_ret − ΔT_HPwithΔT_HP = (T_fwd − T_source)·(1 − 1/COP)(
T_source = min(T_n, T_ret)), i.e. the booster cools the return-level waterbelow the return temperature by the amount the COP can repay. This continuous
target is snapped to a discrete layer to give
hp_return_layer( = theT_boostlayer):T_target(strictly colder than the source).conservative_return_layer, the warmest layer at orbelow
T_target(a floor), so the deposit is never warmer than intendedand discharge cannot create energy; otherwise nearest, with a warning when a
layer ends up above the return temperature.
COP per layer per timestep. The Jensen et-al. thermodynamic model
(
CentralHeatingCopApproximator) is evaluated withsink_outlet = T_fwd,sink_inlet = max(T_n, T_ret)(after preheating),source_inlet = min(T_n, T_ret)and a source_outlet:
source_inlet − heat_source_cooling(a fixed shallow ΔT, e.g. 6 K)⟹ optimistic, the COP ignores how deep the water is actually cooled.
source_outlet = T[hp_return_layer](the real evaporatordepth) — recomputed in
PtesApproximator.booster_copand exported asbooster_cop. Deeper cooling ⟹ lower COP ⟹ more booster electricity, honestly.Energy conservation does not depend on the COP value (heat = evaporator +
elec for any COP); the deposit-depth COP only sharpens the elec/source split.
The ΔT–COP circularity is broken, not solved. Note
ΔT_HPdepends on the COPand the COP (at
source_outlet = T_ret − ΔT_HP) depends onΔT_HP— an implicitequation. It is not solved as a fixed point. Instead a one-shot, two-pass
forward scheme is used: (1)
build_cop_profilescomputesCOP₀once with afixed shallow source cooling, so
COP₀carries noΔT_HPdependence and the loopis cut; (2)
hp_return_layerevaluatesΔT_HP = (T_fwd−T_source)(1−1/COP₀)pertimestep, takes the min over time, and snaps it to a discrete layer; (3)
booster_copre-evaluatesCOP₁once at that snapped depth, and that is the COPthe LP uses. The COP that picks the layer (
COP₀) and the COP the LP runswith (
COP₁) are deliberately left slightly inconsistent — it does not matter,because the deposit is discrete anyway and (per §1a) the LP energy balance is
COP-independent, so the residual only nudges the electricity/source split, never
the energy total. A self-consistent value would be a scalar Picard iteration
ΔT ← (T_fwd−T_source)(1−1/COP(T_ret−ΔT))per (layer, node), but it is not worththe cost.
2. Wiring of the current model (energy-exact, volume-conserving)
Per layer
n, the PyPSA components and their link efficiencies (notation, incl.Δ(a,b),T_retL,T_boost, in §1a).nb=needs_boosting(1 whenT_fwd > T_n, else 0). The three cooling depths are clipped at zero:2a. Charging — volume trade (no volume created)
+xm³ appear atd,−xm³ leaves⇒ volume conserved. Heat drawn= Δ(T_d,T_s)·x =stored-energy gainm3_to_mwh[d]−m3_to_mwh[s]⇒ exact.2b. Discharging — discharger + the direct/boost split
The split fractions always sum to 1 (when
extract_dt > 0; set to 0 otherwise toavoid 0/0), and their boundary cases say which path the discharge takes:
direct_frac = 1, hp_frac = 0⟺below_drop = 0⟺T_retL = T_boost: thereis no usable boosting depth — the return-level layer is already the deposit
layer (common under the conservative floor, where
T_retLis pushed to thebottom). The booster never runs and the whole release goes direct. This is why
the solved 5-layer case draws ~0 booster electricity.
direct_frac = 0, hp_frac = 1⟺direct_drop = 0⟺T_n ≤ T_retL: asub-return layer is being cooled, so the entire extraction is evaporator heat
(no above-return part) — see case (iv).
0 < direct_frac < 1: a genuinely boosted warm layer (T_n > T_retL > T_boost),with both a direct part and an evaporator part.
NB on bus order: electricity must sit on
bus1of the booster — PyPSA-Eur'sdistribution-grid rewiring keys off
bus1for heat-pump electricity and wouldotherwise append
" low voltage"to whatever is onbus1, mangling theevaporator bus into a phantom free-energy source.
2c. Stores, interlayer links, generators, constraints
Everything references the discrete deposit layers, so on every discharge
delivered − elec = ρc_p(T_n − T_deposit) =the bottom-referenced store debit.Volume is conserved at every link. The only dissipation is the interlayer flux
(
standing_loss = 0on the stores, so the loss is single-channel).3. Wiring of the Amos model (PR #2129) — where energy leaks in
Same layer stores, but charging mints volume and discharging is a
preheater → heat-pump chain whose heat references the actual return temperature
while its volume is booked against discrete layers. Same
Δ/nbnotation as §2.3a. Charging — create-volume (volume minted, not traded)
3b. Discharging — discharger → preheater → heat pump
3c. Stores, generators, constraints
Two structural mismatches make this leak (cf. the current model, which has neither):
per-layer vents (rewarded,
marginal_cost = −1) destroy it; the bottom-watergenerator mints more.
Σ_l e_lis not conserved per operation, so the optimisercan route around the physics.
T_ret, volume references a discrete layer. Thepreheater delivers
Δ(T_n, T_ret_actual), but the spent volume is debited onlydown to the nearest
preheater_return_layer/hp_return_layer. When thatdiscrete layer sits above the actual return temperature the discharge debits
too little ⇒ every m³ conjures
ρc_p(T_layer − T_ret_actual); and the HP deliversthe lift
Δ(T_fwd, T_n)regardless of how deep the water was actually cooled,so the lift and the volume debit only reconcile if
hp_return_layersnaps exactlyto
T_ret − ΔT_HP.4. The four cases — volume & energy, current vs Amos
Notation in §1a (
T_retL,T_boost,Δ);v= 1 m³ discharged. "Store debit"is the bottom-referenced energy drop
Σ Δ(from,to)·v(§1a).(i) Charging
−vat a sub-return source,+vat the hot dest → conserved= ρc_p(T_d−T_s)·v =stored gain → exact+vat the dest from nothing; balanced later by free vents / bottom-water(ii) Direct discharge, no boost (warm layer,
T_fwd ≤ T_n)−vatn,+vat the return-level layer → conservedρc_p(T_n−T_retL)= debitm3_to_mwh[n]−m3_to_mwh[retL]→ exact−vatn,+vatpreheater_return_layer(discrete)ρc_p(T_n−T_ret_actual), debitρc_p(T_n−T_prl); mismatchρc_p(T_prl−T_ret)→ free lunch if T_prl > T_ret(iii) Boosted discharge from a layer with
T_n > T_ret(T_fwd > T_n)The discharged m³ is cooled in two stages: directly down to the return level
(
Δ(T_n,T_retL)straight to DH), then by the booster from there down to thereinjection layer (evaporator
Δ(T_retL,T_boost), lifted to DH atCOP/(COP−1)).−vatn,+vat the boost layer (return level is a transient pass-through) → conserveddelivered − elec = Δ(T_n,T_boost)= debitm3_to_mwh[n]−m3_to_mwh[boost]→ exact (the hp-source bus carries the full released enthalpy, the HX splits it)−vatn,+vathp_return_layer(discrete)delivered = Δ(T_n,T_ret)+Δ(T_fwd,T_n); exact only ifT_hp_return = T_ret − (T_fwd−T_n)(1−1/COP)lands exactly on a layer; the discrete snap leaks(iv) Discharge of a sub-return layer (
T_n ≤ T_ret)Δ(T_n,T_retL) = 0once floored); if cooled further they are pure HP-evaporator source. Volume and (low, near-bottom) energy stay exactly tracked.preheater_efficiency = 0forT_n ≤ T_ret, so no direct heat; the volume still flows through the create/vent/bottom-water machinery, so the cold end is where most of the free volume is minted and vented.5. Net energy balance (legacy intermediate model omitted)
Toy, single node, identical boundary conditions (
created = load + losses − elec; SCOP= load/elec):Workflow, PyPSA-Eur 5-layer scenario, 26 urban-central nodes (
created = net PTES heat to DH − elec drawn + interlayer loss):The current model nets to exactly zero energy created with volume conserved
to 5e-7; its only loss is the interlayer standing-loss channel. Amos's structure
conjures energy on discharge, which the optimiser then exploits (the objective is
artificially low and the PTES is over-cycled).
6. Discretisation bias (both models)
The model is a piecewise-constant approximation of a continuously stratified PTES.
For evenly-spaced layers the nominal layer temperature is the band mean, and
the Jensen COP uses both inlet and outlet temperatures (the glide) — already a
mean-temperature representation, more accurate than a single-mean-T COP. The
residual error is quantisation (collapsing each band to a point), bounded by ≈ ½ a
layer spacing. In the toy the
n_layerssweep gives 3→33.5, 5→32.7, 9→31.4 M€:coarser layers under-value the PTES (higher cost), converging downward as N
grows — i.e. conservative. Room to improve within linearity: more / non-uniform
layers concentrated near the forward and return temperatures (layer temps must
stay exogenous — endogenous temperatures are nonlinear).
Conservative discretisation (current model). Because the current model
references discrete deposit layers, it never creates energy regardless of layer
placement; the only question is whether the direct part over- or under-delivers
relative to the physical return temperature. The optional
sector.district_heating.ptes.conservative_return_layerflag floors thereturn-level and reinjection layers to the warmest layer at/below the target, so
discharge stays on the conservative side;
build_ptes_operationsadditionallywarns for any node whose return-level layer sits above its return temperature.
7. Implementation notes (current model)
~L²/2volume-trade charger links per node (one percold-source × hot-dest pair); the single-tank
num_layers == 1case keeps thelegacy create-volume charger.
source_outlet = T[hp_return_layer](the realevaporator depth) in
PtesApproximator.booster_copand exported asbooster_copinptes_operations;prepare_sector_networkuses it for thebooster. Energy conservation is independent of the COP value (heat = evaporator
standing_loss = 0, the losslives entirely in the interlayer-flow constraint. A nonzero store
standing_losswould, with volume-conserving charging and
e_cyclic, drive Σ_l e_l → 0.bus1, evaporator (hp-inlet) onbus2(see §2b note).