Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 0 additions & 4 deletions TEMP/.gitignore

This file was deleted.

23 changes: 8 additions & 15 deletions pvdeg/degradation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1478,26 +1478,21 @@ def acetic_acid_generation(
Ea_gen: float = 90.0,
T_ref: float = 27.0,
encapsulant: str = "AA002",
rh_encapsulant: pd.Series = None,
) -> pd.Series:
"""Calculate the acetic acid generation rate in EVA encapsulant.

Uses an Arrhenius model for the hydrolysis source term of ethylene-vinyl
acetate (EVA), scaled by both temperature and (optionally) relative humidity.
The rate at reference temperature ``T_ref`` and reference humidity (85% RH)
is scaled via:
acetate (EVA). The rate at reference temperature ``T_ref`` is scaled via:

.. math::

R(T, RH) = R_0 \\cdot \\exp\\!\\left[
R(T) = R_0 \\cdot \\exp\\!\\left[
\\frac{-E_a}{R}\\left(\\frac{1}{T} - \\frac{1}{T_{ref}}\\right)
\\right] \\cdot \\frac{RH}{85}
\\right]

The baseline ``Ro`` is calibrated from Kempe et al. (2007) under damp heat
conditions (85°C/85% RH), so it implicitly assumes moisture is present in
the EVA. If ``rh_encapsulant`` is provided, the generation rate is scaled
linearly by relative humidity; otherwise the rate is assumed at 85% RH
(nominal for multi-layer stack moisture transport).
The baseline ``Ro`` is calibrated from Kempe et al. (2007) under damp-heat
conditions (85°C/85% RH), so this implementation assumes that humidity basis
implicitly and models temperature dependence directly.

Validation: Gnocchi et al. (2018) [2]_.

Expand All @@ -1506,17 +1501,15 @@ def acetic_acid_generation(
temp_module : pd.Series
Time-indexed module temperature [°C].
Ro : float, default 0.00331
Acetic acid source term at ``T_ref`` and 85% RH [ng/min/g].
Acetic acid source term at ``T_ref`` under the 85% RH calibration
basis [ng/min/g].
Ea_gen : float, default 90.0
Activation energy for HAc generation [kJ/mol].
T_ref : float, default 27.0
Reference temperature for ``Ro`` [°C].
encapsulant : str, default ``"AA002"``
Key in ``AApermeation.json`` from which to load default parameters.
Set to ``None`` to use explicitly provided values only.
rh_encapsulant : pd.Series, optional
Time-indexed relative humidity in the encapsulant [%]. If provided,
scales generation rate by (RH / 85). If ``None``, assumes 85% RH.

Returns
-------
Expand Down
30 changes: 11 additions & 19 deletions pvdeg/humidity.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,8 @@ def water_saturation_pressure(temp, average=True):
def water_vapor_pressure(
weather_df,
meta=None,
temp_air=None,
relative_humidity=None,
temperature=None,
rh=None,
):
"""Compute the water vapour partial pressure P_H2O.

Expand All @@ -162,30 +162,22 @@ def water_vapor_pressure(
[%] when the override arguments are not supplied.
meta : dict, optional
Location metadata. Not used; kept for pipeline compatibility.
temp_air : pd.Series, optional
Temperature [°C]. When provided, takes precedence over
``weather_df['temp_air']``. Use to inject an upstream pipeline
result (e.g. module surface temperature from
``pvdeg.temperature.module``).
relative_humidity : pd.Series, optional
Relative humidity [%]. When provided, takes precedence over
``weather_df['relative_humidity']``. Use to inject an upstream
pipeline result (e.g. module-surface RH from
``pvdeg.humidity.surface_relative``).
temperature : pd.Series, optional
Temperature [°C] at the location where vapour pressure is evaluated.
When provided, takes precedence over ``weather_df['temp_air']``.
rh : pd.Series, optional
Relative humidity [%] at the same location as ``temperature``.
When provided, takes precedence over ``weather_df['relative_humidity']``.

Returns
-------
pd.Series
Time-indexed water vapour partial pressure P_H2O [kPa].
"""
t = temp_air if temp_air is not None else weather_df["temp_air"]
rh = (
relative_humidity
if relative_humidity is not None
else weather_df["relative_humidity"]
)
t = temperature if temperature is not None else weather_df["temp_air"]
rh_input = rh if rh is not None else weather_df["relative_humidity"]
P_sat, _ = water_saturation_pressure(t)
return (rh / 100.0) * P_sat
return (rh_input / 100.0) * P_sat


def surface_relative(rh_ambient, temp_ambient, temp_module):
Expand Down
212 changes: 144 additions & 68 deletions tutorials/04_scenario/05_scenario_perovskite_sequential.ipynb

Large diffs are not rendered by default.

1,784 changes: 1,652 additions & 132 deletions tutorials/04_scenario/06_scenario_perovskite_ey.ipynb

Large diffs are not rendered by default.

28 changes: 14 additions & 14 deletions tutorials/04_scenario/07_scenario_module_stack.ipynb

Large diffs are not rendered by default.

160 changes: 131 additions & 29 deletions tutorials/04_scenario/08_scenario_backsheet_comparison.ipynb

Large diffs are not rendered by default.

58 changes: 30 additions & 28 deletions tutorials/04_scenario/scripts/05_scenario_perovskite_sequential.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# %% [markdown]
# # Perovskite Degradation Kinetics
#
# Demonstrates the Scenario pipeline using the **Siegler et al. (2022)** material-level kinetic model for MAPbI₃ perovskite degradation.
# Demonstrates the Scenario pipeline using the **Siegler et al. (2022)** material-level kinetic model for MAPbI₃ perovskite degradation.
#
# 1. **Multiple jobs in one pipeline** module temperature, surface humidity, and perovskite degradation rate all in a single `run()` call.
# 2. **Sequential job chaining (`depends_on`)** downstream jobs receive the output of upstream jobs as named keyword arguments at run time.
# 3. **Four-pathway kinetic rate** [mol m⁻² s⁻¹] for MAPbI₃: WPO, DPO, humidity-driven, and thermal pathways.
# 1. **Multiple jobs in one pipeline** — module temperature, surface humidity, and perovskite degradation rate all in a single `run()` call.
# 2. **Sequential job chaining (`depends_on`)** — downstream jobs receive the output of upstream jobs as named keyword arguments at run time.
# 3. **Four-pathway kinetic rate** [mol m⁻² s⁻¹] for MAPbI₃: WPO, DPO, humidity-driven, and thermal pathways.
#
# > **See also:** `06_scenario_perovskite_ey.ipynb` device-level Zhao/Orooji model, CE degradation factor, and energy yield prediction with pvlib.
# > **See also:** `06_scenario_perovskite_ey.ipynb` — device-level Zhao/Orooji model, CE degradation factor, and energy yield prediction with pvlib.

# %% [markdown]
# ## 1. Imports and data
Expand Down Expand Up @@ -42,7 +42,9 @@
with open(os.path.join(TUTORIALS_DATA, mf)) as _f:
mt = json.load(_f)
all_weather[loc], all_meta[loc] = df, mt
print(f" {loc}: {len(df)} rows ({mt['latitude']:.2f}°N, {mt['longitude']:.2f}°E)")
print(
f" {loc}: {len(df)} rows ({mt['latitude']:.2f}°N, {mt['longitude']:.2f}°E)"
)

# Single-location aliases used by demonstration cells below
weather_df = all_weather["Golden, CO"]
Expand All @@ -52,7 +54,7 @@

# %% [markdown]
# ---
# ## 2. Single job perovskite degradation rate
# ## 2. Single job — perovskite degradation rate
#
# The simplest use: one job, no sequential dependencies.
#
Expand All @@ -66,7 +68,7 @@
# %% [markdown]
# ### 2a. Load parameters from the database
#
# `pvdeg.utilities.get_kinetics("D015")` returns D015 as a flat dict the same values the function uses as defaults, but now sourced from the single authoritative location in `DegradationDatabase.json`.
# `pvdeg.utilities.get_kinetics("D015")` returns D015 as a flat dict — the same values the function uses as defaults, but now sourced from the single authoritative location in `DegradationDatabase.json`.
# Pass it to the `parameters` argument to use the DB as the source of truth.

# %%
Expand Down Expand Up @@ -122,36 +124,36 @@

fig, axes = plt.subplots(2, 1, figsize=(12, 7))

# Hourly rate Golden, CO (raw; demonstrates signal structure)
# Hourly rate — Golden, CO (raw; demonstrates signal structure)
rate_default.plot(ax=axes[0], color="steelblue", alpha=0.7)
axes[0].set_ylabel("mol m⁻² s⁻¹")
axes[0].set_title("Perovskite degradation rate total (Golden, CO, hourly)")
axes[0].set_ylabel("mol m⁻² s⁻¹")
axes[0].set_title("Perovskite degradation rate — total (Golden, CO, hourly)")

# Weekly mean rate all locations
# Weekly mean rate — all locations
for loc, rate in _all_rates.items():
rate.resample("W").mean().plot(
ax=axes[1], label=loc, color=_colors[loc], alpha=0.85
)
axes[1].set_ylabel("mol m⁻² s⁻¹")
axes[1].set_title("Perovskite degradation rate weekly mean, all locations")
axes[1].set_ylabel("mol m⁻² s⁻¹")
axes[1].set_title("Perovskite degradation rate — weekly mean, all locations")
axes[1].legend()

fig.tight_layout()

# %% [markdown]
# ---
# ## 3. Sequential analysis degradation rate as a downstream job
# ## 3. Sequential analysis — degradation rate as a downstream job
#
# Each job feeds the next via `depends_on`.
# The pipeline injects named upstream outputs as keyword arguments at run time no manual wiring needed.
# The pipeline injects named upstream outputs as keyword arguments at run time — no manual wiring needed.
#
# **Chain:**
# | Step | Function | Output name | Depends on |
# |------|----------|-------------|------------|
# | 1 | `temperature.module` | `"temp_mod"` | |
# | 2 | `humidity.surface_relative` | `"rh_surface"` | `temp_module` `"temp_mod"` |
# | 3 | `humidity.water_vapor_pressure` | `"P_H2O"` | `temp_air` ← `"temp_mod"`, `relative_humidity` ← `"rh_surface"` |
# | 4 | `degradation.perovskite_degradation` | `"perov_rate"` | `P_H2O` `"P_H2O"` |
# | 1 | `temperature.module` | `"temp_mod"` | — |
# | 2 | `humidity.surface_relative` | `"rh_surface"` | `temp_module` ← `"temp_mod"` |
# | 3 | `humidity.water_vapor_pressure` | `"P_H2O"` | `temperature` ← `"temp_mod"`, `rh` ← `"rh_surface"` |
# | 4 | `degradation.perovskite_degradation` | `"perov_rate"` | `P_H2O` ← `"P_H2O"` |
#
# Step 2 converts ambient RH to **module-surface RH** using the module temperature.
# Step 3 converts that surface RH (and module temperature) into the **water vapour partial pressure** the kinetic model requires.
Expand All @@ -163,13 +165,13 @@
meta_data=meta,
)

# Job 1 module surface temperature
# Job 1 — module surface temperature
s2.addJob(
func=pvdeg.temperature.module,
name="temp_mod",
)

# Job 2 module-surface RH; uses module temperature from Job 1
# Job 2 — module-surface RH; uses module temperature from Job 1
s2.addJob(
func=(
pvdeg.humidity.surface_relative,
Expand All @@ -182,14 +184,14 @@
depends_on={"temp_module": "temp_mod"},
)

# Job 3 water vapour pressure at module surface; uses temp and RH from Jobs 1 & 2
# Job 3 — water vapour pressure at module surface; uses temp and RH from Jobs 1 & 2
s2.addJob(
func=pvdeg.humidity.water_vapor_pressure,
name="P_H2O",
depends_on={"temp_air": "temp_mod", "relative_humidity": "rh_surface"},
depends_on={"temperature": "temp_mod", "rh": "rh_surface"},
)

# Job 4 perovskite degradation rate; uses P_H2O from Job 3
# Job 4 — perovskite degradation rate; uses P_H2O from Job 3
s2.addJob(
func=pvdeg.degradation.perovskite_degradation,
name="perov_rate",
Expand All @@ -206,10 +208,10 @@
perov = s2.results["perov_rate"]

fig, axes = plt.subplots(4, 1, figsize=(12, 10), sharex=True)
temp_mod.plot(ax=axes[0], title="Module temperature [°C]")
temp_mod.plot(ax=axes[0], title="Module temperature [°C]")
rh_surf.plot(ax=axes[1], title="Module-surface relative humidity [%]")
p_h2o.plot(ax=axes[2], title="Water vapour pressure P_H2O [kPa]")
perov.plot(ax=axes[3], title="Perovskite degradation rate total [mol m⁻² s⁻¹]")
perov.plot(ax=axes[3], title="Perovskite degradation rate — total [mol m⁻² s⁻¹]")
fig.tight_layout()

# %% [markdown]
Expand Down Expand Up @@ -238,7 +240,7 @@
{comp: s3.results[comp] for comp in ("WPO", "DPO", "r_hum", "r_therm")}
)
components_df.plot(
title="Perovskite degradation pathways [mol m⁻² s⁻¹]",
title="Perovskite degradation pathways [mol m⁻² s⁻¹]",
figsize=(12, 4),
logy=True,
)
23 changes: 12 additions & 11 deletions tutorials/04_scenario/scripts/06_scenario_perovskite_ey.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
# Keeps the repo working directory free of pvd_job_* clutter.
pvdeg.config.SCENARIO_OUTPUT_PATH = tempfile.gettempdir()

# ── Lifetime threshold ────────────────────────────────────────────────────────
# Lifetime threshold
# Change this to analyse a different degradation endpoint. Plots, prints, map colors will correspond to this setting e.g. 0.90 for T90.
T_THRESHOLD = 0.90
T_LABEL = f"T{int(T_THRESHOLD * 100)}" # e.g. "T90" or "T80"
Expand Down Expand Up @@ -382,21 +382,22 @@ def run_ce_multiyear(wdf, mt, n_years=N_YEARS):
# investment?" — the same metric reported by Orooji et al. (2026).
#
# > **Why not DF_total?**
# > $DF_{total}$ is the *instantaneous* CE at a given hour (device-level metric).
# > It crosses the threshold much earlier than $PR_{Agg}$ because it ignores all the energy
# > already delivered when the device was at high efficiency.
# > Phoenix example (T₉₀): $DF_{total}$ ≈ 7–8 months vs $PR_{Agg}$ ≈ 24–28 months.
# > Both are stored in `df_map`; hover over a state to compare them.
# >
# >$DF_{total}$ is the *instantaneous* CE at a given hour, $PR_{Agg}$ is the averaged energy
# yield over the whole operating period. $DF_{total}$ PR_agg T90 compares all energy actually
# produced to all energy a non-degrading device would have produced over the same period
# Phoenix example (T₉₀): $DF_{total}$ ≈ 7–8 months vs $PR_{Agg}$ ≈ 24–28 months.
# Both are stored in `df_map`; hover over a state to compare them.
#
# **Colour scale:** green = long lifetime (good), red = short lifetime (bad).
#

# %%
# ── Fetch lifetime data for all 50 US states via NSRDB PSM4 API ──────────────
# Fetch lifetime data for all 50 US states via NSRDB PSM4 API
# Get a free key at https://developer.nrel.gov/signup/

NREL_API_KEY = "DEMO_API_KEY"
NREL_EMAIL = "DEMO_EMAIL"
API_KEY = "7SNYG6jZv3arhg1hKj1zWI0Y9gO1mXUBvhsK2b1J"
EMAIL = "rajiv.daxini@nlr.gov"

# abbr: (api_lat, api_lon, label, centre_lat, centre_lon)
STATES = {
Expand Down Expand Up @@ -458,8 +459,8 @@ def run_ce_multiyear(wdf, mt, n_years=N_YEARS):
wdf, mt = pvdeg.weather.get(
database="PSM4",
id=(lat, lon),
api_key=NREL_API_KEY,
email=NREL_EMAIL,
api_key=API_KEY,
email=EMAIL,
year="tmy",
map_variables=True,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
# ## 1. Setup

# %%
# NBVAL_IGNORE_OUTPUT
import os
import json
import tempfile
Expand Down
Loading