Explicit representation of n largest district heating systems#1995
Explicit representation of n largest district heating systems#1995
Conversation
doc/configtables/sector.csv
Outdated
| @@ -0,0 +1,243 @@ | |||
| ,Unit,Values,Description | |||
| transport,--,"{true, false}",Flag to include transport sector. | |||
| heating,--,"{true, false}",Flag to include heating sector. | |||
There was a problem hiding this comment.
All doctables are gone and this needs to be merged in the pydantic schema
- option to scale subnodal demands to align with ISI data assumptions (extracted in CSV) - fix city name mapping includin merge of overlapping city areas - adjust config validation
| nodes = pop_layout.index | ||
| heat_nodes = pop_layout.index | ||
| parent_of_subnode = pd.Series(heat_nodes, index=heat_nodes) | ||
|
|
There was a problem hiding this comment.
This seems overly complicated. Could we simplify that? Also, I'd suggest computing parent_of_subnode in a preceding rule.
| heat_demand_grouped = heat_demand.T.groupby(level=1).sum().T | ||
| heat_load = heat_demand_grouped[heat_nodes].multiply( | ||
| factor * (1 + options["district_heating"]["district_heating_loss"]) | ||
| ) |
There was a problem hiding this comment.
Not sure why we need to define heat_demand_grouped. Doesn't seem to be used elsewhere, as far as I can see.
| heat_dsm_profile_raw = pd.read_csv( | ||
| heat_dsm_profile_file, | ||
| header=1, | ||
| index_col=0, | ||
| parse_dates=True, | ||
| )[nodes].reindex(n.snapshots) | ||
| ) | ||
| heat_dsm_profile = heat_dsm_profile_raw[heat_nodes].reindex(n.snapshots) |
There was a problem hiding this comment.
Again, heat_dsm_profile_raw seems unnecessary. Adding indexing by [heat_nodes] should do the trick.
| heat_demand_res_space = ( | ||
| heat_demand[["residential space"]].T.groupby(level=1).sum().T | ||
| ) | ||
| e_nom = heat_demand_res_space[heat_nodes].multiply(factor) | ||
|
|
| ptes_supp_data = xr.open_dataarray(ptes_direct_utilisation_profile) | ||
| ptes_supplemental_heating_required = ( | ||
| xr.open_dataarray(ptes_direct_utilisation_profile) | ||
| .sel(name=nodes) | ||
| ptes_supp_data.sel(name=heat_nodes) | ||
| .to_pandas() |
| p_max_source_raw = pd.read_csv( | ||
| heat_source_profile_files[heat_source], | ||
| index_col=0, | ||
| parse_dates=True, | ||
| ).squeeze()[nodes] | ||
| ).squeeze() | ||
|
|
||
| # if only dimension is nodes, convert series to dataframe with columns as nodes and index as snapshots | ||
| p_max_source = p_max_source_raw[heat_nodes] | ||
|
|
| : -len(" urban central heat") | ||
| ] | ||
| # Map subnodes to parent clusters for resource bus lookups | ||
| if "parent_node" in district_heat_info.columns: |
There was a problem hiding this comment.
This simply checks if subnodes are enabled, correct? If yes, could we pass sector:district_heating:subnodes:enable here and in other places, where we're checking for that? Seems more readable.
| @@ -4601,12 +4626,6 @@ def add_industry( | |||
| # 1e6 to convert TWh to MWh | |||
| industrial_demand = pd.read_csv(industrial_demand_file, index_col=0) * 1e6 * nyears | |||
|
|
|||
| if not options["biomass"]: | |||
| raise ValueError( | |||
| "Industry demand includes solid biomass, but `sector.biomass` is disabled. " | |||
| "Enable `sector: {biomass: true}` in config." | |||
| ) | |||
There was a problem hiding this comment.
Correct to remove, but should then be checked in the validator.
| lt_heat_nodes = [ | ||
| node | ||
| for node in industrial_demand.index | ||
| if node + " urban central heat" in n.buses.index | ||
| or node + " services urban decentral heat" in n.buses.index | ||
| ] | ||
| lt_heat_nodes = pd.Index(lt_heat_nodes) | ||
|
|
There was a problem hiding this comment.
Isn't this just heat_nodes?
| ft_nodes = urban_central[ | ||
| (urban_central + " Fischer-Tropsch").isin(n.links.index) | ||
| ] |
There was a problem hiding this comment.
ft_nodes (as, e.g. sabatier_nodes) seem to be on parent-node level. Shouldn't we use heat_nodes here?
| demand_column = snakemake.params.get("demand_column", "Dem_GWh") | ||
| label_column = snakemake.params.get("label_column", "Label") |
There was a problem hiding this comment.
Let's make those module level constants rather than config settings (not really something users would change).
| if subnode_countries: | ||
| invalid = set(subnode_countries) - set(countries) | ||
| if invalid: | ||
| logger.warning(f"Invalid subnode_countries {invalid} ignored") | ||
| effective_subnode_countries = [c for c in subnode_countries if c in countries] | ||
| else: | ||
| effective_subnode_countries = countries |
There was a problem hiding this comment.
Let's move this to the config validator
District Heating Subnodes Feature
Summary
This PR introduces district heating (DH) subnode modeling to PyPSA-Eur, enabling finer spatial resolution for urban central heating systems. Key capabilities include:
sector.district_heating.subnodes.countriesData Source
The feature is based on ISI/seenergies district heating area shapes derived from heating density data. These shapes provide:
Structural Integration
New Scripts
scripts/identify_district_heating_subnodes.pyscripts/prepare_district_heating_subnodes.pyscripts/map_dh_systems_to_cities.pyModified Scripts
prepare_sector_network.pydefine_spatial()for district heating nodes; heat buses, loads, and technologies now iterate overheat_nodes(including subnodes); parent node mapping for resource bus lookupsrules/build_sector.smkidentify_district_heating_subnodesandprepare_district_heating_subnodes; conditional inputs for subnode-extended data filesConfiguration
Testing
Test Configuration
Three scenarios are compared:
master_branch(code before this PR),subnodes_off(PR code with subnodes disabled), andsubnodes_on(PR code with subnodes enabled).config/config_subnodes.yaml:config/scenarios_subnodes.yaml:Network Topology
Heat Demand Preservation
District Heating Energy Balance
Total System Costs
Scale-up
I also tested the feature with 39 clusters and 200 subnodes at 3h resolution including river water and geothermal as heat sources for district heating supply to ensure robustness. The network was solved in 6 h with max mem usage of 172 GB.
Here are some related results:
District heating balance
Composition of system costs:
District heating balance map
References
Remaining TODOs