Source code for assetra.system
from __future__ import annotations
from logging import getLogger
from pathlib import Path
import errno
import os
# external
import xarray as xr
# package
from assetra.units import (
EnergyUnit,
RESPONSIVE_UNIT_TYPES,
NONRESPONSIVE_UNIT_TYPES,
)
LOG = getLogger(__name__)
[docs]class EnergySystem:
"""Class responsible for managing unit datasets (built energy systems)
Args:
unit_datasets (dict[Type : xr.Dataset]) : Mapping from derived energy
unit type to its associated unit dataset
"""
def __init__(self, unit_datasets: dict[type : xr.DataArray] = {}):
self._unit_datasets = unit_datasets
# check input
for unit_type in unit_datasets:
if unit_type not in (
RESPONSIVE_UNIT_TYPES + NONRESPONSIVE_UNIT_TYPES
):
LOG.error(
"Constructing energy system with invalid unit dataset"
)
raise RuntimeWarning
@property
def size(self) -> float:
"""
Total number of units in system
"""
return sum(d.sizes["energy_unit"] for d in self._unit_datasets.values())
@property
def system_capacity(self) -> float:
"""
Total nameplate capacity of a system
"""
return sum(
float(d["nameplate_capacity"].sum())
for d in self._unit_datasets.values()
)
@property
def unit_datasets(self) -> dict[type : xr.Dataset]:
"""
Mapping from derived energy unit type to unit dataset
"""
return self._unit_datasets
[docs] def get_system_by_type(self, unit_type: list[type] | type) -> EnergySystem:
"""Return a system comprised of the subset of unit datasets
corresponding to one (or more) energy unit types.
For example, get a [sub]system with only the responsive or non-responsive
units of a system
Args:
unit_type (list[type] | type): Either a derived energy unit type or
a list of derived energy unit types.
Returns:
EnergySystem: An energy system whose unit datasets are a sub-set of
of this energy system.
"""
if isinstance(unit_type, (list, tuple)):
unit_datasets = {
ut: ud
for ut, ud in self._unit_datasets.items()
if ut in unit_type
}
return EnergySystem(unit_datasets)
elif unit_type in RESPONSIVE_UNIT_TYPES + NONRESPONSIVE_UNIT_TYPES:
return EnergySystem({unit_type: self._unit_datasets[unit_type]})
[docs] def save(self, directory: Path, overwrite=False) -> None:
"""Save energy system to a directory. Unit datasets are saved as netcdf
files
Args:
directory (Path): Path to which energy system is saved. This path
should either be empty or not exist yet.
overwrite (bool, optional): _description_. Defaults to False.
"""
directory.mkdir(parents=True, exist_ok=overwrite)
for unit_type, dataset in self._unit_datasets.items():
dataset_file = Path(directory, unit_type.__name__ + ".assetra.nc")
dataset.to_netcdf(dataset_file)
[docs] def load(self, directory: Path) -> None:
"""Load energy system from a saved directory
Args:
directory (Path): Path from which energy system is loaded. This
should be the same path passed to EnergySystem.save
"""
if not directory.exists():
raise FileNotFoundError(
errno.ENOENT,
os.strerror(errno.ENOENT),
str(directory.resolve()),
)
self._unit_datasets = dict()
for unit_type in NONRESPONSIVE_UNIT_TYPES + RESPONSIVE_UNIT_TYPES:
dataset_file = Path(directory, unit_type.__name__ + ".assetra.nc")
if dataset_file.exists():
LOG.info("Found unit dataset: " + str(dataset_file.resolve()))
self._unit_datasets[unit_type] = xr.open_dataset(dataset_file)
[docs]class EnergySystemBuilder:
"""Class responsible for managing energy units and building energy systems"""
def __init__(self):
self._energy_units = []
@property
def energy_units(self) -> tuple[EnergyUnit]:
"""
Energy units added to builder object
"""
return tuple(self._energy_units)
@property
def size(self) -> int:
"""
Number of energy units added to builder object
"""
return len(self._energy_units)
[docs] def add_unit(self, energy_unit: EnergyUnit) -> None:
"""Add an energy unit to the system builder object.
Args:
energy_unit (EnergyUnit): Energy unit to add to system builder
Raises:
RuntimeWarning: Invalid energy unit type added to system builder
RuntimeWarning: Duplicate unit added to energy system builder
"""
# check for valid energy unit
if (
type(energy_unit)
not in NONRESPONSIVE_UNIT_TYPES + RESPONSIVE_UNIT_TYPES
):
LOG.warning("Invalid type added to energy system builder")
raise RuntimeWarning()
# check for duplicates
if energy_unit.id in [u.id for u in self._energy_units]:
LOG.warning("Duplicate unit ID added to energy system builder")
raise RuntimeWarning()
# add unit to internal list
self._energy_units.append(energy_unit)
[docs] def remove_unit(self, energy_unit: EnergyUnit) -> None:
"""Remove an energy unit from the system builder object.
Args:
energy_unit (EnergyUnit): Energy unit to remove from system builder
"""
try:
self._energy_units.remove(energy_unit)
except KeyError:
LOG.warning("Unit to remove not found in energy system builder")
[docs] def build(self) -> EnergySystem:
"""Return a populated EnergySystem instance. Take energy units added to
the builder object, compile each unit type into a unit dataset (fleet),
and instantiate an EnergySystem with the resulting unit dataset
dictionary.
This is the recommended method to instantiate EnergySystem objects
Returns:
EnergySystem: Populated energy system instance
"""
unit_datasets = dict()
# populate unit datasets
for unit_type in NONRESPONSIVE_UNIT_TYPES + RESPONSIVE_UNIT_TYPES:
# get unit by type
units = [
unit for unit in self.energy_units if type(unit) is unit_type
]
# get unit dataset
if len(units) > 0:
unit_datasets[unit_type] = unit_type.to_unit_dataset(units)
return EnergySystem(unit_datasets)
[docs] @staticmethod
def from_energy_system(energy_system: EnergySystem) -> EnergySystemBuilder:
"""Return a populated EnergySystemBuilder instance. Take energy unit
datasets from an energy system, convert datasets into
individual energy units, and add units to a new EnergySystemBuilder
object
This is the inverse to EnergySystem.build, and is useful for
modifying energy systems which have been loaded directly from file
using the EnergySystem.load function
Args:
energy_system (EnergySystem): Populated energy system
Returns:
EnergySystemBuilder: Populated builder instance
"""
builder = EnergySystemBuilder()
for unit_type, unit_dataset in energy_system.unit_datasets.items():
units = unit_type.from_unit_dataset(unit_dataset)
for unit in units:
builder.add_unit(unit)
return builder