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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ models; and this repo provides translation plugins.
| [r2x-core](https://github.com/NatlabRockies/r2x-core) | Shared plugin framework: `PluginContext`, `Rule`, `System`, `@getter` registry, `Result[T, E]` via [rust-ok](https://github.com/NatlabRockies/rust-ok) |
| [r2x-reeds](https://github.com/NatlabRockies/r2x-reeds) | ReEDS parser, transform plugins (`add-ccs-credit`, `break-gens`, etc.), and component models |
| [r2x-plexos](https://github.com/NatlabRockies/r2x-plexos) | PLEXOS parser/exporter and component models, built on [plexosdb](https://github.com/NatlabRockies/plexosdb) |
| [r2x-sienna](https://github.com/NREL-Sienna/r2x-sienna) | Sienna parser/exporter and [PowerSystems.jl](https://github.com/NREL-Sienna/PowerSystems.jl)-compatible models |
| [r2x-sienna](https://github.com/NatlabRockies/r2x-sienna) | Sienna parser/exporter and [PowerSystems.jl](https://github.com/Sienna-Platform/PowerSystems.jl)-compatible models |
| [infrasys](https://github.com/NatlabRockies/infrasys) | Foundational `System` container, time series management, and component storage used by all model packages |
| [plexosdb](https://github.com/NatlabRockies/plexosdb) | Standalone PLEXOS XML database reader/writer |

Expand Down Expand Up @@ -317,4 +317,4 @@ R2X is released under the
[BSD 3-Clause License](./LICENSE.txt).

Developed under software record SWR-24-91 at the
[National Renewable Energy Laboratory](https://www.nrel.gov/) (NREL).
[National Laboratory of the Rockies](https://www.nlr.gov/) (NLR).
2 changes: 1 addition & 1 deletion cliff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ footer = """
trim = true
# postprocessors
postprocessors = [
{ pattern = '\$REPO', replace = "https://github.com/NREL/R2X" }, # replace repository URL
{ pattern = '\$REPO', replace = "https://github.com/NatLabRockies/R2X" }, # replace repository URL
]

[git]
Expand Down
5 changes: 3 additions & 2 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"""

import importlib
import sys

from r2x_core.getters import GETTER_REGISTRY

Expand Down Expand Up @@ -66,8 +67,8 @@ def pytest_runtest_setup(item):
GETTER_REGISTRY.clear()
for mod_name in module_names:
try:
mod = importlib.import_module(mod_name)
importlib.reload(mod)
sys.modules.pop(mod_name, None)
importlib.import_module(mod_name)
except (ImportError, ModuleNotFoundError):
pass
_last_package = pkg_name
Expand Down
6 changes: 3 additions & 3 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@
# -- Intersphinx mapping -----------------------------------------------------

intersphinx_mapping = {
"r2x_core": ("https://nrel.github.io/r2x-core/", None),
"infrasys": ("https://nrel.github.io/infrasys/", None),
"plexosdb": ("https://nrel.github.io/plexosdb/", None),
"r2x_core": ("https://NatLabRockies.github.io/r2x-core/", None),
"infrasys": ("https://NatLabRockies.github.io/infrasys/", None),
"plexosdb": ("https://NatLabRockies.github.io/plexosdb/", None),
}

# -- MyST settings -----------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def plexos_to_sienna(system: System, config: PlexosToSiennaConfig) -> System:
context = PluginContext(source_system=system, config=config)
rules_path = files("r2x_plexos_to_sienna.config") / "rules.json"
rules = Rule.from_records(json.loads(rules_path.read_text()))
context.rules = rules
context.rules = tuple(rules)

assert context.source_system is not None, "source_system must be set"
tmp_ts_dir = context.source_system.get_time_series_directory()
Expand Down
2 changes: 1 addition & 1 deletion packages/r2x-plexos-to-sienna/tests/test_getters.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ def test_basic_node_getters(tmp_path) -> None:
context.source_system.add_component(node)

# Test node getters
assert getters.get_node_number(node, context).unwrap() == 123
assert getters.get_node_number(node, context).unwrap() == 1230
assert getters.get_base_voltage(node, context).unwrap() == 115.0
assert getters.get_node_angle(node, context).unwrap() == 0.0
assert getters.is_slack_bus(node, context).unwrap() == ACBusTypes.PQ
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
attach_region_load_time_series,
attach_reserve_time_series,
attach_time_series_to_generators,
attach_time_series_to_purchasers,
convert_pumped_storage_generators,
ensure_generator_node_memberships,
ensure_region_node_memberships,
Expand All @@ -32,6 +33,7 @@
"attach_region_load_time_series",
"attach_reserve_time_series",
"attach_emissions_to_generators",
"attach_time_series_to_purchasers",
"convert_pumped_storage_generators",
"ensure_generator_node_memberships",
"ensure_region_node_memberships",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,19 @@
"ramp_rate_down": 100.0,
"ramp_rate_up": 100.0
},
"electrolyzer": {
"capacity_MW": 50.0,
"forced_outage_rate": 0.0,
"maintenance_rate": 0.0,
"max_capacity_MW": 500.0,
"max_ramp_up_percentage": 1.0,
"mean_time_to_repair": 0.0,
"min_capacity_MW": 0.0,
"min_down_time": 0.0,
"min_stable_level_percentage": 0.0,
"min_up_time": 0.0,
"start_cost_per_MW": 0.0
},
Comment thread
mcllerena marked this conversation as resolved.
"egs_nearfield": {
"capacity_MW": 50.0,
"forced_outage_rate": 3.09,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@
},
{
"defaults": {
"category": "electrolyzer",
"category": "consuming-tech",
"fuel_price": 0.0,
"heat_rate": 0.0,
"vom_charge": 0.0,
Expand Down Expand Up @@ -147,10 +147,52 @@
"mean_time_to_repair": "mean_time_to_repair_hours",
"start_cost": "gen_startup_cost"
},
"filter": {
"casefold": true,
"field": "technology",
"op": "not_in",
"values": ["electrolyzer", "data-center"]
},
"source_type": "ReEDSConsumingTechnology",
"target_type": "PLEXOSGenerator",
"version": 2
},
{
"defaults": {
"category": "electrolyzer",
"max_load": 0.0
},
"field_map": {
"category": "technology",
"name": "name",
"max_load": "capacity",
"ext": "ext"
},
"getters": {
"units": "get_component_units"
},
"source_type": "ReEDSElectrolyzerDemand",
"target_type": "PLEXOSPurchaser",
"version": 1
},
{
"defaults": {
"category": "data-center",
"max_load": 0.0
},
"field_map": {
"category": "technology",
"name": "name",
Comment thread
mcllerena marked this conversation as resolved.
"max_load": "capacity",
"ext": "ext"
},
"getters": {
"units": "get_component_units"
},
"source_type": "ReEDSDataCenterDemand",
"target_type": "PLEXOSPurchaser",
"version": 2
},
{
"field_map": {
"category": "technology",
Expand Down Expand Up @@ -483,6 +525,18 @@
"target_type": "PLEXOSMembership",
"version": 4
},
{
"name": "purchaser_node_membership",
"system": "target",
"getters": {
"parent_object": "reeds_membership_parent_component",
"child_object": "reeds_membership_component_child_node",
"collection": "reeds_membership_collection_nodes"
},
"source_type": "PLEXOSPurchaser",
"target_type": "PLEXOSMembership",
"version": 5
},
{
"name": "line_from_node_membership",
"system": "target",
Expand Down
26 changes: 15 additions & 11 deletions packages/r2x-reeds-to-plexos/src/r2x_reeds_to_plexos/getters.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,22 +80,26 @@ def _lookup_target_node(context: PluginContext, region_name: str) -> Result[Any,
return Err(ValueError(f"No PLEXOSNode found for region '{region_name}'"))


def _lookup_source_generator(context: PluginContext, name: str) -> Any | None:
"""Find a ReEDS generator-like component by name."""
from r2x_reeds.models import ReEDSConsumingTechnology, ReEDSGenerator, ReEDSStorage
def _lookup_source_membership_component(context: PluginContext, name: str) -> Any | None:
"""Find a ReEDS source component used for membership node resolution by name."""
from r2x_reeds import models as reeds_models

reeds_consuming_technology = reeds_models.ReEDSConsumingTechnology
reeds_generator = reeds_models.ReEDSGenerator
reeds_storage = reeds_models.ReEDSStorage

if context.source_system is None:
return None

for gen in context.source_system.get_components(ReEDSGenerator):
for gen in context.source_system.get_components(reeds_generator):
if gen.name == name:
return gen

for consuming_tech in context.source_system.get_components(ReEDSConsumingTechnology):
for consuming_tech in context.source_system.get_components(reeds_consuming_technology):
if consuming_tech.name == name:
return consuming_tech

for storage in context.source_system.get_components(ReEDSStorage):
for storage in context.source_system.get_components(reeds_storage):
if storage.name == name:
return storage

Expand Down Expand Up @@ -904,13 +908,13 @@ def reeds_membership_component_child_node(
) -> Result[PLEXOSNode, ValueError]:
"""Resolve a component's region to the translated node."""
comp_name = getattr(component, "name", "")
source_gen = _lookup_source_generator(context, comp_name)
if source_gen is None:
return Err(ValueError(f"No source generator found for '{comp_name}'"))
source_component = _lookup_source_membership_component(context, comp_name)
if source_component is None:
return Err(ValueError(f"No source component found for '{comp_name}'"))

region = getattr(source_gen, "region", None)
region = getattr(source_component, "region", None)
if region is None or not getattr(region, "name", None):
return Err(ValueError(f"Source generator '{source_gen.name}' is missing region data"))
return Err(ValueError(f"Source component '{source_component.name}' is missing region data"))

return _lookup_target_node(context, region.name)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,29 @@
from r2x_core import PluginContext, System


def _get_reeds_purchaser_source_types() -> tuple[type[Any], ...]:
"""Return consuming-demand source model types available in the installed ReEDS package."""
from r2x_reeds import models as reeds_models

type_names = ("ReEDSElectrolyzerDemand", "ReEDSDataCenterDemand", "ReEDSConsumingTechnology")
resolved_types: list[type[Any]] = []
for type_name in type_names:
model_type = getattr(reeds_models, type_name, None)
if isinstance(model_type, type) and model_type not in resolved_types:
resolved_types.append(model_type)
return tuple(resolved_types)


def _get_plexos_purchaser_type() -> type[Any]:
"""Return purchaser model type, falling back to generator when purchaser is unavailable."""
from r2x_plexos import models as plexos_models

purchaser_type = getattr(plexos_models, "PLEXOSPurchaser", None)
if isinstance(purchaser_type, type):
return purchaser_type
return PLEXOSGenerator


def attach_region_load_time_series(context: PluginContext) -> None:
"""Attach demand load and time series from ReEDSDemand to the translated PLEXOSRegion."""
from r2x_plexos.models import PLEXOSRegion
Expand Down Expand Up @@ -112,8 +135,54 @@ def attach_time_series_to_generators(context: PluginContext) -> None:
target_gen, name=ts.name, time_series_type=type(ts)
):
context.target_system.add_time_series(deepcopy(ts), target_gen)


def attach_time_series_to_purchasers(context: PluginContext) -> None:
"""Transfer electrolyzer and data center demand time series to translated PLEXOS purchasers."""
if context.source_system is None or context.target_system is None:
return

source_demands: dict[str, Any] = {}
for demand_type in _get_reeds_purchaser_source_types():
for demand in context.source_system.get_components(demand_type):
source_demands[demand.name] = demand
purchaser_type = _get_plexos_purchaser_type()
target_purchasers = {
purchaser.name: purchaser for purchaser in context.target_system.get_components(purchaser_type)
}

for demand_name, source_demand in source_demands.items():
target_purchaser = target_purchasers.get(demand_name)
if target_purchaser is None:
continue

for metadata in context.source_system.time_series.list_time_series_metadata(source_demand):
ts_list = context.source_system.list_time_series(
source_demand, name=metadata.name, **metadata.features
)
if not ts_list:
logger.warning(
"Missing purchaser demand time series {} for {}",
metadata.name,
source_demand.name,
)
continue

ts = deepcopy(ts_list[0])
ts_type = ts.__class__
if not context.target_system.has_time_series(
target_purchaser,
name=ts.name,
time_series_type=ts_type,
**metadata.features,
):
context.target_system.add_time_series(ts, target_purchaser, **metadata.features)
logger.debug(
"Attached purchaser time series {} to purchaser {}",
ts.name,
target_purchaser.name,
)


def ensure_region_node_memberships(context: PluginContext) -> None:
"""Ensure every translated region has a node child membership with matching name."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
attach_region_load_time_series,
attach_reserve_time_series,
attach_time_series_to_generators,
attach_time_series_to_purchasers,
)


Expand All @@ -32,7 +33,7 @@ def reeds_to_plexos(system: System, config: ReedsToPlexosConfig) -> System:

rules_path = files("r2x_reeds_to_plexos.config") / "rules.json"
rules = Rule.from_records(json.loads(rules_path.read_text()))
context.rules = rules
context.rules = tuple(rules)

assert context.source_system is not None, "source_system must be set"
tmp_ts_dir = context.source_system.get_time_series_directory()
Expand All @@ -51,5 +52,6 @@ def reeds_to_plexos(system: System, config: ReedsToPlexosConfig) -> System:
attach_reserve_time_series(context)
attach_time_series_to_generators(context)
attach_region_load_time_series(context)
attach_time_series_to_purchasers(context)

return context.target_system
Loading
Loading