Skip to content

Commit 1c83e8e

Browse files
committed
Refocus dashboard on green ammonia parity analysis
1 parent d37e908 commit 1c83e8e

11 files changed

Lines changed: 2102 additions & 550 deletions

README.md

Lines changed: 220 additions & 39 deletions
Large diffs are not rendered by default.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
market_key,market_label,grey_nh3_delivered_usd_per_tonne,blue_nh3_delivered_usd_per_tonne,grey_nh3_carbon_intensity_tco2_per_tonne,source_name,source_url,source_note,data_quality
2+
Japan,Japan,480,650,2.4,IEA Ammonia Technology Roadmap,https://www.iea.org/reports/ammonia-technology-roadmap,"Screening benchmark for conventional ammonia delivered-cost comparison; carbon intensity uses IEA's direct CO2 intensity framing for ammonia production.",screening proxy
3+
South Korea,South Korea,470,640,2.4,IEA Ammonia Technology Roadmap,https://www.iea.org/reports/ammonia-technology-roadmap,"Screening benchmark for imported conventional ammonia; replace with contract or market quotation data for investment work.",screening proxy
4+
Rotterdam / Northwest Europe,Rotterdam / Northwest Europe,420,600,2.4,IEA Ammonia Technology Roadmap,https://www.iea.org/reports/ammonia-technology-roadmap,"Northwest Europe conventional ammonia benchmark used to estimate green premium and carbon-price parity.",screening proxy
5+
Germany / Netherlands,Germany / Netherlands,430,610,2.4,IEA Ammonia Technology Roadmap,https://www.iea.org/reports/ammonia-technology-roadmap,"Germany/Netherlands benchmark inherits Northwest Europe ammonia import-market logic.",screening proxy
6+
Europe,Europe,430,610,2.4,IEA Ammonia Technology Roadmap,https://www.iea.org/reports/ammonia-technology-roadmap,"Generic Europe benchmark for corridor-level screening when no single import terminal is specified.",screening proxy
7+
Germany,Germany,430,610,2.4,IEA Ammonia Technology Roadmap,https://www.iea.org/reports/ammonia-technology-roadmap,"Germany benchmark for regional North Sea ammonia routes.",screening proxy
8+
East China,East China,390,570,2.4,IEA Ammonia Technology Roadmap,https://www.iea.org/reports/ammonia-technology-roadmap,"Coastal China benchmark for comparing imported green ammonia against conventional ammonia alternatives.",screening proxy
9+
Singapore,Singapore,460,630,2.4,IEA Ammonia Technology Roadmap,https://www.iea.org/reports/ammonia-technology-roadmap,"Singapore benchmark supports hub, power, and marine-fuel screening use cases.",screening proxy
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
market_key,market_label,wholesale_power_usd_per_mwh,industrial_power_usd_per_mwh,lng_power_benchmark_usd_per_mwh,clean_firm_power_benchmark_usd_per_mwh,carbon_price_usd_per_tco2,source_name,source_url,source_note,data_quality
2+
Japan,Japan,115,155,125,170,90,IEA Electricity 2026,https://www.iea.org/reports/electricity-2026/prices,"Screening power-price benchmark for an energy-importing market; compare ammonia-fired generation against LNG and clean-firm value, not residential tariff.",screening proxy
3+
South Korea,South Korea,105,135,115,160,80,IEA Electricity 2026,https://www.iea.org/reports/electricity-2026/prices,"Screening benchmark for Korean power-market and fuel-import comparison.",screening proxy
4+
Rotterdam / Northwest Europe,Rotterdam / Northwest Europe,95,130,110,160,100,IEA Electricity 2026,https://www.iea.org/reports/electricity-2026/prices,"Northwest Europe benchmark uses wholesale, LNG-to-power, and clean-firm proxy values for feasibility screening.",screening proxy
5+
Germany / Netherlands,Germany / Netherlands,100,140,115,170,100,IEA Electricity 2026,https://www.iea.org/reports/electricity-2026/prices,"Germany/Netherlands benchmark for comparing imported ammonia power cost with regional clean-firm alternatives.",screening proxy
6+
Europe,Europe,95,130,110,160,100,IEA Electricity 2026,https://www.iea.org/reports/electricity-2026/prices,"Generic Europe benchmark used when the importer is specified as Europe rather than a single node.",screening proxy
7+
Germany,Germany,100,140,115,170,100,IEA Electricity 2026,https://www.iea.org/reports/electricity-2026/prices,"Germany benchmark for North Sea and adjacent import routes.",screening proxy
8+
East China,East China,80,100,95,135,55,IEA Electricity 2026,https://www.iea.org/reports/electricity-2026/prices,"Coastal China power benchmark is a screening proxy; replace with provincial industrial tariff and dispatch benchmark data for diligence.",screening proxy
9+
Singapore,Singapore,145,170,150,200,80,IEA Electricity 2026,https://www.iea.org/reports/electricity-2026/prices,"Singapore benchmark reflects an import-dependent LNG-linked power system and potential clean-firm premium.",screening proxy

main.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
PROCESSED_DIR,
3333
ScoreWeights,
3434
)
35+
from src.benchmark_model import (
36+
apply_conventional_ammonia_benchmarks,
37+
benchmark_source_notes,
38+
)
3539
from src.cost_model import calculate_costs
3640
from src.data_loader import (
3741
WORLD_BANK_SOURCE_URLS,
@@ -40,14 +44,21 @@
4044
ensure_project_directories,
4145
fetch_world_bank_corridor_indicators,
4246
load_corridors,
47+
load_conventional_ammonia_benchmarks,
4348
load_export_region_potential,
49+
load_import_market_power_benchmarks,
4450
load_shipping_route_geometries,
4551
load_shipping_routes,
4652
apply_shipping_route_geometries,
4753
apply_shipping_routes,
4854
try_fetch_country_metadata,
4955
)
5056
from src.distance import add_shipping_distance
57+
from src.parity_model import calculate_break_even_metrics
58+
from src.power_model import (
59+
apply_power_benchmarks,
60+
calculate_ammonia_power_competitiveness,
61+
)
5162
from src.scoring import calculate_feasibility_scores
5263
from src.visualization import write_dashboard_html
5364

@@ -89,6 +100,8 @@ def main() -> Path:
89100
logging.info("Loading fallback corridor dataset.")
90101
corridors = load_corridors()
91102
export_regions = load_export_region_potential()
103+
conventional_benchmarks = load_conventional_ammonia_benchmarks()
104+
power_benchmarks = load_import_market_power_benchmarks()
92105

93106
# These notes are written into the HTML and lineage JSON. They make the
94107
# dashboard self-documenting when the standalone file is shared outside the
@@ -100,6 +113,7 @@ def main() -> Path:
100113
"Trade context: IEA notes ammonia dominates announced low-emissions hydrogen trade projects and Europe is a major target import market: https://www.iea.org/reports/global-hydrogen-review-2024/trade-and-infrastructure",
101114
"Singapore ammonia hub context: EMA and MPA are studying imported low- or zero-carbon ammonia for power generation and bunkering: https://www.mpa.gov.sg/media-centre/details/consortium-appointed-for-next-phase-of-study-on-low--or-zero-carbon-ammonia-power-generation-and-bunkering-solution",
102115
]
116+
source_notes.extend(benchmark_source_notes(conventional_benchmarks, power_benchmarks))
103117

104118
world_bank_indicators = None
105119
if args.skip_online:
@@ -145,6 +159,14 @@ def main() -> Path:
145159
source_notes.extend(geometry_notes)
146160
corridors = calculate_costs(corridors, assumptions)
147161
corridors = calculate_feasibility_scores(corridors, assumptions, weights)
162+
corridors = apply_power_benchmarks(corridors, power_benchmarks)
163+
corridors = apply_conventional_ammonia_benchmarks(
164+
corridors,
165+
conventional_benchmarks,
166+
assumptions,
167+
)
168+
corridors = calculate_ammonia_power_competitiveness(corridors, assumptions)
169+
corridors = calculate_break_even_metrics(corridors, assumptions)
148170

149171
# Processed CSV and lineage JSON are generated audit artifacts. They are
150172
# useful during analysis and local QA, but ignored from the public source

output/green_ammonia_trade_corridor_simulator.html

Lines changed: 653 additions & 254 deletions
Large diffs are not rendered by default.

src/benchmark_model.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
"""Benchmark comparisons for ammonia commodity competitiveness.
2+
3+
The corridor model calculates delivered green ammonia. This module adds the
4+
missing market-context layer: how that delivered cost compares with conventional
5+
grey ammonia, blue ammonia, and carbon-adjusted grey ammonia in the destination
6+
market. The result is not a price forecast; it is an auditable parity screen.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import pandas as pd
12+
13+
from src.config import EngineeringAssumptions
14+
from src.utils import clip_0_100, inverse_linear_score
15+
16+
17+
def _merge_by_importer(
18+
corridors: pd.DataFrame,
19+
benchmarks: pd.DataFrame,
20+
prefix: str,
21+
) -> pd.DataFrame:
22+
"""Merge benchmark rows by `corridors.importer` -> `benchmarks.market_key`."""
23+
24+
if benchmarks.empty:
25+
return corridors.copy()
26+
renamed = benchmarks.add_prefix(prefix)
27+
return corridors.merge(
28+
renamed,
29+
left_on="importer",
30+
right_on=f"{prefix}market_key",
31+
how="left",
32+
)
33+
34+
35+
def apply_conventional_ammonia_benchmarks(
36+
corridors: pd.DataFrame,
37+
conventional_benchmarks: pd.DataFrame,
38+
assumptions: EngineeringAssumptions,
39+
) -> pd.DataFrame:
40+
"""Attach conventional ammonia benchmarks and calculate green premiums."""
41+
42+
df = _merge_by_importer(corridors, conventional_benchmarks, "conv_")
43+
44+
# If a future corridor lacks a benchmark row, fall back to conservative
45+
# generic values so the dashboard still renders and the missing-data issue
46+
# remains visible through blank source fields.
47+
fallback_series = pd.Series(index=df.index, dtype=float)
48+
df["conv_grey_nh3_delivered_usd_per_tonne"] = pd.to_numeric(
49+
df.get("conv_grey_nh3_delivered_usd_per_tonne", fallback_series),
50+
errors="coerce",
51+
).fillna(430.0)
52+
df["conv_blue_nh3_delivered_usd_per_tonne"] = pd.to_numeric(
53+
df.get("conv_blue_nh3_delivered_usd_per_tonne", fallback_series),
54+
errors="coerce",
55+
).fillna(610.0)
56+
df["conv_grey_nh3_carbon_intensity_tco2_per_tonne"] = pd.to_numeric(
57+
df.get("conv_grey_nh3_carbon_intensity_tco2_per_tonne", fallback_series),
58+
errors="coerce",
59+
).fillna(assumptions.conventional_nh3_tco2_per_tonne_nh3)
60+
61+
# Prefer a market-specific carbon-price proxy from the power benchmark once
62+
# that table has been joined; otherwise use the global default assumption.
63+
carbon_price = pd.to_numeric(
64+
df.get("power_carbon_price_usd_per_tco2", fallback_series),
65+
errors="coerce",
66+
).fillna(assumptions.default_carbon_price_usd_per_tco2)
67+
df["carbon_price_usd_per_tco2"] = carbon_price
68+
69+
df["carbon_adjusted_grey_nh3_cost_usd_per_tonne"] = (
70+
df["conv_grey_nh3_delivered_usd_per_tonne"]
71+
+ carbon_price * df["conv_grey_nh3_carbon_intensity_tco2_per_tonne"]
72+
)
73+
df["green_premium_vs_grey_usd_per_tonne"] = (
74+
df["delivered_cost_usd_per_tonne_nh3"]
75+
- df["conv_grey_nh3_delivered_usd_per_tonne"]
76+
)
77+
df["green_premium_vs_carbon_adjusted_grey_usd_per_tonne"] = (
78+
df["delivered_cost_usd_per_tonne_nh3"]
79+
- df["carbon_adjusted_grey_nh3_cost_usd_per_tonne"]
80+
)
81+
df["green_premium_vs_blue_usd_per_tonne"] = (
82+
df["delivered_cost_usd_per_tonne_nh3"]
83+
- df["conv_blue_nh3_delivered_usd_per_tonne"]
84+
)
85+
avoided = df["conv_grey_nh3_carbon_intensity_tco2_per_tonne"].clip(lower=0.01)
86+
df["carbon_price_required_for_grey_parity_usd_per_tco2"] = (
87+
df["green_premium_vs_grey_usd_per_tonne"] / avoided
88+
).clip(lower=0.0)
89+
90+
df["commodity_competitiveness_score"] = df[
91+
"green_premium_vs_carbon_adjusted_grey_usd_per_tonne"
92+
].apply(
93+
lambda value: inverse_linear_score(
94+
value,
95+
assumptions.green_premium_score_best_usd_per_tonne_nh3,
96+
assumptions.green_premium_score_worst_usd_per_tonne_nh3,
97+
)
98+
)
99+
df["commodity_competitiveness_score"] = df["commodity_competitiveness_score"].apply(clip_0_100)
100+
return df
101+
102+
103+
def benchmark_source_notes(
104+
conventional_benchmarks: pd.DataFrame,
105+
power_benchmarks: pd.DataFrame,
106+
) -> list[str]:
107+
"""Summarize unique benchmark sources for dashboard source disclosure."""
108+
109+
notes: list[str] = []
110+
for label, frame in (
111+
("Conventional ammonia benchmark", conventional_benchmarks),
112+
("Import-market power benchmark", power_benchmarks),
113+
):
114+
if frame.empty:
115+
notes.append(f"{label}: unavailable; generic fallback values were used.")
116+
continue
117+
source_rows = (
118+
frame[["source_name", "source_url", "source_note", "data_quality"]]
119+
.drop_duplicates()
120+
.to_dict("records")
121+
)
122+
for row in source_rows:
123+
notes.append(
124+
f"{label}: {row['source_name']} ({row['data_quality']}) - "
125+
f"{row['source_note']} Source: {row['source_url']}"
126+
)
127+
return notes

src/config.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
FALLBACK_EXPORT_REGIONS_CSV = DATA_DIR / "fallback_export_regions.csv"
2626
REAL_SHIPPING_ROUTES_CSV = DATA_DIR / "real_shipping_routes.csv"
2727
SHIPPING_ROUTE_GEOMETRIES_JSON = DATA_DIR / "shipping_route_geometries.json"
28+
CONVENTIONAL_AMMONIA_BENCHMARKS_CSV = DATA_DIR / "conventional_ammonia_benchmarks.csv"
29+
IMPORT_MARKET_POWER_BENCHMARKS_CSV = DATA_DIR / "import_market_power_benchmarks.csv"
2830
COUNTRY_METADATA_CACHE = RAW_DIR / "country_metadata_restcountries.json"
2931
WORLD_BANK_INDICATOR_CACHE = RAW_DIR / "world_bank_corridor_indicators.json"
3032
DATA_LINEAGE_JSON = PROCESSED_DIR / "data_lineage.json"
@@ -81,6 +83,23 @@ class EngineeringAssumptions:
8183
cracking_cost_usd_per_kg_h2: float = 1.20
8284
conventional_nh3_tco2_per_tonne_nh3: float = 2.4
8385

86+
# End-use competitiveness assumptions. These turn delivered ammonia into
87+
# two decision-relevant comparisons: conventional ammonia parity and
88+
# ammonia-fired power parity against local electricity benchmarks.
89+
nh3_lhv_mwh_per_tonne: float = 5.17
90+
ammonia_power_generation_efficiency: float = 0.42
91+
ammonia_power_variable_om_usd_per_mwh: float = 12.0
92+
ammonia_power_nox_retrofit_adder_usd_per_mwh: float = 8.0
93+
default_carbon_price_usd_per_tco2: float = 100.0
94+
95+
# Benchmark-score anchors are wider than corridor delivered-cost anchors
96+
# because power conversion and carbon parity gaps can be much larger than
97+
# route-to-route differences in delivered ammonia cost.
98+
green_premium_score_best_usd_per_tonne_nh3: float = -50.0
99+
green_premium_score_worst_usd_per_tonne_nh3: float = 500.0
100+
power_gap_score_best_usd_per_mwh: float = -20.0
101+
power_gap_score_worst_usd_per_mwh: float = 260.0
102+
84103
# Scoring anchors convert engineering metrics into normalized 0-100 values.
85104
# They are not market forecasts; they define the screening comparison frame.
86105
delivered_cost_score_best_usd_per_tonne_nh3: float = 450.0

0 commit comments

Comments
 (0)