samuelrince's picture
fix: use same unit for min, mean and max
2605769
from dataclasses import dataclass
from enum import Enum
from ecologits.impacts.modeling import Impacts, Energy, GWP, ADPe, PE, WCF, Usage, Embodied
from pint import UnitRegistry, Quantity
import streamlit as st
import plotly.graph_objects as go
#####################################################################################
### UNITS DEFINITION
#####################################################################################
u = UnitRegistry()
u.define("Wh = watt_hour")
u.define("mWh = milliwatt_hour")
u.define("kWh = kilowatt_hour")
u.define("MWh = megawatt_hour")
u.define("GWh = gigawatt_hour")
u.define("TWh = terawatt_hour")
u.define("gCO2eq = gram")
u.define("mgCO2eq = milligram")
u.define("kgCO2eq = kilogram")
u.define("tCO2eq = metricton")
u.define("kgSbeq = kilogram")
u.define("gSbeq = gram")
u.define("mgSbeq = milligram")
u.define("µgSbeq = microgram")
u.define("kJ = kilojoule")
u.define("MJ = megajoule")
u.define("L = liter")
u.define("mL = milliliter")
u.define("m = meter")
u.define("km = kilometer")
u.define("s = second")
u.define("min = minute")
u.define("h = hour")
q = u.Quantity
@dataclass
class QImpacts:
energy: Quantity
gwp: Quantity
adpe: Quantity
pe: Quantity
wcf: Quantity
ranges: bool = False
energy_min: Quantity | None = None
energy_max: Quantity | None = None
gwp_min: Quantity | None = None
gwp_max: Quantity | None = None
adpe_min: Quantity | None = None
adpe_max: Quantity | None = None
pe_min: Quantity | None = None
pe_max: Quantity | None = None
wcf_min: Quantity | None = None
wcf_max: Quantity | None = None
class PhysicalActivity(str, Enum):
RUNNING = "running"
WALKING = "walking"
class EnergyProduction(str, Enum):
NUCLEAR = "nuclear"
WIND = "wind"
COUNTRIES = [
("cook_islands", 38.81, 9_556),
("tonga", 51.15, 104_490),
("comoros", 100, 821_632),
("samoa", 100, 821_632),
]
#####################################################################################
### EQUIVALENT RAW DATA
#####################################################################################
# From https://www.runningtools.com/energyusage.htm
RUNNING_ENERGY_EQ = q("294 kJ / km") # running 1 km at 10 km/h with a weight of 70 kg
WALKING_ENERGY_EQ = q("196 kJ / km") # walking 1 km at 3 km/h with a weight of 70 kg
# From https://selectra.info/energie/actualites/insolite/consommation-vehicules-electriques-france-2040
# and https://www.tesla.com/fr_fr/support/power-consumption
EV_ENERGY_EQ = q("0.17 kWh / km")
# From https://impactco2.fr/outils/comparateur?value=1&comparisons=streamingvideo
STREAMING_GWP_EQ = q("15.6 h / kgCO2eq")
# From https://ourworldindata.org/population-growth
ONE_PERCENT_WORLD_POPULATION = 80_000_000
DAYS_IN_YEAR = 365
# For a 900 MW nuclear plant -> 500 000 MWh / month
# From https://www.edf.fr/groupe-edf/espaces-dedies/jeunes-enseignants/pour-les-jeunes/lenergie-de-a-a-z/produire-de-lelectricite/le-nucleaire-en-chiffres
YEARLY_NUCLEAR_ENERGY_EQ = q("6 TWh")
# For a 2MW wind turbine
# https://www.ecologie.gouv.fr/eolien-terrestre
YEARLY_WIND_ENERGY_EQ = q("4.2 GWh")
# Ireland yearly electricity consumption
# From https://en.wikipedia.org/wiki/List_of_countries_by_electricity_consumption
YEARLY_IRELAND_ELECTRICITY_CONSUMPTION = q("33 TWh")
IRELAND_POPULATION_MILLION = 5
# From https://impactco2.fr/outils/comparateur?value=1&comparisons=&equivalent=avion-pny
# 1.77t for one passenger (round-trip) x 100 passenger
AIRPLANE_PARIS_NYC_GWP_EQ = q("177000 kgCO2eq")
#####################################################################################
### IMPACTS FORMATING
#####################################################################################
def format_energy(energy_value: float, energy_unit = Energy(value=0.).unit) -> Quantity:
val = q(energy_value, energy_unit)
if val < q("1 kWh"):
val = val.to("Wh")
if val < q("1 Wh"):
val = val.to("mWh")
return val
def format_gwp(gwp_value: float, gwp_unit = GWP(value=0.).unit) -> Quantity:
val = q(gwp_value, gwp_unit)
if val < q("1 kgCO2eq"):
val = val.to("gCO2eq")
if val < q("1 gCO2eq"):
val = val.to("mgCO2eq")
return val
def format_adpe(adpe_value: float, adpe_unit = ADPe(value=0.).unit) -> Quantity:
val = q(adpe_value, adpe_unit)
if val < q("1 kgSbeq"):
val = val.to("gSbeq")
if val < q("1 gSbeq"):
val = val.to("mgSbeq")
if val < q("1 mgSbeq"):
val = val.to("µgSbeq")
return val
def format_pe(pe_value: float, pe_unit = PE(value=0.).unit) -> Quantity:
val = q(pe_value, pe_unit)
if val < q("1 MJ"):
val = val.to("kJ")
return val
def format_wcf(wcf_value: float, wcf_unit = WCF(value=0.).unit) -> Quantity:
val = q(wcf_value, wcf_unit)
if val < q("1 L"):
val = val.to("mL")
return val
def format_impacts(impacts: Impacts) -> tuple[QImpacts, Usage, Embodied]:
if isinstance(impacts.energy.value, float):
return QImpacts(
energy=format_energy(impacts.energy.value),
gwp=format_gwp(impacts.gwp.value),
adpe=format_adpe(impacts.adpe.value),
pe=format_pe(impacts.pe.value),
wcf=format_wcf(impacts.wcf.value)
), impacts.usage, impacts.embodied
else:
energy = format_energy(impacts.energy.value.mean)
gwp = format_gwp(impacts.gwp.value.mean)
adpe = format_adpe(impacts.adpe.value.mean)
pe = format_pe(impacts.pe.value.mean)
wcf = format_wcf(impacts.wcf.value.mean)
return QImpacts(
energy=energy,
energy_min=format_energy(impacts.energy.value.min).to(energy.units),
energy_max=format_energy(impacts.energy.value.max).to(energy.units),
gwp=gwp,
gwp_min=format_gwp(impacts.gwp.value.min).to(gwp.units),
gwp_max=format_gwp(impacts.gwp.value.max).to(gwp.units),
adpe=adpe,
adpe_min=format_adpe(impacts.adpe.value.min).to(adpe.units),
adpe_max=format_adpe(impacts.adpe.value.max).to(adpe.units),
pe=pe,
pe_min=format_pe(impacts.pe.value.min).to(pe.units),
pe_max=format_pe(impacts.pe.value.max).to(pe.units),
wcf=wcf,
wcf_min=format_wcf(impacts.wcf.value.min).to(wcf.units),
wcf_max=format_wcf(impacts.wcf.value.max).to(wcf.units),
ranges=True
), impacts.usage, impacts.embodied
#####################################################################################
### EQUIVALENT FORMATING
#####################################################################################
def format_energy_eq_physical_activity(
energy: Quantity,
) -> tuple[PhysicalActivity, Quantity]:
energy = energy.to("kJ")
running_eq = energy / RUNNING_ENERGY_EQ
if running_eq > q("1 km"):
return PhysicalActivity.RUNNING, running_eq
walking_eq = energy / WALKING_ENERGY_EQ
if walking_eq < q("1 km"):
walking_eq = walking_eq.to("meter")
return PhysicalActivity.WALKING, walking_eq
def format_energy_eq_electric_vehicle(energy: Quantity) -> Quantity:
energy = energy.to("kWh")
ev_eq = energy / EV_ENERGY_EQ
if ev_eq < q("1 km"):
ev_eq = ev_eq.to("meter")
return ev_eq
def format_gwp_eq_streaming(gwp: Quantity) -> Quantity:
gwp = gwp.to("kgCO2eq")
streaming_eq = gwp * STREAMING_GWP_EQ
if streaming_eq < q("1 h"):
streaming_eq = streaming_eq.to("min")
if streaming_eq < q("1 min"):
streaming_eq = streaming_eq.to("s")
return streaming_eq
def format_energy_eq_electricity_production(
energy: Quantity,
) -> tuple[EnergyProduction, Quantity]:
electricity_eq = energy * ONE_PERCENT_WORLD_POPULATION * DAYS_IN_YEAR
electricity_eq = electricity_eq.to("TWh")
if electricity_eq > YEARLY_NUCLEAR_ENERGY_EQ:
return EnergyProduction.NUCLEAR, electricity_eq / YEARLY_NUCLEAR_ENERGY_EQ
electricity_eq = electricity_eq.to("GWh")
return EnergyProduction.WIND, electricity_eq / YEARLY_WIND_ENERGY_EQ
def format_energy_eq_electricity_consumption_ireland(energy: Quantity) -> Quantity:
electricity_eq = energy * ONE_PERCENT_WORLD_POPULATION * DAYS_IN_YEAR
electricity_eq = electricity_eq.to("TWh")
return electricity_eq / YEARLY_IRELAND_ELECTRICITY_CONSUMPTION
def format_gwp_eq_airplane_paris_nyc(gwp: Quantity) -> Quantity:
gwp_eq = gwp * ONE_PERCENT_WORLD_POPULATION * DAYS_IN_YEAR
gwp_eq = gwp_eq.to("kgCO2eq")
return gwp_eq / AIRPLANE_PARIS_NYC_GWP_EQ
#####################################################################################
### VISUALIZATIONS
#####################################################################################
def range_plot (mean_val, min_val, max_val, unit):
fig = go.Figure()
# Background bar
fig.add_trace(go.Bar(
x=[max_val],
y=[''],
orientation='h',
marker=dict(color="#0B3B36"),
showlegend=False,
hoverinfo='skip',
))
# Vertical line
fig.add_shape(
type="line",
x0=mean_val, y0=-1,
x1=mean_val, y1=1,
line=dict(color='#00BF63', width=3, dash="solid"),
#name="Average"
)
# Add labels
for val, pos, text in zip([max_val, min_val]*2,[0.85,0.85,1.6,1.6], ["Max", "Min", f'{max_val:.3g} {unit}', f'{min_val:.3g} {unit}']):
fig.add_annotation(
x=val,
y=-pos,
text=text,
showarrow=False,
font=dict(color="black", size=16)
)
fig.add_annotation(
x=mean_val,
y=1.65,
text=f'{mean_val:.3g} {unit}',
showarrow=False,
font=dict(color="black", size=35)
)
# Layout adjustments
fig.update_layout(
height=160,
width = 400,
xaxis=dict(range=[min_val, max_val], showgrid=False, showticklabels=False),
yaxis=dict(showticklabels=False),
plot_bgcolor='white',
margin=dict(l=100, r=100, t=0, b=20),
showlegend=False
)
# Show the plot in Streamlit
st.plotly_chart(fig, use_container_width=True)