Skip to content

Commit

Permalink
feat(plexos): plexos parser xml (#93)
Browse files Browse the repository at this point in the history
Update the plexos parser to find a single xml file in the run_directory
when available. If the fmap is provided that will be used in favor of
finding the only xml file. When multiple xml files are present, this
will raise an Error, and then the fmap will be required again.
  • Loading branch information
JensZack authored Dec 5, 2024
1 parent 959ef4d commit 835e904
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 28 deletions.
23 changes: 12 additions & 11 deletions src/r2x/parser/plexos.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
reconcile_timeseries,
)
from .plexos_utils import (
find_xml,
DATAFILE_COLUMNS,
PLEXOS_ACTION_MAP,
filter_property_dates,
Expand Down Expand Up @@ -168,20 +169,20 @@ def __init__(self, *args, xml_file: str | None = None, **kwargs) -> None:
# TODO(pesap): Rename exceptions to include R2X
# https://github.com/NREL/R2X/issues/5
# R2X needs at least one of this maps defined to correctly work.
if (
not self.fuel_map
and not self.device_map
and not self.device_match_string
and not self.category_map
):
msg = (
"Neither `plexos_fuel_map` or `plexos_device_map` or `device_match_string` was provided. "
"To fix, provide any of the mappings."
)
one_required = ["fuel_map", "device_map", "device_match_string", "category_map"]
if all(getattr(self, one_req, {}) == {} for one_req in one_required):
msg = f'At least one of {", or ".join(one_required)} is required to initialize PlexosParser'
raise ParserError(msg)

# Populate databse from XML file.
xml_file = xml_file or self.run_folder / self.config.fmap["xml_file"]["fname"]
# If xml file is not specified, check user_dict["fmap"]["xml_file"] or use
# only xml file in project directory
if xml_file is None:
xml_file = self.config.fmap.get("xml_file", {}).get("fname", None)
xml_file = xml_file or str(find_xml(self.run_folder))

xml_file = str(self.run_folder / xml_file)

self.db = PlexosSQLite(xml_fname=xml_file)

# Extract scenario data
Expand Down
41 changes: 41 additions & 0 deletions src/r2x/parser/plexos_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from enum import Enum
from typing import Any
from collections.abc import Sequence
from os import PathLike
from pathlib import Path

from numpy._typing import NDArray
import pint
Expand Down Expand Up @@ -465,3 +467,42 @@ def time_slice_handler(
raise NotImplementedError

return month_datetime_series


def find_xml(directory: PathLike):
"""
Parameters
----------
directory: Pathlike
directory to search in for an xml file
Raises
------
FileNotFoundError:
if no xml file is in <directory>
OR more than one xml files are in <directory>
NotADirectoryError:
if <directory> is not a directory
Returns
-------
ret: Path
Path to a single xml file
"""
directory = Path(directory)
if not directory.is_dir():
raise NotADirectoryError(f"Can't search {directory}, not a directory")

xml_files = list(directory.glob("*.xml"))

if len(xml_files) == 0:
raise FileNotFoundError(f"No xml file in {directory}")

if len(xml_files) > 1:
raise FileNotFoundError(f"More than one xml file in {directory}")

return xml_files[0]
33 changes: 33 additions & 0 deletions tests/test_parser_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from polars.testing import assert_frame_equal
from tempfile import NamedTemporaryFile
from r2x.parser.handler import csv_handler
from r2x.parser.plexos_utils import find_xml


@pytest.fixture
Expand Down Expand Up @@ -33,3 +34,35 @@ def test_csv_handler_basic(temp_csv_file):

with pytest.raises(FileNotFoundError):
_ = csv_handler(Path("non_existent_file.csv"))


@pytest.fixture
def basic_xml():
data = """
<t_category>
<category_id>1</category_id>
<class_id>1</class_id>
<rank>0</rank>
<name>-</name>
</t_category>
"""
return data


def test_find_xml(tmp_path: Path, basic_xml: str):
# Case 1: no xml files in tmp_path
with pytest.raises(FileNotFoundError):
find_xml(tmp_path)

# Case 2: 1 xml file in tmp_path
xml_file_1 = tmp_path / "file1.xml"
xml_file_1.write_text(basic_xml)

assert xml_file_1 == find_xml(tmp_path)

# Case 3: 2 xml files in tmp_path
xml_file_2 = tmp_path / "file2.xml"
xml_file_2.write_text(basic_xml)

with pytest.raises(FileNotFoundError):
find_xml(tmp_path)
23 changes: 6 additions & 17 deletions tests/test_plexos_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
from plexosdb import XMLHandler
from r2x.api import System
from r2x.config import Scenario
from r2x.exceptions import ParserError
from r2x.parser.handler import get_parser_data
from r2x.parser.plexos import PlexosParser
from r2x.exceptions import ParserError

DB_NAME = "2-bus_example.xml"
MODEL_NAME = "main_model"
Expand Down Expand Up @@ -58,30 +58,19 @@ def test_build_system(plexos_parser_instance):
assert isinstance(system, System)


def test_raise_if_no_map_provided(tmp_path, data_folder):
scenario = Scenario.from_kwargs(
name="plexos_test",
input_model="plexos",
run_folder=data_folder,
output_folder=tmp_path,
solve_year=2035,
model=MODEL_NAME,
weather_year=2012,
fmap={"xml_file": {"fname": DB_NAME}},
)
with pytest.raises(ParserError):
_ = get_parser_data(scenario, parser_class=PlexosParser)


def test_parser_system(pjm_scenario):
plexos_category_map = {
"thermal": {"fuel": "GAS", "type": "CC"},
"solar": {"fuel": "SUN", "type": "WT"},
"wind": {"fuel": "WIND", "type": "PV"},
}
pjm_scenario.defaults["plexos_category_map"] = plexos_category_map
pjm_scenario.model = "model_2012"

with pytest.raises(ParserError):
parser = get_parser_data(pjm_scenario, parser_class=PlexosParser)

pjm_scenario.defaults["plexos_category_map"] = plexos_category_map

parser = get_parser_data(pjm_scenario, parser_class=PlexosParser)
system = parser.build_system()
assert isinstance(system, System)
Expand Down

0 comments on commit 835e904

Please sign in to comment.