Skip to content
Open
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ wheels/
*.egg-info/
.installed.cfg
*.egg
.DS_Store
*.DS_Store

# PyInstaller
# Usually these files are written by a python script from a template
Expand Down
58 changes: 28 additions & 30 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,38 +1,36 @@
default_language_version:
python: python3

python: python3
repos:
- repo: https://github.com/pycqa/isort
- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
name: isort
stages: [pre-commit]

- repo: https://github.com/psf/black
rev: 22.3.0
hooks:
- id: black
name: black
stages: [pre-commit]
language_version: python3

- repo: https://github.com/pre-commit/pre-commit-hooks
- id: isort
name: isort
stages: [pre-commit]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-executables-have-shebangs
- id: check-json
- id: check-yaml
- id: check-merge-conflict
- id: check-symlinks
- id: mixed-line-ending
- id: pretty-format-json
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-executables-have-shebangs
- id: check-json
- id: check-yaml
args: [--unsafe] # allow Python constructors
- id: check-merge-conflict
- id: check-symlinks
- id: mixed-line-ending
- id: pretty-format-json
args: [--autofix]

- repo: https://github.com/pycqa/flake8
rev: '5.0.4'
- repo: https://github.com/lyz-code/yamlfix/
rev: 1.19.1
hooks:
- id: yamlfix
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.8.1
hooks:
- id: flake8
args: [--max-line-length=88, "--ignore=E741,W503,E203", "--per-file-ignores=*/__init__.py:F401"]
- id: ruff-format
types_or: [python, pyi, jupyter]
- id: ruff
types_or: [python, pyi, jupyter]
args: [--fix, --unsafe-fixes]
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,11 @@ includes levelized cost of hydrogen (LCOH) analysis utilities.

Python 3.11+ is required.

Create conda environment:

```bash
pip install .
conda create --name bert python=3.11 -y
conda activate bert
```

Optional extras:
Expand Down
7 changes: 6 additions & 1 deletion electrolyzer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@
__email__ = "christopher.bay@nrel.gov"
__version__ = "0.2.0"

# noqa
from pathlib import Path


BERT_ROOT_DIR = Path(__file__).resolve().parent
BERT_EXAMPLE_DIR = BERT_ROOT_DIR.parent / "examples"
# BERT_LIBRARY_DIR = BERT_ROOT_DIR.parent / "library"
Empty file.
Empty file.
62 changes: 62 additions & 0 deletions electrolyzer/components/cell/cell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import openmdao.api as om
from attrs import field, define, validators

from electrolyzer.core.utilities import BaseConfig


@define(kw_only=True)
class CellBaseConfig(BaseConfig):
A_cell: float = field(validator=(validators.gt(0.0)))
membrane_thickness: float = field(validator=(validators.gt(0.0)))
temperature: float = field(validator=(validators.ge(50.0)))
P_anode: float = field(validator=(validators.ge(0.0)))
P_cathode: float = field(validator=(validators.ge(0.0)))
R_ohmic_elec: float = field(validator=(validators.ge(0.0)))


class CellBaseClass(om.ExplicitComponent):
def initialize(self):
self.options.declare("plant_config", types=dict)
self.options.declare("tech_config", types=dict)

def setup(self):
self.n_timesteps = self.options["plant_config"]["simulation"]["n_timesteps"]
self.dt = self.options["plant_config"]["simulation"]["dt"]

# design variables
self.add_input("cell_active_area", val=self.config.A_cell, units="A/(cm**2)")
self.add_input("membrane_thickness", val=self.config.membrane_thickness, units="cm")
self.add_input("operating_temperature", val=self.config.temperature, units="degC")
self.add_input("anode_pressure", self.config.P_anode, units="bar")
self.add_input("cathode_pressure", self.config.P_cathode, units="bar")
self.add_input("R_elec", self.config.R_ohmic_elec, units="ohm*(cm**2)")

# input profiles
self.add_input("current_in", val=0.0, shape_by_conn=True, units="A") # OR current density?
self.add_input("current_density_in", val=0.0, shape_by_conn=True, units="A/(cm**2)")

# output profiles
self.add_output("cell_voltage", val=0.0, copy_shape="current_in", units="V")
self.add_output(
"hydrogen_produced", val=0.0, copy_shape="current_in", units=f"kg/({self.dt}*s)"
)
self.add_output(
"oxygen_produced", val=0.0, copy_shape="current_in", units=f"kg/({self.dt}*s)"
)
self.add_output(
"water_consumed", val=0.0, copy_shape="current_in", units=f"kg/({self.dt}*s)"
)
self.add_output("hydrogen_production_rate", val=0.0, copy_shape="current_in", units="kg/s")
self.add_input("current_density_out", val=0.0, copy_shape="current_in", units="A/(cm**2)")

# output design variables
self.add_output("rated_cell_voltage", val=0.0, units="V")

def compute(self, inputs, outputs, discrete_inputs, discrete_outputs):
"""
Computation for the OM component.

For a template class this is not implement and raises an error.
"""

raise NotImplementedError("This method should be implemented in a subclass.")
37 changes: 37 additions & 0 deletions electrolyzer/components/cell/pem_cell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from attrs import field, define, validators

from electrolyzer.tools.validators import contains, range_val
from electrolyzer.components.cell.cell import CellBaseClass, CellBaseConfig


@define(kw_only=True)
class PEMCellConfig(CellBaseConfig):
f1: float = field(default=250, validator=validators.ge(0))
f2: float = field(default=0.996, validator=range_val(0.5, 1.0))
i_0a: float = field(default=2.0e-7, validator=range_val(0.0, 1.0))
i_0c: float = field(default=2.0e-3, validator=range_val(0.0, 1.0))
alpha_a: float = field(default=2.0, validator=range_val(0.0, 4.0))
alpha_c: float = field(default=0.5, validator=range_val(0.0, 4.0))

water_vaporization_method: str = field(
default="buck", converter=(str.lower, str.strip), validator=contains(["buck", "antoine"])
)
activation_method: str = field(
default="arcsinh", validator=contains(["ln", "log10", "arcsinh"])
)
Urev0_calc_method: str = field(
default="normal", validator=contains(["normal", "temp_adjusted"])
)


class PEMCell(CellBaseClass):
def setup(self):
self.config = PEMCellConfig.from_dict(self.options["tech_config"]["cell_parameters"])
super().setup()
# Design parameters
self.add_input("i_0a", val=self.config.i_0a, shape=1, units="A/(cm**2)")
self.add_input("i_0c", val=self.config.i_0c, shape=1, units="A/(cm**2)")
self.add_input("f1", val=self.config.f1, shape=1, units="(mA**2)/(cm**4)")
self.add_input("f2", val=self.config.f2, shape=1, units="unitless")
self.add_input("alpha_a", val=self.config.alpha_a, shape=1, units="unitless")
self.add_input("alpha_c", val=self.config.alpha_c, shape=1, units="unitless")
Empty file.
Empty file.
Empty file.
26 changes: 26 additions & 0 deletions electrolyzer/components/stack/stack.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import openmdao.api as om


class StackBaseClass(om.ExplicitComponent):
def initialize(self):
self.options.declare("plant_config", types=dict)
self.options.declare("tech_config", types=dict)

def setup(self):
# design variables

# input profiles
self.add_input("current", val=0.0, shape=self.n_timesteps, units="A")

# output profiles

# constraints

def compute(self, inputs, outputs, discrete_inputs, discrete_outputs):
"""
Computation for the OM component.

For a template class this is not implement and raises an error.
"""

raise NotImplementedError("This method should be implemented in a subclass.")
Empty file.
Empty file.
Empty file.
Empty file added electrolyzer/core/__init__.py
Empty file.
19 changes: 19 additions & 0 deletions electrolyzer/core/bert.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
class BERT:
def __init__(self, config_input):
# read in config file; it's a yaml dict that looks like this:
self.load_config(config_input)

def load_config(config_input):
pass

def create_custom_models(self, model_config, config_parent_path, model_types, prefix=""):
pass

def setup(self):
pass

def run(self):
pass

def post_process(self):
pass
2 changes: 2 additions & 0 deletions electrolyzer/core/file_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def play_func(txt):
return txt
Empty file.
95 changes: 95 additions & 0 deletions electrolyzer/core/utilities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
from typing import Any

import attrs
import numpy as np
from attrs import Attribute, define


@define(kw_only=True)
class BaseConfig:
"""
A Mixin class to allow for kwargs overloading when a data class doesn't
have a specific parameter defined. This allows passing of larger dictionaries
to a data class without throwing an error.
"""

@classmethod
def from_dict(cls, data: dict, strict=True, additional_cls_name: str | None = None):
"""Maps a data dictionary to an ``attrs``-defined class.

Args:
data (dict): The data dictionary to be mapped.
strict (bool): A flag enabling strict parameter processing, meaning that no extra
parameters may be passed in or an AttributeError will be raised.
additional_cls_name (str | None): The name of the model class creating the configuration
data class. Provides an easier to diagnose error message for end users when
the class name is provided.

Returns:
cls: The ``attrs``-defined class.
"""
# Check for any inputs that aren't part of the class definition
if strict is True:
class_attr_names = [a.name for a in cls.__attrs_attrs__]
extra_args = [d for d in data if d not in class_attr_names]
if len(extra_args):
if additional_cls_name is not None:
msg = (
f"{additional_cls_name} setup failed as a result of {cls.__name__}"
f" receiving extraneous inputs: {extra_args}"
)
else:
msg = (
f"The initialization for {cls.__name__} was given extraneous "
f"inputs: {extra_args}"
)
raise AttributeError(msg)

kwargs = {a.name: data[a.name] for a in cls.__attrs_attrs__ if a.name in data and a.init}

# Map the inputs must be provided: 1) must be initialized, 2) no default value defined
required_inputs = [
a.name for a in cls.__attrs_attrs__ if a.init and a.default is attrs.NOTHING
]
undefined = sorted(set(required_inputs) - set(kwargs))

if undefined:
if additional_cls_name is not None:
msg = (
f"{additional_cls_name} setup failed as a result of {cls.__name__}"
f" missing the following inputs: {undefined}"
)
else:
msg = (
f"The class definition for {cls.__name__} is missing the following inputs: "
f"{undefined}"
)
raise AttributeError(msg)
return cls(**kwargs)

def as_dict(self) -> dict:
"""Creates a JSON and YAML friendly dictionary that can be save for future reloading.
This dictionary will contain only `Python` types that can later be converted to their
proper `Turbine` formats.

Returns:
dict: All key, value pairs required for class re-creation.
"""
return attrs.asdict(self, filter=attr_filter, value_serializer=attr_serializer)


def attr_serializer(inst: type, field: Attribute, value: Any):
if isinstance(value, np.ndarray):
return value.tolist()
return value


def attr_filter(inst: Attribute, value: Any) -> bool:
if inst.init is False:
return False
if value is None:
return False
if isinstance(value, np.ndarray):
if value.size == 0:
return False
return True
Loading
Loading