Framework for modelling anion and proton exchange membrane electrochemical cell devices, such as water electrolyzers and fuel cells.
Project description
marapendi
marapendi is a Python framework for physics-based modelling of proton-exchange membrane (PEM) fuel cells.
The model is zero-dimensional (single operating point). Both steady-state and transient formulations are provided. The steady-state solver computes cell voltage as a function of current density, accounting for activation, ohmic and mass-transport losses, membrane water balance, and two-phase liquid-water transport in the porous layers. The transient solver integrates coupled ODEs for the MEA temperature and membrane water-content profile under time-varying operating conditions.
marapendi offers:
- A basis for building fuel cell and electrolyzer models — pre-defined
correlations and sub-models for heat transfer, reaction kinetics,
two-phase transport, membrane water balance, ohmic losses, and more (see
docs/science/), with a focus on 0D PEM/AEM models. Sub-models are ordinary Python classes, so overriding a single correlation or swapping a whole sub-model is a matter of subclassing. - Fast, practical calibration, validation and simulation — ready-to-use, fast steady-state and transient 0D cell models, low-cost enough to make sensitivity analysis, parameter estimation and cross-validation practical, through an easy-to-use API for defining, calibrating and simulating cell models in a few lines of code (see the Quick start below).
- Open, readable and well-documented code — multiple runnable examples demonstrating marapendi's capabilities (see Examples below), backed by detailed documentation and openly readable, commented code designed for transparency and easy understanding.
- Cross-platform support — a transient 0D model available as a MATLAB/Simulink S-function block (see MATLAB / Simulink below).
Features
- PEM fuel cell — polarization curves from first principles (Butler-Volmer kinetics, Nernst equation, membrane water transport, GDL/MPL liquid saturation)
- Two steady-state model variants —
ExplicitSteadyStateModel(one forward pass, fast) andImplicitSteadyStateModel(self-consistent MEA temperature via vectorised secant iteration); both accept full current-density arrays in a single call - Piecewise-linear membrane water balance —
MembraneWaterBalanceModelPiecewisefits the PFSA equilibrium isotherm RH(λ) with a piecewise-linear regression; this is the default model used in the steady-state solvers - Transient model —
TransientModelintegrates coupled MEA-temperature and membrane water-content ODEs (Ferrara et al., 2018) under time-varying conditions viascipy.integrate.solve_ivp;solve()auto-attaches adiagnosticsCellState(voltage, HFR, water contents, saturation) at each internal time step, andevaluate()lets you re-sample any dense-output trajectory at arbitrary times - Parameter estimation —
SteadyStatePolarizationCurveCalibrationfits kinetic and transport parameters to experimental polarization and HFR data using differential-evolution global optimisation, with k-fold cross-validation and automatic model-complexity selection - Clean component/state separation — static cell parameters and runtime state are kept strictly separate, making the data flow explicit
- MATLAB/Simulink block —
TransientPEMFC(matlab/transient_pemfc/) drivesTransientModelfrom Simulink, calling the live Python source at every solver step; cell parameters can be supplied as a dotted Python builder path or a plain MATLAB struct (seematlab/transient_pemfc/README.md)
Installation
Requires Python 3.10+.
From PyPI
pip install marapendi
The simplest way to install marapendi as a library. See
docs/installation.rst for installing straight
from GitLab (no clone) or from a local clone for development.
With conda (recommended)
conda env create -f ci/conda_env.yml
conda activate marapendi
pip install -e .
With pip
pip install -e .
Quick start
import numpy as np
import marapendi as mrpd
# --- Build the cell ---
liq = mrpd.DarcyTransportModel(J_function_exponent=2)
cell = mrpd.FuelCell(
area=25e-4,
electric_resistance=30e-7,
ca=mrpd.FuelCellSide(
cl=mrpd.PtCCatalystLayer(
ecsa=70e3, platinum_loading=0.4e-2, ionomer=mrpd.PFSAIonomer(),
reaction=mrpd.ElectrochemicalReaction(
reference_exchange_current_density=2.5e-4,
reaction_order=0.54, activation_energy=67e6,
reference_activity=1e5, reference_temperature=353.15,
number_of_electrons=2, charge_transfer_coeff=0.5,
),
thickness=10e-6, two_phase_transport_model=liq,
),
gdl=mrpd.GasDiffusionLayer(
thickness=200e-6, porosity=0.6, contact_angle=120.,
tortuosity=2.0, absolute_permeability=1e-12,
thermal_conductivity=0.5, two_phase_transport_model=liq,
),
ch=mrpd.FlowChannel(width=1e-3, height=1e-3, length=0.1, n_parallel=20, reactant='o2'),
thermal_contact_resistance=4e-4,
),
an=mrpd.FuelCellSide(
cl=mrpd.PtCCatalystLayer(thickness=5e-6, two_phase_transport_model=liq),
gdl=mrpd.GasDiffusionLayer(
thickness=200e-6, porosity=0.6, contact_angle=120.,
tortuosity=2.0, absolute_permeability=1e-12,
thermal_conductivity=0.5, two_phase_transport_model=liq,
),
ch=mrpd.FlowChannel(width=1e-3, height=1e-3, length=0.1, n_parallel=20, reactant='h2'),
thermal_contact_resistance=4e-4,
),
membrane=mrpd.PFSA(
ionomer=mrpd.PFSAIonomer(equivalent_weight=1100, dry_density=1980),
dry_thickness=25e-6,
),
)
# --- Operating conditions ---
T = 353.15
conditions = mrpd.CellConditions(
current_density=np.linspace(1e3, 2e4, 20), # A/m²
cell_temperature=T,
ca=mrpd.SideConditions(
inlet_temperature=T, outlet_pressure=1.5e5,
dry_o2_mole_fraction=0.21, inlet_relative_humidity=0.5, stoichiometry=2.0,
),
an=mrpd.SideConditions(
inlet_temperature=T, outlet_pressure=1.5e5,
dry_h2_mole_fraction=1.0, inlet_relative_humidity=0.5, stoichiometry=1.5,
),
)
# --- Polarization curve (explicit model, one vectorised call) ---
model = mrpd.ExplicitSteadyStateModel()
state = model.set_initial_conditions(cell, conditions)
state = model.solve(cell, conditions, state)
# state.cell_voltage — array of voltages (V)
# state.mea_temperature — array of MEA temperatures (K)
# --- Implicit model (self-consistent T_MEA, fully vectorised) ---
imp_model = mrpd.ImplicitSteadyStateModel()
state = imp_model.set_initial_conditions(cell, conditions)
state = imp_model.solve(cell, conditions, state)
# --- Transient model (time-varying conditions) ---
from marapendi.models.base.transient import TransientModel
cond_0 = mrpd.CellConditions( # single operating point for initial conditions
current_density=np.atleast_1d(1e4),
cell_temperature=T,
ca=mrpd.SideConditions(inlet_temperature=T, outlet_pressure=1.5e5,
dry_o2_mole_fraction=0.21, stoichiometry=2.0,
inlet_relative_humidity=0.5),
an=mrpd.SideConditions(inlet_temperature=T, outlet_pressure=1.5e5,
dry_h2_mole_fraction=1.0, stoichiometry=1.5,
inlet_relative_humidity=0.5),
)
tr_model = TransientModel(n_memb_mesh=5)
sol = tr_model.solve(cell, cond_0, t_span=(0, 3600))
# sol.y[0] → T_MEA(t) [K]
# sol.y[1:] → λ(ξ, t) [mol H2O / mol site]
# sol.diagnostics → CellState with .cell_voltage, .hfr, .membrane.water_content, …
# Pass a callable for time-varying conditions:
def conditions_t(t):
i = 5e3 if t < 1800 else 2e4 # step change at 30 min
return mrpd.CellConditions(current_density=np.atleast_1d(i), cell_temperature=T,
ca=cond_0.ca, an=cond_0.an)
sol = tr_model.solve(cell, conditions_t, t_span=(0, 3600))
Package structure
src/marapendi/
├── components/ # Component tree (no physics) — geometry/material dataclasses
│ ├── cell/
│ │ ├── cell.py # Cell, CellSide (base classes)
│ │ └── fuelcell.py # FuelCell, FuelCellSide
│ ├── membrane/
│ │ ├── ionomer_base.py # Ionomer (abstract base class)
│ │ ├── pem.py # PFSAIonomer, PFSA, NafionD2020
│ │ ├── aem.py # PAPIonomer, AEM, PAP85
│ │ └── membrane_base.py # Membrane (composes an Ionomer instance)
│ ├── porous_layers/ # GasDiffusionLayer, MicroPorousLayer, PtCCatalystLayer, …
│ ├── channel/ # FlowChannel, ChannelGasResistanceModel, BakerChannelGasResistanceModel
│ └── electrolyte/ # ElectrolyteSolution, KOHSolution
├── simulation/ # Runtime state and operating conditions (pure data)
│ ├── state.py # CellState, CellSideState, LayerState, GasState, GasFlowState, …
│ ├── conditions.py # CellConditions, SideConditions
│ └── load_cycles/ # LoadCycle, standardised ID-FAST/FC-DLC driving cycles
├── models/ # Physics: orchestration + sub-models
│ ├── base/
│ │ ├── explicit_steady_state.py # ExplicitSteadyStateModel
│ │ ├── implicit_steady_state.py # ImplicitSteadyStateModel (self-consistent T_MEA)
│ │ └── transient.py # TransientModel (ODE for T_MEA + membrane water profile)
│ ├── water_balance/
│ │ ├── water_balance.py # WaterBalanceModel (orchestration)
│ │ ├── membrane_pwl.py # MembraneWaterBalanceModelPiecewise (default)
│ │ └── membrane.py # MembraneWaterBalanceModel (Affonso Nobrega et al. 2026)
│ ├── thermo/ # species/molecular-weight constants, water properties
│ ├── thermal.py # ThermalModel
│ ├── voltage.py # VoltageModel
│ ├── darcy.py # DarcyTransportModel (two-phase liquid water)
│ ├── diffusion.py # PorousGasDiffusionModel (Fickian + Knudsen)
│ └── gas_transport_resistance.py # GasTransportModel (channel-to-CL resistance network)
├── degradation/ # PtDissolution, PlatinumOxideFormation
├── estimation/ # BaseModelCalibration, SteadyStatePolarizationCurveCalibration, Parameter
└── interop/ # simulink_bridge.py — thin adapter for the MATLAB Simulink block
Examples
Each example is a self-contained, runnable Python script under examples/,
rendered with its output plots at docs/auto_examples/ when the docs are
built (see Documentation below).
| Script | Description |
|---|---|
| examples/plot_01_polarization_curve.py | Assemble a cell, simulate a polarization curve, plot V–i and HFR |
| examples/plot_02_quasi_steady.py | Replay a test-bench log with a vectorised quasi-steady simulation; compare simulated and measured voltage over time |
| examples/plot_03_implicit_vs_explicit.py | Compare explicit and implicit steady-state models on voltage, MEA temperature, and HFR |
| examples/plot_04_transient.py | Transient simulation over an ID-FAST driving cycle; compare against the quasi-steady-state prediction |
| examples/plot_05_multi_condition.py | Simulate and compare polarization curves across multiple experimental conditions |
| examples/plot_06_parameter_estimation.py | Fit kinetic and transport parameters to multi-condition experimental data; k-fold cross-validation and complexity selection |
| examples/plot_07_pwl_membrane.py | Compare the piecewise-linear (standard) and paper-model (Affonso Nobrega et al. 2026) membrane water balance models |
| examples/plot_08_water_balance.py | Water balance vs. cell temperature: theoretical water production against modelled inlet/outlet water flows |
| examples/plot_09_membrane_correlations.py | Plot membrane/ionomer correlations (conductivity, isotherm, permeability, diffusivity) vs. water content at several temperatures |
MATLAB / Simulink
matlab/transient_pemfc/ builds a TransientPEMFC Simulink block that calls
TransientModel directly through MATLAB's py. interface — no physics is
re-implemented in MATLAB, so the block always reflects whatever is on disk in
src/marapendi/. See matlab/transient_pemfc/README.md and the
Simulink block guide for setup and usage.
Running the tests
pytest
Documentation
Build the Sphinx docs from the docs/ directory:
cd docs
make html
open _build/html/index.html
Start at docs/installation.rst for a runnable
quick-start snippet, docs/user_guide/ for task-oriented
guides, and docs/science/ for the governing equations and
literature references behind every model.
Reference
Pedro Affonso Nobrega, Christophe Morin, Anass Chabani, Mathias Herlé, "A zero-dimensional PEM fuel cell model with self-consistent MEA temperature and membrane water balance", J. Electrochem. Soc. 173, 114503 (2026).
Author
Pedro Affonso Nobrega, pedro.affonso_nobrega@minesparis.psl.eu
License
MIT License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file marapendi-0.1.0.tar.gz.
File metadata
- Download URL: marapendi-0.1.0.tar.gz
- Upload date:
- Size: 129.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
69f29cc99f9232470193645d48b6e45b628c352957f85d4482e80786328701cf
|
|
| MD5 |
7cea0e2819b014c10443978c089e5067
|
|
| BLAKE2b-256 |
4b7de60e5dec5a91d97facec966779f10847de4b745f31134aec0b5da28e0c7d
|
Provenance
The following attestation bundles were made for marapendi-0.1.0.tar.gz:
Publisher:
publish.yml on affonsop/marapendi
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
marapendi-0.1.0.tar.gz -
Subject digest:
69f29cc99f9232470193645d48b6e45b628c352957f85d4482e80786328701cf - Sigstore transparency entry: 2182333092
- Sigstore integration time:
-
Permalink:
affonsop/marapendi@a02e8200e836108e28fe871456878357c9ca162b -
Branch / Tag:
refs/tags/v0.4.1 - Owner: https://github.com/affonsop
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a02e8200e836108e28fe871456878357c9ca162b -
Trigger Event:
push
-
Statement type:
File details
Details for the file marapendi-0.1.0-py3-none-any.whl.
File metadata
- Download URL: marapendi-0.1.0-py3-none-any.whl
- Upload date:
- Size: 119.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2b9665dfb4b692fac7501d9ee77af6ca97ea548848c9d0659b6c963768c10124
|
|
| MD5 |
3a3cee15ad765713f94fe5a3994dd95d
|
|
| BLAKE2b-256 |
63cc487af58b18eaf2c344ce32c605d2b09c304a510981d4fc830fad4867135d
|
Provenance
The following attestation bundles were made for marapendi-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on affonsop/marapendi
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
marapendi-0.1.0-py3-none-any.whl -
Subject digest:
2b9665dfb4b692fac7501d9ee77af6ca97ea548848c9d0659b6c963768c10124 - Sigstore transparency entry: 2182333271
- Sigstore integration time:
-
Permalink:
affonsop/marapendi@a02e8200e836108e28fe871456878357c9ca162b -
Branch / Tag:
refs/tags/v0.4.1 - Owner: https://github.com/affonsop
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a02e8200e836108e28fe871456878357c9ca162b -
Trigger Event:
push
-
Statement type: