From a675829fb380962b9609608d1ba4ab270577c59f Mon Sep 17 00:00:00 2001 From: mvelasqu Date: Thu, 30 Apr 2026 12:19:44 -0600 Subject: [PATCH 1/5] feat: add complete class/collection object references for plexos versions --- docs/source/api/enums.md | 46 +++++++++ src/plexosdb/enums.py | 178 ++++++++++++++++++++++++++++++++-- tests/test_enums_functions.py | 43 ++++---- 3 files changed, 241 insertions(+), 26 deletions(-) diff --git a/docs/source/api/enums.md b/docs/source/api/enums.md index ee8bd9f..8ecdab5 100644 --- a/docs/source/api/enums.md +++ b/docs/source/api/enums.md @@ -1,5 +1,51 @@ # Enumerations +## Version Coverage + +The enum module now includes coverage constants for schema sizes in supported +versions: + +- v9.2: 96 classes, 776 collections +- v10: 96 classes, 806 collections + +## v10 Collection Differences vs v9.2 + +The following 30 collection relationships are present in v10 and not present in +v9.2. + +| Collection Relationship | +| -------------------------------------------------- | +| Capacity Facilities: Zone -> Facility | +| Capacity Gas Plants: Zone -> Gas Plant | +| Capacity Gas Storages: Zone -> Gas Storage | +| Capacity Heat Plants: Zone -> Heat Plant | +| Capacity Water Plants: Zone -> Water Plant | +| Conditions: Battery -> Variable | +| Entities: Flow Node -> Entity | +| Entities: Flow Path -> Entity | +| Entities: Flow Storage -> Entity | +| Facilities: Region -> Facility | +| Facilities: Zone -> Facility | +| Gas Paths: Gas Contract -> Gas Path | +| Gas Paths: Gas Pipeline -> Gas Path | +| Gas Plants: Region -> Gas Plant | +| Gas Plants: Zone -> Gas Plant | +| Gas Storages: Region -> Gas Storage | +| Gas Storages: Zone -> Gas Storage | +| Heat Plants: Region -> Heat Plant | +| Heat Plants: Zone -> Heat Plant | +| Initial Gas Path: Gas Transport -> Gas Path | +| Interfaces Monitored: Contingency -> Interface | +| Lines: Reserve -> Line | +| Lines Monitored: Contingency -> Line | +| Node: Gas Storage -> Node | +| ORDC System Lambda Nodes: Pool -> Node | +| Start Gas Nodes: Generator -> Gas Node | +| Transformers Monitored: Contingency -> Transformer | +| Water Plants: Region -> Water Plant | +| Water Plants: Zone -> Water Plant | +| Zones: Reserve -> Zone | + ```{eval-rst} .. automodule:: plexosdb.enums :members: diff --git a/src/plexosdb/enums.py b/src/plexosdb/enums.py index c665af2..3da9f84 100644 --- a/src/plexosdb/enums.py +++ b/src/plexosdb/enums.py @@ -1,7 +1,7 @@ """Plexos model enums that define the data schema.""" from enum import Enum, StrEnum -from typing import cast +from typing import Optional, cast class Schema(Enum): @@ -61,36 +61,113 @@ class ClassEnum(StrEnum): System = "System" Generator = "Generator" + PowerStation = "PowerStation" Fuel = "Fuel" + FuelContract = "FuelContract" + Power2X = "Power2X" Battery = "Battery" Storage = "Storage" + Waterway = "Waterway" Emission = "Emission" + Abatement = "Abatement" + PhysicalContract = "PhysicalContract" Reserve = "Reserve" + Reliability = "Reliability" + FinancialContract = "FinancialContract" + Cournot = "Cournot" + RSI = "RSI" Region = "Region" + Pool = "Pool" Zone = "Zone" Node = "Node" + Load = "Load" Line = "Line" + MLF = "MLF" Transformer = "Transformer" + FlowControl = "FlowControl" Interface = "Interface" - DataFile = "Data File" + Contingency = "Contingency" + Hub = "Hub" + TransmissionRight = "TransmissionRight" + HeatPlant = "HeatPlant" + HeatNode = "HeatNode" + HeatStorage = "HeatStorage" + GasField = "GasField" + GasPlant = "GasPlant" + GasPipeline = "GasPipeline" + GasNode = "GasNode" + GasStorage = "GasStorage" + GasDemand = "GasDemand" + GasDSMProgram = "GasDSMProgram" + GasBasin = "GasBasin" + GasZone = "GasZone" + GasContract = "GasContract" + GasTransport = "GasTransport" + GasPath = "GasPath" + GasCapacityReleaseOffer = "GasCapacityReleaseOffer" + WaterPlant = "WaterPlant" + WaterPipeline = "WaterPipeline" + WaterNode = "WaterNode" + WaterStorage = "WaterStorage" + WaterDemand = "WaterDemand" + WaterZone = "WaterZone" + WaterPumpStation = "WaterPumpStation" + WaterPump = "WaterPump" + Vehicle = "Vehicle" + ChargingStation = "ChargingStation" + Fleet = "Fleet" + Company = "Company" + Commodity = "Commodity" + Process = "Process" + Facility = "Facility" + Maintenance = "Maintenance" + FlowNetwork = "FlowNetwork" + FlowNode = "FlowNode" + FlowPath = "FlowPath" + FlowStorage = "FlowStorage" + Entity = "Entity" + Market = "Market" + DataFile = "DataFile" + Variable = "Variable" Timeslice = "Timeslice" + Global = "Global" Scenario = "Scenario" + WeatherStation = "WeatherStation" Model = "Model" + Project = "Project" Horizon = "Horizon" Report = "Report" + Stochastic = "Stochastic" + Preview = "Preview" + LTPlan = "LTPlan" PASA = "PASA" MTSchedule = "MTSchedule" STSchedule = "STSchedule" Transmission = "Transmission" - Diagnostic = "Diagnostic" Production = "Production" + Competition = "Competition" Performance = "Performance" - Variable = "Variable" + Diagnostic = "Diagnostic" + List = "List" + Layout = "Layout" Constraint = "Constraint" + Objective = "Objective" + DecisionVariable = "DecisionVariable" + NonlinearConstraint = "NonlinearConstraint" Purchaser = "Purchaser" + @classmethod + def _missing_(cls, value: object) -> Optional["ClassEnum"]: + """Resolve class values that differ only by whitespace.""" + if not isinstance(value, str): + return None + + normalized = value.replace(" ", "") + for member in cls: + if member.value.replace(" ", "") == normalized: + return member -plexos_class_mapping = {enum_member.name: enum_member.value for enum_member in ClassEnum} + return None class CollectionEnum(StrEnum): @@ -135,6 +212,79 @@ class CollectionEnum(StrEnum): Constraints = "Constraints" Variables = "Variables" Purchasers = "Purchasers" + PowerStations = "PowerStations" + FuelContracts = "FuelContracts" + Power2X = "Power2X" + Waterways = "Waterways" + Abatements = "Abatements" + PhysicalContracts = "PhysicalContracts" + Reliability = "Reliability" + FinancialContracts = "FinancialContracts" + Cournots = "Cournots" + RSIs = "RSIs" + Pools = "Pools" + Loads = "Loads" + MLFs = "MLFs" + FlowControls = "FlowControls" + Contingencies = "Contingencies" + Hubs = "Hubs" + TransmissionRights = "TransmissionRights" + HeatPlants = "HeatPlants" + HeatNodes = "HeatNodes" + HeatStorages = "HeatStorages" + GasFields = "GasFields" + GasPlants = "GasPlants" + GasPipelines = "GasPipelines" + GasNodes = "GasNodes" + GasStorages = "GasStorages" + GasDemands = "GasDemands" + GasDSMPrograms = "GasDSMPrograms" + GasBasins = "GasBasins" + GasZones = "GasZones" + GasContracts = "GasContracts" + GasTransports = "GasTransports" + GasPaths = "GasPaths" + GasCapacityReleaseOffers = "GasCapacityReleaseOffers" + WaterPlants = "WaterPlants" + WaterPipelines = "WaterPipelines" + WaterNodes = "WaterNodes" + WaterStorages = "WaterStorages" + WaterDemands = "WaterDemands" + WaterZones = "WaterZones" + WaterPumpStations = "WaterPumpStations" + WaterPumps = "WaterPumps" + Vehicles = "Vehicles" + ChargingStations = "ChargingStations" + Fleets = "Fleets" + Companies = "Companies" + Commodities = "Commodities" + Processes = "Processes" + Facilities = "Facilities" + Maintenances = "Maintenances" + FlowNetworks = "FlowNetworks" + FlowNodes = "FlowNodes" + FlowPaths = "FlowPaths" + FlowStorages = "FlowStorages" + Entities = "Entities" + Markets = "Markets" + Objectives = "Objectives" + DecisionVariables = "DecisionVariables" + NonlinearConstraints = "NonlinearConstraints" + Timeslices = "Timeslices" + Globals = "Globals" + WeatherStations = "WeatherStations" + Projects = "Projects" + Stochastic = "Stochastic" + Preview = "Preview" + LTPlan = "LTPlan" + Competition = "Competition" + Lists = "Lists" + Layouts = "Layouts" + + +PLEXOS_CLASS_COUNT_V9_V10 = 96 +PLEXOS_COLLECTION_COUNT_V92 = 776 +PLEXOS_COLLECTION_COUNT_V10 = 806 def str2enum(string: str, schema_enum: type[Enum] = Schema) -> Schema | None: @@ -156,10 +306,20 @@ def get_default_collection(class_enum: ClassEnum) -> CollectionEnum: if class_enum in special_cases: return special_cases[class_enum] - collection_name = f"{class_enum}s" - if collection_name not in CollectionEnum.__members__: - collection_name = class_enum.name - return CollectionEnum[collection_name] + normalized_key = class_enum.value.replace(" ", "") + + candidates = ( + f"{normalized_key}s", + normalized_key, + f"{class_enum.name}s", + class_enum.name, + ) + + for candidate in candidates: + if candidate in CollectionEnum.__members__: + return CollectionEnum[candidate] + + raise KeyError(f"Default collection is not defined for class {class_enum.value!r}") def _parse_str_enum(enum_cls: type[Enum], value: str | Enum) -> Enum: diff --git a/tests/test_enums_functions.py b/tests/test_enums_functions.py index 2ec3653..cfcab06 100644 --- a/tests/test_enums_functions.py +++ b/tests/test_enums_functions.py @@ -269,23 +269,6 @@ def test_collection_enum_all_members_exist(collection_name: str): assert isinstance(collection_obj.value, str) -def test_plexos_class_mapping_contains_all_classes(): - """Verify plexos_class_mapping contains all ClassEnum members.""" - from plexosdb.enums import ClassEnum, plexos_class_mapping - - for class_member in ClassEnum: - assert class_member.name in plexos_class_mapping - assert plexos_class_mapping[class_member.name] == class_member.value - - -def test_plexos_class_mapping_is_dict(): - """Verify plexos_class_mapping is a dictionary.""" - from plexosdb.enums import plexos_class_mapping - - assert isinstance(plexos_class_mapping, dict) - assert len(plexos_class_mapping) > 0 - - def test_class_enum_string_values(): """Verify all ClassEnum values are strings.""" from plexosdb.enums import ClassEnum @@ -379,6 +362,16 @@ def test_parse_str_enum_exact_value_and_spaces(): assert _parse_str_enum(ClassEnum, "DataFile") == ClassEnum.DataFile +def test_class_enum_constructor_accepts_spaced_or_unspaced_values(): + """ClassEnum constructor should tolerate whitespace differences.""" + from plexosdb.enums import ClassEnum + + assert ClassEnum("MT Schedule") == ClassEnum.MTSchedule + assert ClassEnum("MTSchedule") == ClassEnum.MTSchedule + assert ClassEnum("ST Schedule") == ClassEnum.STSchedule + assert ClassEnum("STSchedule") == ClassEnum.STSchedule + + def test_parse_str_enum_invalid_value_raises(): """Test _parse_str_enum raises ValueError for invalid value.""" from plexosdb.enums import _parse_str_enum, ClassEnum @@ -401,3 +394,19 @@ def test_parse_class_enum_and_collection_enum(): parse_class_enum("NotAClass") with pytest.raises(ValueError): parse_collection_enum("NotACollection") + + +def test_class_enum_contains_96_classes_for_v9_v10(): + """ClassEnum should cover the full 96 classes used by v9/v10.""" + from plexosdb.enums import ClassEnum, PLEXOS_CLASS_COUNT_V9_V10 + + assert PLEXOS_CLASS_COUNT_V9_V10 == 96 + assert len(ClassEnum) == 96 + + +def test_collection_count_constants_for_v92_and_v10(): + """Collection count constants should match documented schema totals.""" + from plexosdb.enums import PLEXOS_COLLECTION_COUNT_V10, PLEXOS_COLLECTION_COUNT_V92 + + assert PLEXOS_COLLECTION_COUNT_V92 == 776 + assert PLEXOS_COLLECTION_COUNT_V10 == 806 From 14dcfdf63e208e747f8e1987a6d60c116e8729fc Mon Sep 17 00:00:00 2001 From: mvelasqu Date: Thu, 7 May 2026 10:20:39 -0600 Subject: [PATCH 2/5] fix: add more comprehensive error type for memberships addition --- src/plexosdb/db.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/plexosdb/db.py b/src/plexosdb/db.py index 85a5a85..1f7370d 100644 --- a/src/plexosdb/db.py +++ b/src/plexosdb/db.py @@ -498,6 +498,27 @@ def add_membership( child_object_id = self.get_object_id(child_class_enum, child_object_name) collection_id = self.get_collection_id(collection_enum, parent_class_enum, child_class_enum) + if self.check_membership_exists( + parent_object_name, + child_object_name, + parent_class=parent_class_enum, + child_class=child_class_enum, + collection=collection_enum, + ): + existing_membership_id = self.get_membership_id( + parent_object_name, + child_object_name, + collection_enum, + ) + msg = ( + "Membership already exists: " + f"{parent_class_enum}:{parent_object_name} -> " + f"{child_class_enum}:{child_object_name} " + f"in collection {collection_enum} " + f"(membership_id={existing_membership_id})." + ) + raise ValueError(msg) + # NOTE: Measure if this is faster than passing the ids query = f""" INSERT INTO {Schema.Memberships.name} @@ -507,7 +528,14 @@ def add_membership( """ params = (parent_class_id, parent_object_id, collection_id, child_class_id, child_object_id) query_status = self._db.execute(query, params) - assert query_status, "Membership already exists for the parent and object combination." + if not query_status: + msg = ( + "Failed to add membership: " + f"{parent_class_enum}:{parent_object_name} -> " + f"{child_class_enum}:{child_object_name} " + f"in collection {collection_enum}." + ) + raise RuntimeError(msg) self._db.execute("UPDATE t_collection set is_enabled=1 where collection_id = ?", (collection_id,)) return self._db.last_insert_rowid() From b1d688e623292cc965e548143d932a68018bbb8c Mon Sep 17 00:00:00 2001 From: mvelasqu Date: Thu, 7 May 2026 10:21:03 -0600 Subject: [PATCH 3/5] feat: resolve different combinations of collection names for new enum types --- src/plexosdb/enums.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/plexosdb/enums.py b/src/plexosdb/enums.py index 3da9f84..84b305e 100644 --- a/src/plexosdb/enums.py +++ b/src/plexosdb/enums.py @@ -308,15 +308,29 @@ def get_default_collection(class_enum: ClassEnum) -> CollectionEnum: normalized_key = class_enum.value.replace(" ", "") + # Handle consonant+y plurals used by several classes (e.g., Facility -> Facilities). + plural_ies = ( + f"{normalized_key[:-1]}ies" + if ( + normalized_key.endswith("y") + and len(normalized_key) > 1 + and normalized_key[-2].lower() not in "aeiou" + ) + else None + ) + plural_es = f"{normalized_key}es" if normalized_key.endswith(("s", "x", "z", "ch", "sh")) else None + candidates = ( f"{normalized_key}s", + plural_es, + plural_ies, normalized_key, f"{class_enum.name}s", class_enum.name, ) for candidate in candidates: - if candidate in CollectionEnum.__members__: + if candidate and candidate in CollectionEnum.__members__: return CollectionEnum[candidate] raise KeyError(f"Default collection is not defined for class {class_enum.value!r}") From 28d439616e63f761d9817903f9438f17311ea265 Mon Sep 17 00:00:00 2001 From: mvelasqu Date: Thu, 7 May 2026 10:22:15 -0600 Subject: [PATCH 4/5] test: add and update tests to match current state of code base --- tests/test_enums_functions.py | 39 +++++++++++++++++++++++++++++++++++ tests/test_plexosdb_checks.py | 18 ++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/tests/test_enums_functions.py b/tests/test_enums_functions.py index cfcab06..7b89611 100644 --- a/tests/test_enums_functions.py +++ b/tests/test_enums_functions.py @@ -100,6 +100,34 @@ def test_get_default_collection_for_pasa(): assert result == CollectionEnum.PASA +@pytest.mark.parametrize( + "class_enum,expected_collection", + [ + ("Contingency", "Contingencies"), + ("Company", "Companies"), + ("Commodity", "Commodities"), + ("Facility", "Facilities"), + ("Entity", "Entities"), + ], +) +def test_get_default_collection_for_consonant_y_pluralization(class_enum: str, expected_collection: str): + """Verify get_default_collection resolves consonant+y classes to ...ies collections.""" + from plexosdb.enums import ClassEnum, CollectionEnum, get_default_collection + + class_obj = getattr(ClassEnum, class_enum) + result = get_default_collection(class_obj) + expected_obj = getattr(CollectionEnum, expected_collection) + assert result == expected_obj + + +def test_get_default_collection_for_es_pluralization(): + """Verify get_default_collection resolves +es plurals (e.g., Process -> Processes).""" + from plexosdb.enums import ClassEnum, CollectionEnum, get_default_collection + + result = get_default_collection(ClassEnum.Process) + assert result == CollectionEnum.Processes + + def test_schema_enum_attributes_member(): """Verify Schema.Attributes enum member exists.""" from plexosdb.enums import Schema @@ -344,6 +372,17 @@ def test_get_default_collection_for_supported_classes(): assert isinstance(result, CollectionEnum) +def test_get_default_collection_for_all_non_system_classes(): + """All classes except System should resolve to a default collection.""" + from plexosdb.enums import ClassEnum, CollectionEnum, get_default_collection + + for class_member in ClassEnum: + if class_member == ClassEnum.System: + continue + result = get_default_collection(class_member) + assert isinstance(result, CollectionEnum) + + def test_schema_enum_name_and_label_properties(): """Test Schema enum name and label properties for all members.""" from plexosdb.enums import Schema diff --git a/tests/test_plexosdb_checks.py b/tests/test_plexosdb_checks.py index d5680a4..90decb8 100644 --- a/tests/test_plexosdb_checks.py +++ b/tests/test_plexosdb_checks.py @@ -268,6 +268,24 @@ def test_check_membership_exists_returns_true_for_valid_membership(db_base): assert result +@pytest.mark.checks +def test_add_membership_duplicate_raises_detailed_error(db_base): + """Verify duplicate membership errors include parent, child, and collection context.""" + from plexosdb.enums import ClassEnum, CollectionEnum + + db = db_base + db.add_object(ClassEnum.Generator, "Gen1") + db.add_object(ClassEnum.Node, "Node1") + db.add_membership(ClassEnum.Generator, ClassEnum.Node, "Gen1", "Node1", CollectionEnum.Nodes) + + with pytest.raises(ValueError, match="Gen1") as exc_info: + db.add_membership(ClassEnum.Generator, ClassEnum.Node, "Gen1", "Node1", CollectionEnum.Nodes) + + message = str(exc_info.value) + assert "Node1" in message + assert "Nodes" in message + + @pytest.mark.checks @pytest.mark.parametrize( "parent_name,child_name", From 59339bb70cbc07196323006db2cbfbaefbb98bc2 Mon Sep 17 00:00:00 2001 From: mvelasqu Date: Thu, 7 May 2026 10:28:48 -0600 Subject: [PATCH 5/5] docs: update docs to match recent additions of the gas library --- docs/source/howtos/gas_library_tutorial.md | 276 +++++++++++++++++++++ docs/source/howtos/index.md | 6 + docs/source/index.md | 6 + docs/source/tutorial.md | 6 + 4 files changed, 294 insertions(+) create mode 100644 docs/source/howtos/gas_library_tutorial.md diff --git a/docs/source/howtos/gas_library_tutorial.md b/docs/source/howtos/gas_library_tutorial.md new file mode 100644 index 0000000..3b7bcbb --- /dev/null +++ b/docs/source/howtos/gas_library_tutorial.md @@ -0,0 +1,276 @@ +# Gas Library: Systems and Memberships + +This guide shows how to create Gas Library objects and memberships using +`ClassEnum` and `CollectionEnum`. + +```{note} +Most examples in the current docs are Electric Library oriented. +Use this page as the Gas Library counterpart for object and membership setup. +``` + +## Discover Valid Gas Collections First + +Start from an existing Gas XML and inspect available parent-child collection +combinations. + +```python +from plexosdb import PlexosDB +from plexosdb.enums import ClassEnum + +db = PlexosDB.from_xml("/path/to/gas_sys.xml") +print(db.list_collections(parent_class=ClassEnum.GasNode)) +``` + +Example result for `ClassEnum.GasNode`: + +```text +[ + {'collection_id': 427, 'collection_name': 'Template', 'parent_class_name': 'Gas Node', 'child_class_name': 'Gas Node'}, + {'collection_id': 429, 'collection_name': 'Gas Zones', 'parent_class_name': 'Gas Node', 'child_class_name': 'Gas Zone'}, + {'collection_id': 431, 'collection_name': 'Gas Paths', 'parent_class_name': 'Gas Node', 'child_class_name': 'Gas Path'}, + {'collection_id': 432, 'collection_name': 'Facilities', 'parent_class_name': 'Gas Node', 'child_class_name': 'Facility'}, + {'collection_id': 433, 'collection_name': 'Markets', 'parent_class_name': 'Gas Node', 'child_class_name': 'Market'}, + {'collection_id': 434, 'collection_name': 'Constraints', 'parent_class_name': 'Gas Node', 'child_class_name': 'Constraint'}, + {'collection_id': 435, 'collection_name': 'Objectives', 'parent_class_name': 'Gas Node', 'child_class_name': 'Objective'}, + {'collection_id': 430, 'collection_name': 'Gas Transports', 'parent_class_name': 'Gas Node', 'child_class_name': 'Gas Transport'} +] +``` + +Use this output to choose valid memberships for your model. + +## Create Core Gas Objects + +```python +from plexosdb import PlexosDB +from plexosdb.enums import ClassEnum + +# Reuse a Gas XML-backed database so Gas classes/collections exist. +db = PlexosDB.from_xml("/path/to/gas_sys.xml") + +# Core gas objects +db.add_object(ClassEnum.GasNode, "GN_x") +db.add_object(ClassEnum.GasZone, "GZ_x") +db.add_object(ClassEnum.GasPath, "GP_x") +db.add_object(ClassEnum.GasTransport, "GT_x") + +# Related classes frequently used with Gas Node +db.add_object(ClassEnum.Facility, "FAC_x") +db.add_object(ClassEnum.Market, "MKT_x") +``` + +## Create Extended Heat + Gas Objects + +Use this block when you want a broader sandbox that includes the full Heat/Gas +set below: + +- `HeatPlant`, `HeatNode`, `HeatStorage` +- `GasField`, `GasPlant`, `GasPipeline`, `GasNode`, `GasStorage` +- `GasDemand`, `GasDSMProgram`, `GasBasin`, `GasZone` +- `GasContract`, `GasTransport`, `GasPath`, `GasCapacityReleaseOffer` + +```python +from plexosdb.enums import ClassEnum + +db = PlexosDB.from_xml("/path/to/gas_sys.xml") + +objects_to_create = [ + (ClassEnum.HeatPlant, "HP_1"), + (ClassEnum.HeatNode, "HN_1"), + (ClassEnum.HeatStorage, "HS_1"), + (ClassEnum.GasField, "GF_1"), + (ClassEnum.GasPlant, "GPL_1"), + (ClassEnum.GasPipeline, "GPI_1"), + (ClassEnum.GasNode, "GN_1"), + (ClassEnum.GasStorage, "GS_1"), + (ClassEnum.GasDemand, "GD_1"), + (ClassEnum.GasDSMProgram, "GDSM_1"), + (ClassEnum.GasBasin, "GB_1"), + (ClassEnum.GasZone, "GZ_1"), + (ClassEnum.GasContract, "GC_1"), + (ClassEnum.GasTransport, "GT_1"), + (ClassEnum.GasPath, "GPA_1"), + (ClassEnum.GasCapacityReleaseOffer, "GCRO_1"), +] + +for class_enum, object_name in objects_to_create: + if not db.check_object_exists(class_enum, object_name): + db.add_object(class_enum, object_name) +``` + +## Create Heat/Gas Memberships for a Full Chain + +```python +from plexosdb.enums import CollectionEnum + +# Verify valid collections for each parent class before adding links. +print(db.list_collections(parent_class=ClassEnum.GasField)) +print(db.list_collections(parent_class=ClassEnum.GasContract)) +print(db.list_collections(parent_class=ClassEnum.GasTransport)) +print(db.list_collections(parent_class=ClassEnum.GasDemand)) + +# Gas Field -> Gas Basin +db.add_membership( + parent_class_enum=ClassEnum.GasField, + child_class_enum=ClassEnum.GasBasin, + parent_object_name="GF_1", + child_object_name="GB_1", + collection_enum=CollectionEnum.GasBasins, +) + +# Gas Field -> Gas Node +db.add_membership( + parent_class_enum=ClassEnum.GasField, + child_class_enum=ClassEnum.GasNode, + parent_object_name="GF_1", + child_object_name="GN_1", + collection_enum=CollectionEnum.GasNodes, +) + +# Gas Contract -> Gas Field +db.add_membership( + parent_class_enum=ClassEnum.GasContract, + child_class_enum=ClassEnum.GasField, + parent_object_name="GC_1", + child_object_name="GF_1", + collection_enum=CollectionEnum.GasFields, +) + +# Gas Contract -> Gas Transport +db.add_membership( + parent_class_enum=ClassEnum.GasContract, + child_class_enum=ClassEnum.GasTransport, + parent_object_name="GC_1", + child_object_name="GT_1", + collection_enum=CollectionEnum.GasTransports, +) + +# Gas Transport -> Gas Path +db.add_membership( + parent_class_enum=ClassEnum.GasTransport, + child_class_enum=ClassEnum.GasPath, + parent_object_name="GT_1", + child_object_name="GPA_1", + collection_enum=CollectionEnum.GasPaths, +) + +# Gas Demand -> Gas Node +db.add_membership( + parent_class_enum=ClassEnum.GasDemand, + child_class_enum=ClassEnum.GasNode, + parent_object_name="GD_1", + child_object_name="GN_1", + collection_enum=CollectionEnum.GasNodes, +) +``` + +## Create Model/Scenario Variants (System Alternatives) + +In PLEXOS there is one `System` object. To represent different "systems" for +study purposes, create separate `Model` and `Scenario` objects and link them. + +```python +from plexosdb.enums import ClassEnum, CollectionEnum + +# Model variants (system alternatives) +db.add_object(ClassEnum.Model, "GasSystem_Base") +db.add_object(ClassEnum.Model, "GasSystem_Expansion") + +# Scenario variants +db.add_object(ClassEnum.Scenario, "Base") +db.add_object(ClassEnum.Scenario, "HighDemand") +db.add_object(ClassEnum.Scenario, "LowSupply") + +# Link each model to scenarios +db.add_membership( + parent_class_enum=ClassEnum.Model, + child_class_enum=ClassEnum.Scenario, + parent_object_name="GasSystem_Base", + child_object_name="Base", + collection_enum=CollectionEnum.Scenarios, +) + +db.add_membership( + parent_class_enum=ClassEnum.Model, + child_class_enum=ClassEnum.Scenario, + parent_object_name="GasSystem_Expansion", + child_object_name="HighDemand", + collection_enum=CollectionEnum.Scenarios, +) + +db.add_membership( + parent_class_enum=ClassEnum.Model, + child_class_enum=ClassEnum.Scenario, + parent_object_name="GasSystem_Expansion", + child_object_name="LowSupply", + collection_enum=CollectionEnum.Scenarios, +) +``` + +## Create Gas Memberships with `CollectionEnum` + +```python +from plexosdb.enums import CollectionEnum + +# Note: +# db.add_object(...) already creates the default System -> object membership. + +# Gas Node -> Gas Zone +db.add_membership( + parent_class_enum=ClassEnum.GasNode, + child_class_enum=ClassEnum.GasZone, + parent_object_name="GN_x", + child_object_name="GZ_x", + collection_enum=CollectionEnum.GasZones, +) + +# Gas Node -> Gas Path +db.add_membership( + parent_class_enum=ClassEnum.GasNode, + child_class_enum=ClassEnum.GasPath, + parent_object_name="GN_x", + child_object_name="GP_x", + collection_enum=CollectionEnum.GasPaths, +) + +# Gas Node -> Gas Transport +db.add_membership( + parent_class_enum=ClassEnum.GasNode, + child_class_enum=ClassEnum.GasTransport, + parent_object_name="GN_x", + child_object_name="GT_x", + collection_enum=CollectionEnum.GasTransports, +) + +# Gas Node -> Facility +db.add_membership( + parent_class_enum=ClassEnum.GasNode, + child_class_enum=ClassEnum.Facility, + parent_object_name="GN_x", + child_object_name="FAC_x", + collection_enum=CollectionEnum.Facilities, +) + +# Gas Node -> Market +db.add_membership( + parent_class_enum=ClassEnum.GasNode, + child_class_enum=ClassEnum.Market, + parent_object_name="GN_x", + child_object_name="MKT_x", + collection_enum=CollectionEnum.Markets, +) +``` + +## Mapping Display Names to Enums + +`db.list_collections()` returns collection names as display text (for example, +`"Gas Zones"`). For API calls, use `CollectionEnum` values (for example, +`CollectionEnum.GasZones`). + +If you need to parse display text dynamically, use `parse_collection_enum`: + +```python +from plexosdb.enums import parse_collection_enum + +collection_enum = parse_collection_enum("Gas Zones") +assert collection_enum == CollectionEnum.GasZones +``` diff --git a/docs/source/howtos/index.md b/docs/source/howtos/index.md index d05b2f5..7ae0bfd 100644 --- a/docs/source/howtos/index.md +++ b/docs/source/howtos/index.md @@ -6,11 +6,17 @@ This section contains practical guides for common tasks with PlexosDB. +```{note} +Most existing examples in this documentation are based on the Electric Library. +Use the Gas-specific guide below for Gas Library object and membership patterns. +``` + ```{toctree} :maxdepth: 1 :caption: Contents create_db +gas_library_tutorial add_objects add_properties delete_objects diff --git a/docs/source/index.md b/docs/source/index.md index 3fccd3b..80c65e8 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -21,6 +21,12 @@ simulation models. The library converts PLEXOS XML files into SQLite databases and offers a comprehensive API for creating, querying, and manipulating energy system models. +```{note} +Most examples in the current documentation are based on Electric Library +workflows. For Gas Library object and membership patterns, see +[Gas Library: Systems and Memberships](howtos/gas_library_tutorial). +``` + ### Key Features PlexosDB offers the following capabilities: diff --git a/docs/source/tutorial.md b/docs/source/tutorial.md index 0cfa977..643399d 100644 --- a/docs/source/tutorial.md +++ b/docs/source/tutorial.md @@ -4,6 +4,12 @@ This tutorial provides a step-by-step introduction to PlexosDB, guiding you through the essential concepts and operations for working with PLEXOS energy market simulation models. +```{note} +Tutorial examples below are primarily Electric Library oriented. For Gas +Library object and membership examples, see +[Gas Library: Systems and Memberships](howtos/gas_library_tutorial). +``` + ## Prerequisites Before starting this tutorial, ensure you have: