Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Global Energy Monitor data integration
- Technology configuration system
- Network plotting with customizable styles
- Policies (subsidies) and differentiated fuel costs

### Changed
- Improved documentation structure with tutorials and reference guides
Expand Down Expand Up @@ -67,7 +68,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Version History Notes

PyPSA-China (PIK) is adapted from the Zhou et al. version, which was originally developed by Hailiang Liu et al. This changelog tracks changes from version 1.0.0 onwards in the PIK implementation.
PyPSA-China (PIK) is based on the paper by Zhou et al, which extends a version original developed by Hailiang Liu et al. This changelog tracks changes from version 1.0.0 onwards in the PIK implementation.

For detailed information about specific changes, see the [commit history](https://github.com/pik-piam/PyPSA-China-PIK/commits/main) on GitHub.

Expand Down
39 changes: 27 additions & 12 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -542,18 +542,33 @@ nodes:
- **`splits`**: Custom groupings of admin level 2 regions within provinces

## Fuel Subsidies

```yaml
subsidies:
enabled: false
gas:
Guangdong: -10
Jiangsu: -10
Zhejiang: -10
Beijing: -11
Tianjin: -11
Shanghai: -11
```
Fuel subsidies can be speficied for all years or per year

=== "All years"
```yaml
subsidies:
enabled: false
gas:
Guangdong: -10
Jiangsu: -10
Zhejiang: -10
Beijing: -11
Tianjin: -11
Shanghai: -11
```
=== "Year by year"
```yaml
subsidies:
enabled: false
gas:
2020:
Guangdong: -10
Jiangsu: -10
Zhejiang: -10
Beijing: -11
Tianjin: -11
Shanghai: -11
```

Provincial fuel subsidies configuration:
- **`enabled`**: Enable/disable fuel subsidy system
Expand Down
34 changes: 34 additions & 0 deletions examples/historical.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# A Configuration to reproduce historical mix for 2020 and 2025
# run with `snakemake --configfile=examples/historical.yml`

run:
name: "reproduce_historical_load"
foresight: "overnight"

scenario:
co2_pathway: ["exp175default"] # co2_scenarios that will be used
topology: "current+FCG" # "current" or "FCG" or "current+FCG" or "current+Neighbor"
planning_horizons:
- 2020
- 2025

subsidies:
enabled: True # Set to false to disable fuel subsidies
# Year-dependent format: subsidies.fuel_type -> year -> province -> value (EUR/MWh)
# Only negative values allowed (subsidies reduce marginal cost)
# Gas favoured over coal in urban areas due to PM2.5 concerns
gas:
2020:
Guangdong: -10.16
Jiangsu: -10.16
Zhejiang: -10.15
Beijing: -12.5
Tianjin: -12.5
Shanghai: -12.5
Xinjiang: -10
# location dependent fuel prices (cheaper in Shaanxi and Inner Mongolia for example)
coal:
2020:
Xinjiang: -4
InnerMongolia: -4.5
Hebei: -4
7 changes: 3 additions & 4 deletions workflow/scripts/plot_time_series.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,10 +267,9 @@ def plot_residual_load_duration_curve(
)
.groupby(level=1)
.sum()
.loc[vre_techs]
.sum()
)

tech_filter = [t for t in vre_techs if t in vre_supply.index]
vre_supply = vre_supply.loc[tech_filter].sum()
residual = (load - vre_supply).sort_values(ascending=False) / PLOT_CAP_UNITS
residual.reset_index(drop=True).plot(ax=ax, lw=3)
ax.set_ylabel(f"Residual Load [{PLOT_CAP_LABEL}]")
Expand Down Expand Up @@ -529,7 +528,7 @@ def plot_vre_timemap(
# co2_pathway="SSP2-PkBudg1000-CHA-pypsaelh2",
heating_demand="positive",
# configfiles=["resources/tmp/remind_coupled_cg.yaml"],
planning_horizons="2050",
planning_horizons="2025",
winter_day1="12-10 21:00", # mm-dd HH:MM
winter_day2="12-17 12:00", # mm-dd HH:MM
spring_day1="03-31 21:00", # mm-dd HH:MM
Expand Down
89 changes: 0 additions & 89 deletions workflow/scripts/prepare_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,86 +246,6 @@ def add_co2_capture_support(
)


def add_fuel_subsidies(n: pypsa.Network, subsidy_config: dict):
"""Apply fuel subsidies to generators as a post-processing step.

Subsidies are applied to generators based on their carrier and location.
The subsidy values (in EUR/MWh fuel) are divided by efficiency to convert
to electricity basis (EUR/MWhel). Links are not modified as they get their
fuel from generators.

Args:
n (pypsa.Network): The network object to modify.
subsidy_config (dict): Subsidy configuration dictionary with keys like
"coal" or "gas", each containing a dict mapping provinces to subsidy values.
"""
if not subsidy_config:
return

carriers = subsidy_config.keys()

for carrier in carriers:
subs_dict = subsidy_config.get(carrier, {})
if not subs_dict:
continue

# Convert subsidy dict to Series indexed by province
subs = pd.Series(subs_dict, dtype=float)

# Check that subsidies are non-positive (negative = subsidy, positive would be a reward)
if (subs > 0).any():
raise ValueError(
f"Positive subsidy values found for carrier '{carrier}': "
f"{subs[subs > 0].to_dict()}. Only zero or negative values are allowed "
f"(negative reduces marginal cost, positive would increase it)."
)

# Check if location column exists
if "location" not in n.generators.columns:
logger.warning(
f"Location column not found in generators. "
f"Cannot apply subsidies for carrier '{carrier}'."
)
continue

# Query generators with matching carrier and location in subsidy provinces
mask = n.generators.query(
f"carrier == @carrier and location in @subs.index"
).index

if mask.empty:
logger.warning(
f"No generators found with carrier '{carrier}' and locations "
f"in {list(subs.index)}. Skipping subsidy application."
)
continue

# Merge subsidies with generators by location
gen_locs = n.generators.loc[mask, "location"]
subs_to_apply = gen_locs.map(subs).fillna(0.0)

# Check if all provinces were found
missing_provs = set(subs.index) - set(gen_locs.unique())
if missing_provs:
logger.warning(
f"Subsidies specified for provinces {missing_provs} but no "
f"generators found with carrier '{carrier}' in these provinces."
)

# Apply subsidies: divide by efficiency to convert from fuel to electricity basis
# Handle cases where efficiency might be NaN or missing
efficiencies = n.generators.loc[mask, "efficiency"].fillna(1.0)
subs_electricity = subs_to_apply / efficiencies

# Subtract subsidy from marginal cost (negative subsidy = cost reduction)
n.generators.loc[mask, "marginal_cost"] += subs_electricity

logger.info(
f"Applied subsidies for carrier '{carrier}' to {len(mask)} generators "
f"in provinces {sorted(gen_locs.unique())}"
)


def add_conventional_generators(
network: pypsa.Network,
nodes: pd.Index,
Expand Down Expand Up @@ -786,7 +706,6 @@ def add_wind_and_solar(
Raises:
ValueError: If unsupported technologies are specified or if paths not specified
"""

unsupported = set(techs).difference({"solar", "onwind", "offwind"})
if unsupported:
raise ValueError(f"Carrier(s) {unsupported} not wind or solar pv")
Expand Down Expand Up @@ -1698,14 +1617,6 @@ def prepare_network(

assign_locations(network)

# Apply fuel subsidies as post-processing step
subsidy_config = config.get("subsidies", {})
if subsidy_config and subsidy_config.get("enabled", True):
# Remove 'enabled' key before passing to add_fuel_subsidies
subsidy_config_clean = {k: v for k, v in subsidy_config.items() if k != "enabled"}
if subsidy_config_clean:
add_fuel_subsidies(network, subsidy_config_clean)

return network


Expand Down
Loading