From 6a7c75d50bad81fa15bec8663af10e705825aee4 Mon Sep 17 00:00:00 2001 From: Luke Lowery Date: Tue, 3 Feb 2026 15:33:44 -0600 Subject: [PATCH 1/3] Support for Python 3.12-3.13 --- .github/workflows/python-package.yml | 2 +- CHANGELOG.md | 7 +++ VERSION | 2 +- docs/dev/components.rst | 14 ++--- esapp/components/README.md | 19 +++++++ esapp/components/gobject.py | 41 +++++++-------- esapp/indexable.py | 28 +++++----- esapp/utils/dynamics.py | 2 +- esapp/utils/gic.py | 6 +-- tests/conftest.py | 8 +-- tests/test_gobject.py | 34 ++++++------- tests/test_grid_components.py | 52 +++++++++---------- tests/test_indexing.py | 76 ++++++++++++++-------------- tests/test_integration_workbench.py | 18 +++---- 14 files changed, 164 insertions(+), 145 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 2fca7a7e..3c3e5d6f 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index eceebd6a..40315ea4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +[0.1.3] - 2026-02-03 +-------------------- + +**Changed** +- Replaced deprecated `@classmethod @property` pattern in `GObject` with standard `@classmethod` methods for Python 3.13 compatibility (e.g. `Bus.keys` is now `Bus.keys()`) +- Added Python 3.12 and 3.13 to CI test matrix + [0.1.2] - 2026-02-03 -------------------- diff --git a/VERSION b/VERSION index 8294c184..7693c96b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.2 \ No newline at end of file +0.1.3 \ No newline at end of file diff --git a/docs/dev/components.rst b/docs/dev/components.rst index a342a56e..3e427f0b 100644 --- a/docs/dev/components.rst +++ b/docs/dev/components.rst @@ -40,15 +40,15 @@ define fields with ``(PowerWorld name, data type, priority flags)``: ObjectString = 'Bus' # Sets Bus.TYPE — must be last member -The base class collects these into queryable properties: +The base class collects these into queryable classmethods: .. code-block:: python - Bus.TYPE # 'Bus' - PowerWorld object type (from ObjectString) - Bus.keys # ['BusNum'] - primary key fields - Bus.fields # all field names - Bus.secondary # alternate identifier fields - Bus.editable # user-modifiable fields + Bus.TYPE() # 'Bus' - PowerWorld object type (from ObjectString) + Bus.keys() # ['BusNum'] - primary key fields + Bus.fields() # all field names + Bus.secondary() # alternate identifier fields + Bus.editable() # user-modifiable fields Field Priority Flags ~~~~~~~~~~~~~~~~~~~~ @@ -131,4 +131,4 @@ API Stability ESA++ uses semantic versioning: - **Public API** (stable): PowerWorld, component classes, exception types -- **Internal API** (may change): SAW mixins, Indexable internals, GObject metaclass +- **Internal API** (may change): SAW mixins, Indexable internals, GObject internals diff --git a/esapp/components/README.md b/esapp/components/README.md index 79667556..1e00fb1f 100644 --- a/esapp/components/README.md +++ b/esapp/components/README.md @@ -47,6 +47,25 @@ wb.dyn.watch(Gen, [TS.Gen.P, TS.Gen.Speed, TS.Gen.Delta]) --- +# GObject Schema Access + +Each `GObject` subclass (like `Bus`, `Gen`) exposes its schema via `@classmethod` methods: + +```python +Bus.TYPE() # 'Bus' — the PowerWorld object type string +Bus.keys() # ['BusNum'] — primary key fields +Bus.fields() # all field names +Bus.secondary() # alternate identifier fields +Bus.editable() # user-modifiable fields +Bus.identifiers() # keys + secondary (as a set) +Bus.settable() # identifiers + editable (as a set) +``` + +These are populated automatically during class creation by `GObject.__new__`, +which inspects each member's `FieldPriority` flags. + +--- + # PWRaw File Schema Description **File Format:** Tab-Separated Values (TSV) diff --git a/esapp/components/gobject.py b/esapp/components/gobject.py index 401d9185..7950752e 100644 --- a/esapp/components/gobject.py +++ b/esapp/components/gobject.py @@ -36,8 +36,8 @@ class GObject(Enum): Subclasses should define their members to build the schema. The class automatically populates `_FIELDS`, `_KEYS`, and `_TYPE` attributes - based on its member definitions. These are exposed through the `fields`, - `keys`, and `TYPE` class properties. + based on its member definitions. These are exposed through the `fields()`, + `keys()`, and `TYPE()` classmethods. Example: -------- @@ -67,21 +67,21 @@ def __new__(cls, *args): cls._SECONDARY = [] if '_EDITABLE' not in cls.__dict__: cls._EDITABLE = [] - + # The object type string name is the only argument for this member if len(args) == 1: cls._TYPE = args[0] - + # Set integer and name as member value value = len(cls.__members__) + 1 obj = object.__new__(cls) obj._value_ = value return obj - + # Everything else is a field with (name, dtype, priority) else: - field_name_str, field_dtype, field_priority = args + field_name_str, field_dtype, field_priority = args # Set integer and name as member value value = len(cls.__members__) + 1 @@ -104,53 +104,51 @@ def __new__(cls, *args): cls._EDITABLE.append(field_name_str) return obj - + def __repr__(self) -> str: # For the type-defining member, show the type. if isinstance(self._value_, int): - return f'<{self.__class__.__name__}.{self.name}: TYPE={self.__class__.TYPE}>' + return f'<{self.__class__.__name__}.{self.name}: TYPE={self.__class__.TYPE()}>' # For field members, show the field info. return f'<{self.__class__.__name__}.{self.name}: Field={self._value_[1]}>' - + def __str__(self) -> str: # For the type-defining member, it has no string field name. if isinstance(self._value_, int): return self.name # For field members, return the PowerWorld field name string. return str(self._value_[1]) - + @classmethod - @property def keys(cls): return getattr(cls, '_KEYS', []) - + @classmethod - @property def fields(cls): return getattr(cls, '_FIELDS', []) @classmethod - @property def secondary(cls): """Secondary identifier fields (used with primary keys to identify records).""" return getattr(cls, '_SECONDARY', []) @classmethod - @property def editable(cls): return getattr(cls, '_EDITABLE', []) @classmethod - @property def identifiers(cls): """All identifier fields: primary keys + secondary keys.""" return set(getattr(cls, '_KEYS', [])) | set(getattr(cls, '_SECONDARY', [])) @classmethod - @property def settable(cls): """Fields that can be set: identifiers (primary + secondary keys) + editable fields.""" - return cls.identifiers | set(getattr(cls, '_EDITABLE', [])) + return cls.identifiers() | set(getattr(cls, '_EDITABLE', [])) + + @classmethod + def TYPE(cls): + return getattr(cls, '_TYPE', 'NO_OBJECT_NAME') @classmethod def is_editable(cls, field_name: str) -> bool: @@ -160,9 +158,4 @@ def is_editable(cls, field_name: str) -> bool: @classmethod def is_settable(cls, field_name: str) -> bool: """Check if a field can be set (either a key or editable).""" - return field_name in cls.settable - - @classmethod - @property - def TYPE(cls): - return getattr(cls, '_TYPE', 'NO_OBJECT_NAME') \ No newline at end of file + return field_name in cls.settable() diff --git a/esapp/indexable.py b/esapp/indexable.py index 96c8b02a..be280cbd 100644 --- a/esapp/indexable.py +++ b/esapp/indexable.py @@ -82,7 +82,7 @@ def __getitem__(self, index) -> Optional[DataFrame]: # 2. Determine the complete set of fields to retrieve. # Always start with the object's key fields. - fields_to_get = set(gtype.keys) + fields_to_get = set(gtype.keys()) # 3. Add any additional fields based on the request. if requested_fields is None: @@ -90,7 +90,7 @@ def __getitem__(self, index) -> Optional[DataFrame]: pass elif requested_fields == slice(None): # Case: pw[Bus, :] -> add all defined fields. - fields_to_get.update(gtype.fields) + fields_to_get.update(gtype.fields()) else: # Case: pw[Bus, 'field'] or pw[Bus, ['f1', 'f2']] # Normalize to an iterable to handle single or multiple fields. @@ -110,7 +110,7 @@ def __getitem__(self, index) -> Optional[DataFrame]: return None # 5. Retrieve data from PowerWorld - return self.esa.GetParamsRectTyped(gtype.TYPE, sorted(list(fields_to_get))) + return self.esa.GetParamsRectTyped(gtype.TYPE(), sorted(list(fields_to_get))) def __setitem__(self, args, value) -> None: """ @@ -185,7 +185,7 @@ def _bulk_update_from_df(self, gtype: Type[GObject], df: DataFrame): *"not found"*: a. Check whether the DataFrame contains all **primary keys** - (``gtype.keys``). If any are missing, raise ``ValueError`` + (``gtype.keys()``). If any are missing, raise ``ValueError`` — we cannot identify/create objects without them. b. Fall back to ``ChangeParametersMultipleElement`` which iterates row-by-row. When the SAW property @@ -208,7 +208,7 @@ def _bulk_update_from_df(self, gtype: Type[GObject], df: DataFrame): The GObject subclass representing the type of objects to update. df : pandas.DataFrame The DataFrame containing object data. Must include all - primary key columns (``gtype.keys``). + primary key columns (``gtype.keys()``). Raises ------ @@ -225,17 +225,17 @@ def _bulk_update_from_df(self, gtype: Type[GObject], df: DataFrame): non_settable = [c for c in df.columns if not gtype.is_settable(c)] if non_settable: raise ValueError( - f"Cannot set read-only field(s) on {gtype.TYPE}: {non_settable}" + f"Cannot set read-only field(s) on {gtype.TYPE()}: {non_settable}" ) try: - self.esa.ChangeParametersMultipleElementRect(gtype.TYPE, df.columns.tolist(), df) + self.esa.ChangeParametersMultipleElementRect(gtype.TYPE(), df.columns.tolist(), df) except PowerWorldPrerequisiteError as e: if "not found" in str(e).lower(): - missing_keys = set(gtype.keys) - set(df.columns) + missing_keys = set(gtype.keys()) - set(df.columns) if missing_keys: raise ValueError( - f"Missing required primary key field(s) for {gtype.TYPE}: {missing_keys}. " + f"Missing required primary key field(s) for {gtype.TYPE()}: {missing_keys}. " f"All primary keys must be included to create new objects." ) from e # Primary keys present — fall back to @@ -245,7 +245,7 @@ def _bulk_update_from_df(self, gtype: Type[GObject], df: DataFrame): cols = df.columns.tolist() values = df.values.tolist() try: - self.esa.ChangeParametersMultipleElement(gtype.TYPE, cols, values) + self.esa.ChangeParametersMultipleElement(gtype.TYPE(), cols, values) except PowerWorldPrerequisiteError as create_err: if 'not found' not in str(create_err).lower(): raise @@ -277,11 +277,11 @@ def _broadcast_update_to_fields(self, gtype: Type[GObject], fields: list[str], v non_settable = [f for f in fields if not gtype.is_settable(f)] if non_settable: raise ValueError( - f"Cannot set read-only field(s) on {gtype.TYPE}: {non_settable}" + f"Cannot set read-only field(s) on {gtype.TYPE()}: {non_settable}" ) # For objects without keys (e.g., Sim_Solution_Options), we construct # the change DataFrame directly without reading from PowerWorld first. - if not gtype.keys: + if not gtype.keys(): data_dict = {} if len(fields) == 1: data_dict[fields[0]] = [value] @@ -297,7 +297,7 @@ def _broadcast_update_to_fields(self, gtype: Type[GObject], fields: list[str], v # For objects with keys, we first get the keys (primary keys) # of all existing objects to ensure we only modify what's already there. else: - keys = gtype.keys + keys = gtype.keys() change_df = self[gtype, keys] if change_df is None or change_df.empty: @@ -313,4 +313,4 @@ def _broadcast_update_to_fields(self, gtype: Type[GObject], fields: list[str], v change_df[fields] = value # Send the minimal DataFrame to PowerWorld. - self.esa.ChangeParametersMultipleElementRect(gtype.TYPE, change_df.columns.tolist(), change_df) \ No newline at end of file + self.esa.ChangeParametersMultipleElementRect(gtype.TYPE(), change_df.columns.tolist(), change_df) \ No newline at end of file diff --git a/esapp/utils/dynamics.py b/esapp/utils/dynamics.py index 28f606e1..06b6d037 100644 --- a/esapp/utils/dynamics.py +++ b/esapp/utils/dynamics.py @@ -68,7 +68,7 @@ def prepare(self, pw) -> List[str]: """ fields = [] for gtype, flds in self._watch_fields.items(): - pw.esa.TSResultStorageSetAll(object=gtype.TYPE, value=True) + pw.esa.TSResultStorageSetAll(object=gtype.TYPE(), value=True) objs = pw[gtype, ['ObjectID']] diff --git a/esapp/utils/gic.py b/esapp/utils/gic.py index 08a0d171..98595c31 100644 --- a/esapp/utils/gic.py +++ b/esapp/utils/gic.py @@ -216,8 +216,8 @@ def settings(self, value: Optional[DataFrame] = None) -> Optional[DataFrame]: Current settings if value is None. """ return self._pw.esa.GetParametersMultipleElement( - GIC_Options_Value.TYPE, - GIC_Options_Value.fields + GIC_Options_Value.TYPE(), + GIC_Options_Value.fields() )[['VariableName', 'ValueField']] def timevary_csv(self, fpath: str) -> None: @@ -234,7 +234,7 @@ def timevary_csv(self, fpath: str) -> None: Branch '1' '2' '2', 0.1, 0.11, 0.14 """ csv = read_csv(fpath, header=None) - obj = GICInputVoltObject.TYPE + obj = GICInputVoltObject.TYPE() fields = ['WhoAmI'] + [f'GICObjectInputDCVolt:{i+1}' for i in range(csv.columns.size - 1)] for row in csv.to_records(False): diff --git a/tests/conftest.py b/tests/conftest.py index f19ec7b1..0b796a65 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -347,13 +347,13 @@ def get_sample_gobject_subclasses(require_keys=False, require_multiple_editable= # Apply filters if requested if require_keys: - all_classes = [c for c in all_classes if hasattr(c, 'keys') and c.keys] + all_classes = [c for c in all_classes if hasattr(c, 'keys') and c.keys()] if require_editable_non_key: def has_editable_non_key(cls): if not hasattr(cls, 'editable') or not hasattr(cls, 'keys'): return False - editable_non_key = [f for f in cls.editable if f not in cls.keys] + editable_non_key = [f for f in cls.editable() if f not in cls.keys()] return len(editable_non_key) >= 1 all_classes = [c for c in all_classes if has_editable_non_key(c)] @@ -361,7 +361,7 @@ def has_editable_non_key(cls): def has_multiple_editable(cls): if not hasattr(cls, 'editable') or not hasattr(cls, 'keys'): return False - editable_non_key = [f for f in cls.editable if f not in cls.keys] + editable_non_key = [f for f in cls.editable() if f not in cls.keys()] return len(editable_non_key) >= 2 all_classes = [c for c in all_classes if has_multiple_editable(c)] @@ -371,7 +371,7 @@ def has_multiple_editable(cls): sample = [] for type_name in priority_types: for cls in all_classes: - if hasattr(cls, 'TYPE') and cls.TYPE == type_name: + if hasattr(cls, 'TYPE') and cls.TYPE() == type_name: sample.append(cls) break diff --git a/tests/test_gobject.py b/tests/test_gobject.py index c766fcb1..075161fa 100644 --- a/tests/test_gobject.py +++ b/tests/test_gobject.py @@ -72,40 +72,40 @@ class TestGObjectProperties: def test_keys_property(self): """keys property returns primary key fields.""" - assert isinstance(grid.Bus.keys, list) - assert "BusNum" in grid.Bus.keys + assert isinstance(grid.Bus.keys(), list) + assert "BusNum" in grid.Bus.keys() def test_fields_property(self): """fields property returns all field names.""" - assert isinstance(grid.Bus.fields, list) - assert len(grid.Bus.fields) > 0 + assert isinstance(grid.Bus.fields(), list) + assert len(grid.Bus.fields()) > 0 def test_secondary_property(self): """secondary property returns secondary identifier fields.""" # Gen has secondary identifiers (alternate keys, base values) - assert isinstance(grid.Gen.secondary, list) + assert isinstance(grid.Gen.secondary(), list) # BusName_NomVolt is a secondary identifier (alternate key) - assert "BusName_NomVolt" in grid.Gen.secondary + assert "BusName_NomVolt" in grid.Gen.secondary() def test_editable_property(self): """editable property returns editable fields.""" - assert isinstance(grid.Bus.editable, list) + assert isinstance(grid.Bus.editable(), list) def test_identifiers_property(self): """identifiers includes both primary and secondary keys.""" - identifiers = grid.Gen.identifiers + identifiers = grid.Gen.identifiers() assert isinstance(identifiers, set) assert "BusNum" in identifiers # Primary key assert "GenID" in identifiers # Primary key (composite) def test_settable_property(self): """settable includes identifiers and editable fields.""" - settable = grid.Bus.settable + settable = grid.Bus.settable() assert isinstance(settable, set) def test_is_editable_method(self): """is_editable correctly identifies editable fields.""" - editable_fields = grid.Bus.editable + editable_fields = grid.Bus.editable() if editable_fields: assert grid.Bus.is_editable(editable_fields[0]) is True # Non-existent field should return False @@ -114,16 +114,16 @@ def test_is_editable_method(self): def test_is_settable_method(self): """is_settable correctly identifies settable fields.""" # Key fields are settable - key = grid.Bus.keys[0] + key = grid.Bus.keys()[0] assert grid.Bus.is_settable(key) is True # Non-existent field should return False assert grid.Bus.is_settable("NonExistentField") is False def test_type_property(self): """TYPE property returns the PowerWorld object type string.""" - assert grid.Bus.TYPE == "Bus" - assert grid.Gen.TYPE == "Gen" - assert grid.Load.TYPE == "Load" + assert grid.Bus.TYPE() == "Bus" + assert grid.Gen.TYPE() == "Gen" + assert grid.Load.TYPE() == "Load" class TestGObjectWithoutType: @@ -132,12 +132,12 @@ class TestGObjectWithoutType: def test_base_gobject_type_default(self): """Base GObject class returns default TYPE when not set.""" # GObject itself has no _TYPE attribute, should return default - assert GObject.TYPE == "NO_OBJECT_NAME" + assert GObject.TYPE() == "NO_OBJECT_NAME" def test_base_gobject_empty_keys(self): """Base GObject class returns empty keys list.""" - assert GObject.keys == [] + assert GObject.keys() == [] def test_base_gobject_empty_fields(self): """Base GObject class returns empty fields list.""" - assert GObject.fields == [] + assert GObject.fields() == [] diff --git a/tests/test_grid_components.py b/tests/test_grid_components.py index 809964b5..fe222897 100644 --- a/tests/test_grid_components.py +++ b/tests/test_grid_components.py @@ -1,5 +1,5 @@ """ -Unit tests for GObject metaclass and auto-generated component classes. +Unit tests for GObject classmethods and auto-generated component classes. These are **unit tests** that do NOT require PowerWorld Simulator. They test field collection, key/editable/settable classification, and validate that all @@ -17,7 +17,7 @@ @pytest.fixture(scope="module") def test_gobject_class() -> Type[grid.GObject]: - """A simple GObject subclass for testing metaclass behavior.""" + """A simple GObject subclass for testing classmethod behavior.""" class TestGObject(grid.GObject): ID = ("id", int, grid.FieldPriority.PRIMARY) NAME = ("name", str, grid.FieldPriority.SECONDARY | grid.FieldPriority.REQUIRED) @@ -28,33 +28,33 @@ class TestGObject(grid.GObject): def test_gobject_fields_are_collected(test_gobject_class): - """All field names are collected in the .fields property.""" - assert test_gobject_class.fields == ['id', 'name', 'value', 'duplicate_key'] + """All field names are collected in the .fields() method.""" + assert test_gobject_class.fields() == ['id', 'name', 'value', 'duplicate_key'] def test_gobject_keys_are_collected(test_gobject_class): - """PRIMARY fields are collected in .keys.""" - assert test_gobject_class.keys == ['id', 'duplicate_key'] + """PRIMARY fields are collected in .keys().""" + assert test_gobject_class.keys() == ['id', 'duplicate_key'] def test_gobject_editable_fields(test_gobject_class): - """EDITABLE fields are collected in .editable.""" - assert test_gobject_class.editable == ['value'] + """EDITABLE fields are collected in .editable().""" + assert test_gobject_class.editable() == ['value'] def test_gobject_secondary_fields(test_gobject_class): - """SECONDARY fields are collected in .secondary.""" - assert test_gobject_class.secondary == ['name', 'duplicate_key'] + """SECONDARY fields are collected in .secondary().""" + assert test_gobject_class.secondary() == ['name', 'duplicate_key'] def test_gobject_identifiers(test_gobject_class): - """identifiers returns union of primary + secondary keys.""" - assert test_gobject_class.identifiers == {'id', 'name', 'duplicate_key'} + """identifiers() returns union of primary + secondary keys.""" + assert test_gobject_class.identifiers() == {'id', 'name', 'duplicate_key'} def test_gobject_settable_fields(test_gobject_class): - """settable returns identifiers + editable fields.""" - assert test_gobject_class.settable == {'id', 'name', 'duplicate_key', 'value'} + """settable() returns identifiers + editable fields.""" + assert test_gobject_class.settable() == {'id', 'name', 'duplicate_key', 'value'} def test_gobject_is_editable(test_gobject_class): @@ -73,18 +73,18 @@ def test_gobject_is_settable(test_gobject_class): @pytest.mark.parametrize("g_object_class", get_all_gobject_subclasses()) def test_real_gobject_subclass_is_well_formed(g_object_class: Type[grid.GObject]): """Validates every auto-generated GObject subclass has correct structure.""" - assert g_object_class.TYPE != 'NO_OBJECT_NAME', f"{g_object_class.__name__} missing ObjectString" - assert isinstance(g_object_class.TYPE, str) - assert isinstance(g_object_class.fields, list) - assert isinstance(g_object_class.keys, list) - assert isinstance(g_object_class.editable, list) + assert g_object_class.TYPE() != 'NO_OBJECT_NAME', f"{g_object_class.__name__} missing ObjectString" + assert isinstance(g_object_class.TYPE(), str) + assert isinstance(g_object_class.fields(), list) + assert isinstance(g_object_class.keys(), list) + assert isinstance(g_object_class.editable(), list) - assert set(g_object_class.keys).issubset(set(g_object_class.fields)) - assert set(g_object_class.editable).issubset(set(g_object_class.fields)) - assert set(g_object_class.secondary).issubset(set(g_object_class.fields)) + assert set(g_object_class.keys()).issubset(set(g_object_class.fields())) + assert set(g_object_class.editable()).issubset(set(g_object_class.fields())) + assert set(g_object_class.secondary()).issubset(set(g_object_class.fields())) - expected_identifiers = set(g_object_class.keys) | set(g_object_class.secondary) - assert g_object_class.identifiers == expected_identifiers + expected_identifiers = set(g_object_class.keys()) | set(g_object_class.secondary()) + assert g_object_class.identifiers() == expected_identifiers - expected_settable = expected_identifiers | set(g_object_class.editable) - assert g_object_class.settable == expected_settable + expected_settable = expected_identifiers | set(g_object_class.editable()) + assert g_object_class.settable() == expected_settable diff --git a/tests/test_indexing.py b/tests/test_indexing.py index df321d6c..346bd870 100644 --- a/tests/test_indexing.py +++ b/tests/test_indexing.py @@ -25,22 +25,22 @@ def pytest_generate_tests(metafunc): """Dynamically parametrize tests that use the g_object fixture.""" if "g_object" in metafunc.fixturenames: classes = get_sample_gobject_subclasses() - ids = [c.TYPE if hasattr(c, 'TYPE') else c.__name__ for c in classes] + ids = [c.TYPE() if hasattr(c, 'TYPE') else c.__name__ for c in classes] metafunc.parametrize("g_object", classes, ids=ids) elif "g_object_keyed" in metafunc.fixturenames: # Objects that have keys (for tests that require GetParamsRectTyped to return key data) classes = get_sample_gobject_subclasses(require_keys=True) - ids = [c.TYPE if hasattr(c, 'TYPE') else c.__name__ for c in classes] + ids = [c.TYPE() if hasattr(c, 'TYPE') else c.__name__ for c in classes] metafunc.parametrize("g_object_keyed", classes, ids=ids) elif "g_object_keyed_editable" in metafunc.fixturenames: # Objects with keys AND at least 1 editable non-key field classes = get_sample_gobject_subclasses(require_keys=True, require_editable_non_key=True) - ids = [c.TYPE if hasattr(c, 'TYPE') else c.__name__ for c in classes] + ids = [c.TYPE() if hasattr(c, 'TYPE') else c.__name__ for c in classes] metafunc.parametrize("g_object_keyed_editable", classes, ids=ids) elif "g_object_multi_editable" in metafunc.fixturenames: # Objects with at least 2 editable non-key fields classes = get_sample_gobject_subclasses(require_multiple_editable=True) - ids = [c.TYPE if hasattr(c, 'TYPE') else c.__name__ for c in classes] + ids = [c.TYPE() if hasattr(c, 'TYPE') else c.__name__ for c in classes] metafunc.parametrize("g_object_multi_editable", classes, ids=ids) @@ -62,7 +62,7 @@ def indexable_instance() -> Indexable: def test_getitem_key_fields(indexable_instance: Indexable, g_object: Type[grid.GObject]): """idx[GObject] retrieves only key fields.""" mock_esa = indexable_instance.esa - unique_keys = sorted(list(set(g_object.keys))) + unique_keys = sorted(list(set(g_object.keys()))) if not unique_keys: result = indexable_instance[g_object] @@ -74,14 +74,14 @@ def test_getitem_key_fields(indexable_instance: Indexable, g_object: Type[grid.G mock_esa.GetParamsRectTyped.return_value = mock_df result_df = indexable_instance[g_object] - mock_esa.GetParamsRectTyped.assert_called_once_with(g_object.TYPE, unique_keys) + mock_esa.GetParamsRectTyped.assert_called_once_with(g_object.TYPE(), unique_keys) assert_frame_equal(result_df, mock_df) def test_getitem_all_fields(indexable_instance: Indexable, g_object: Type[grid.GObject]): """idx[GObject, :] retrieves all fields.""" mock_esa = indexable_instance.esa - expected_fields = sorted(list(set(g_object.keys) | set(g_object.fields))) + expected_fields = sorted(list(set(g_object.keys()) | set(g_object.fields()))) if not expected_fields: result = indexable_instance[g_object, :] @@ -92,24 +92,24 @@ def test_getitem_all_fields(indexable_instance: Indexable, g_object: Type[grid.G mock_esa.GetParamsRectTyped.return_value = mock_df result_df = indexable_instance[g_object, :] - mock_esa.GetParamsRectTyped.assert_called_once_with(g_object.TYPE, expected_fields) + mock_esa.GetParamsRectTyped.assert_called_once_with(g_object.TYPE(), expected_fields) assert_frame_equal(result_df, mock_df) def test_getitem_specific_fields(indexable_instance: Indexable, g_object: Type[grid.GObject]): """idx[GObject, ['Field1']] retrieves specific fields plus all keys.""" mock_esa = indexable_instance.esa - specific_fields = [f for f in g_object.fields if f not in g_object.keys] + specific_fields = [f for f in g_object.fields() if f not in g_object.keys()] if not specific_fields: pytest.skip(f"{g_object.__name__} has no non-key fields.") field = specific_fields[0] - expected = sorted(list(set(g_object.keys) | {field})) + expected = sorted(list(set(g_object.keys()) | {field})) mock_df = pd.DataFrame({f: [1, 2] for f in expected}) mock_esa.GetParamsRectTyped.return_value = mock_df result_df = indexable_instance[g_object, [field]] - mock_esa.GetParamsRectTyped.assert_called_once_with(g_object.TYPE, expected) + mock_esa.GetParamsRectTyped.assert_called_once_with(g_object.TYPE(), expected) assert_frame_equal(result_df, mock_df) @@ -134,12 +134,12 @@ def test_getitem_none_return(indexable_instance: Indexable): def test_setitem_broadcast(indexable_instance: Indexable, g_object: Type[grid.GObject]): """idx[GObject, 'Field'] = value broadcasts to all rows.""" mock_esa = indexable_instance.esa - editable_fields = [f for f in g_object.editable if f not in g_object.keys] + editable_fields = [f for f in g_object.editable() if f not in g_object.keys()] if not editable_fields: pytest.skip(f"{g_object.__name__} has no editable non-key fields.") field = editable_fields[0] - unique_keys = sorted(list(set(g_object.keys))) + unique_keys = sorted(list(set(g_object.keys()))) if not unique_keys: indexable_instance[g_object, field] = 1.234 @@ -159,7 +159,7 @@ def test_setitem_broadcast(indexable_instance: Indexable, g_object: Type[grid.GO def test_setitem_bulk_update_from_df(indexable_instance: Indexable, g_object: Type[grid.GObject]): """idx[GObject] = df performs bulk update.""" mock_esa = indexable_instance.esa - settable_cols = list(g_object.settable) + settable_cols = list(g_object.settable()) if not settable_cols: pytest.skip(f"{g_object.__name__} has no settable fields.") @@ -167,7 +167,7 @@ def test_setitem_bulk_update_from_df(indexable_instance: Indexable, g_object: Ty indexable_instance[g_object] = update_df mock_esa.ChangeParametersMultipleElementRect.assert_called_once_with( - g_object.TYPE, update_df.columns.tolist(), update_df + g_object.TYPE(), update_df.columns.tolist(), update_df ) @@ -175,13 +175,13 @@ def test_setitem_broadcast_multiple_fields(indexable_instance: Indexable, g_obje """idx[GObject, ['F1','F2']] = [v1, v2] broadcasts multiple values.""" mock_esa = indexable_instance.esa g_object = g_object_multi_editable - editable_fields = [f for f in g_object.editable if f not in g_object.keys] + editable_fields = [f for f in g_object.editable() if f not in g_object.keys()] # Filtering already ensures at least 2 editable non-key fields assert len(editable_fields) >= 2, f"{g_object.__name__} should have >= 2 editable fields" fields = editable_fields[:2] values = [1.1, 2.2] - unique_keys = sorted(list(set(g_object.keys))) + unique_keys = sorted(list(set(g_object.keys()))) if not unique_keys: indexable_instance[g_object, fields] = values @@ -214,7 +214,7 @@ def test_setitem_invalid_index(indexable_instance: Indexable): def test_setitem_non_settable_field(indexable_instance: Indexable): """ValueError when setting a read-only field.""" - non_settable = [f for f in grid.Bus.fields if f not in grid.Bus.settable] + non_settable = [f for f in grid.Bus.fields() if f not in grid.Bus.settable()] if not non_settable: pytest.skip("Bus has no non-settable fields.") with pytest.raises(ValueError, match="Cannot set read-only field"): @@ -223,7 +223,7 @@ def test_setitem_non_settable_field(indexable_instance: Indexable): def test_setitem_bulk_non_settable_column(indexable_instance: Indexable): """ValueError when bulk update includes a read-only column.""" - non_settable = [f for f in grid.Bus.fields if f not in grid.Bus.settable] + non_settable = [f for f in grid.Bus.fields() if f not in grid.Bus.settable()] if not non_settable: pytest.skip("Bus has no non-settable fields.") update_df = pd.DataFrame({"BusNum": [1, 2], non_settable[0]: [100, 200]}) @@ -238,7 +238,7 @@ def test_setitem_bulk_non_settable_column(indexable_instance: Indexable): def test_setitem_allows_secondary_identifier_fields(indexable_instance: Indexable): """Bulk update with SECONDARY identifier fields is allowed (e.g. LoadID).""" mock_esa = indexable_instance.esa - assert "LoadID" in grid.Load.settable + assert "LoadID" in grid.Load.settable() update_df = pd.DataFrame({ "BusNum": [1, 2], "LoadID": ["1", "2"], "LoadSMW": [10.0, 20.0] }) @@ -248,17 +248,17 @@ def test_setitem_allows_secondary_identifier_fields(indexable_instance: Indexabl def test_gobject_identifiers_property(): """identifiers includes both primary and secondary keys.""" - assert "BusNum" in grid.Load.identifiers - assert "LoadID" in grid.Load.identifiers - assert "BusNum" in grid.Gen.identifiers - assert "GenID" in grid.Gen.identifiers + assert "BusNum" in grid.Load.identifiers() + assert "LoadID" in grid.Load.identifiers() + assert "BusNum" in grid.Gen.identifiers() + assert "GenID" in grid.Gen.identifiers() def test_keyless_object_single_field(indexable_instance: Indexable): """Setting a field on a keyless object creates a single-row DataFrame.""" mock_esa = indexable_instance.esa - assert not grid.Sim_Solution_Options.keys - editable = list(grid.Sim_Solution_Options.editable) + assert not grid.Sim_Solution_Options.keys() + editable = list(grid.Sim_Solution_Options.editable()) assert len(editable) > 0 indexable_instance[grid.Sim_Solution_Options, editable[0]] = "YES" @@ -272,7 +272,7 @@ def test_keyless_object_single_field(indexable_instance: Indexable): def test_keyless_object_multiple_fields(indexable_instance: Indexable): """Setting multiple fields on a keyless object.""" mock_esa = indexable_instance.esa - editable = list(grid.Sim_Solution_Options.editable) + editable = list(grid.Sim_Solution_Options.editable()) assert len(editable) >= 2 fields = editable[:2] @@ -286,7 +286,7 @@ def test_keyless_object_multiple_fields(indexable_instance: Indexable): def test_keyless_object_value_length_mismatch(indexable_instance: Indexable): """Mismatched field/value counts on keyless object raises ValueError.""" - editable = list(grid.Sim_Solution_Options.editable) + editable = list(grid.Sim_Solution_Options.editable()) fields = editable[:2] with pytest.raises(ValueError, match="must be a list/tuple of the same length"): indexable_instance[grid.Sim_Solution_Options, fields] = ["YES", "NO", "EXTRA"] @@ -295,10 +295,10 @@ def test_keyless_object_value_length_mismatch(indexable_instance: Indexable): def test_setitem_with_nan_values(indexable_instance: Indexable): """NaN values are passed through to PowerWorld unchanged.""" mock_esa = indexable_instance.esa - editable_fields = [f for f in grid.Bus.editable if f not in grid.Bus.keys] + editable_fields = [f for f in grid.Bus.editable() if f not in grid.Bus.keys()] if not editable_fields: pytest.skip("Bus has no editable non-key fields.") - key_field = grid.Bus.keys[0] + key_field = grid.Bus.keys()[0] update_df = pd.DataFrame({ key_field: [1, 2, 3], @@ -377,13 +377,13 @@ def test_getitem_with_gobject_enum_field(indexable_instance: Indexable): pytest.skip("Could not find a suitable GObject field member.") field_name = field_member.value[1] - expected_fields = sorted(list(set(grid.Bus.keys) | {field_name})) + expected_fields = sorted(list(set(grid.Bus.keys()) | {field_name})) mock_df = pd.DataFrame({f: [1, 2] for f in expected_fields}) mock_esa.GetParamsRectTyped.return_value = mock_df result_df = indexable_instance[grid.Bus, field_member] - mock_esa.GetParamsRectTyped.assert_called_once_with(grid.Bus.TYPE, expected_fields) + mock_esa.GetParamsRectTyped.assert_called_once_with(grid.Bus.TYPE(), expected_fields) assert_frame_equal(result_df, mock_df) @@ -409,10 +409,10 @@ def test_setitem_broadcast_empty_dataframe(indexable_instance: Indexable, g_obje """Setting field on objects when no objects exist (empty DataFrame) is a no-op.""" mock_esa = indexable_instance.esa g_object = g_object_keyed_editable - editable_fields = [f for f in g_object.editable if f not in g_object.keys] + editable_fields = [f for f in g_object.editable() if f not in g_object.keys()] # Filtering already ensures keys exist and at least 1 editable non-key field - assert g_object.keys, f"{g_object.__name__} should have keys" + assert g_object.keys(), f"{g_object.__name__} should have keys" assert editable_fields, f"{g_object.__name__} should have editable non-key fields" mock_esa.GetParamsRectTyped.return_value = pd.DataFrame() @@ -426,7 +426,7 @@ def test_setitem_broadcast_empty_dataframe(indexable_instance: Indexable, g_obje def test_setitem_broadcast_none_dataframe(indexable_instance: Indexable): """Setting field when GetParamsRectTyped returns None is a no-op.""" mock_esa = indexable_instance.esa - editable_fields = [f for f in grid.Bus.editable if f not in grid.Bus.keys] + editable_fields = [f for f in grid.Bus.editable() if f not in grid.Bus.keys()] if not editable_fields: pytest.skip("Bus has no editable non-key fields.") @@ -508,16 +508,16 @@ def test_getitem_single_string_field(indexable_instance: Indexable): """idx[GObject, 'FieldName'] works with a single string field.""" mock_esa = indexable_instance.esa - non_key_fields = [f for f in grid.Bus.fields if f not in grid.Bus.keys] + non_key_fields = [f for f in grid.Bus.fields() if f not in grid.Bus.keys()] if not non_key_fields: pytest.skip("Bus has no non-key fields.") field = non_key_fields[0] - expected_fields = sorted(list(set(grid.Bus.keys) | {field})) + expected_fields = sorted(list(set(grid.Bus.keys()) | {field})) mock_df = pd.DataFrame({f: [1, 2] for f in expected_fields}) mock_esa.GetParamsRectTyped.return_value = mock_df result_df = indexable_instance[grid.Bus, field] - mock_esa.GetParamsRectTyped.assert_called_once_with(grid.Bus.TYPE, expected_fields) + mock_esa.GetParamsRectTyped.assert_called_once_with(grid.Bus.TYPE(), expected_fields) assert_frame_equal(result_df, mock_df) diff --git a/tests/test_integration_workbench.py b/tests/test_integration_workbench.py index 56bcb62a..42b08c33 100644 --- a/tests/test_integration_workbench.py +++ b/tests/test_integration_workbench.py @@ -347,25 +347,25 @@ def test_component_access(wb, component_class): """ Verifies that PowerWorld can read key fields for every defined component. """ - if component_class.TYPE in CRASH_PRONE_COMPONENTS: - pytest.skip(f"Skipping {component_class.TYPE}: Known to cause SimAuto crashes.") + if component_class.TYPE() in CRASH_PRONE_COMPONENTS: + pytest.skip(f"Skipping {component_class.TYPE()}: Known to cause SimAuto crashes.") try: df = wb[component_class] except SimAutoFeatureError as e: - pytest.skip(f"Object type {component_class.TYPE} cannot be retrieved via SimAuto: {e.message}") + pytest.skip(f"Object type {component_class.TYPE()} cannot be retrieved via SimAuto: {e.message}") except (PowerWorldError, COMError) as e: err_msg = str(e) if "Access violation" in err_msg or "memory resources" in err_msg: - pytest.skip(f"Object type {component_class.TYPE} causes PW crash: {e}") + pytest.skip(f"Object type {component_class.TYPE()} causes PW crash: {e}") with tempfile.NamedTemporaryFile(suffix=".csv", delete=False) as tmp: tmp_path = tmp.name try: - fields = component_class.keys if component_class.keys else ["ALL"] - wb.esa.SaveObjectFields(tmp_path, component_class.TYPE, fields) - pytest.fail(f"Object type {component_class.TYPE} is supported but failed to read: {e}") + fields = component_class.keys() if component_class.keys() else ["ALL"] + wb.esa.SaveObjectFields(tmp_path, component_class.TYPE(), fields) + pytest.fail(f"Object type {component_class.TYPE()} is supported but failed to read: {e}") except PowerWorldError: - pytest.skip(f"Object type {component_class.TYPE} not supported by this PW version.") + pytest.skip(f"Object type {component_class.TYPE()} not supported by this PW version.") finally: if os.path.exists(tmp_path): try: os.remove(tmp_path) @@ -376,7 +376,7 @@ def test_component_access(wb, component_class): if df is not None: assert isinstance(df, pd.DataFrame) if not df.empty: - for key in component_class.keys: + for key in component_class.keys(): assert key in df.columns From 8dbfaf4e6f48291be0bde1c83e836faffd256088 Mon Sep 17 00:00:00 2001 From: Luke Lowery Date: Tue, 3 Feb 2026 15:40:52 -0600 Subject: [PATCH 2/3] Update Docs --- docs/dev/components.rst | 2 +- .../03_components_and_fields.ipynb | 25 ++++----- .../getting_started/04_creating_objects.ipynb | 55 ++----------------- 3 files changed, 19 insertions(+), 63 deletions(-) diff --git a/docs/dev/components.rst b/docs/dev/components.rst index 3e427f0b..de10136b 100644 --- a/docs/dev/components.rst +++ b/docs/dev/components.rst @@ -38,7 +38,7 @@ define fields with ``(PowerWorld name, data type, priority flags)``: """Name""" # ... more fields ... - ObjectString = 'Bus' # Sets Bus.TYPE — must be last member + ObjectString = 'Bus' # Sets Bus.TYPE() — must be last member The base class collects these into queryable classmethods: diff --git a/docs/examples/getting_started/03_components_and_fields.ipynb b/docs/examples/getting_started/03_components_and_fields.ipynb index 32941e05..d4d49311 100644 --- a/docs/examples/getting_started/03_components_and_fields.ipynb +++ b/docs/examples/getting_started/03_components_and_fields.ipynb @@ -27,16 +27,15 @@ "execution_count": 1, "id": "hidden-setup", "metadata": { + "nbsphinx": "hidden", "tags": [ "remove-cell" - ], - "nbsphinx": "hidden" + ] }, "outputs": [], "source": [ "from esapp.components import *\n", - "from esapp import TS\n", - "from esapp.components import TSField" + "from esapp import TS" ] }, { @@ -72,7 +71,7 @@ } ], "source": [ - "Bus.keys, Gen.keys, Branch.keys" + "Bus.keys(), Gen.keys(), Branch.keys()" ] }, { @@ -82,21 +81,21 @@ "source": [ "Beyond primary keys, each component has several field categories:\n", "\n", - "- **Identifiers** — the union of primary and secondary keys (like\n", + "- **Identifiers** - the union of primary and secondary keys (like\n", " `BusName` or `AreaNum`). Secondary keys help PowerWorld resolve\n", " which object you mean during writes.\n", - "- **Editable** — fields you can modify (generation setpoints, voltage\n", + "- **Editable** - fields you can modify (generation setpoints, voltage\n", " targets, load values, etc.). Writing to a read-only field raises\n", " a `ValueError`.\n", - "- **Settable** — identifiers plus editable fields, i.e. everything\n", + "- **Settable** - identifiers plus editable fields, i.e. everything\n", " allowed in a write operation.\n", - "- **Fields** — the complete set of all known fields, including\n", + "- **Fields** - the complete set of all known fields, including\n", " read-only calculated results." ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 8, "id": "identifiers", "metadata": {}, "outputs": [ @@ -106,13 +105,13 @@ "{'AreaNum', 'BusName', 'BusName_NomVolt', 'BusNomVolt', 'BusNum', 'ZoneNum'}" ] }, - "execution_count": 3, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "Bus.identifiers" + "Bus.identifiers()" ] }, { @@ -133,7 +132,7 @@ } ], "source": [ - "len(Bus.editable), len(Bus.settable), len(Bus.fields)" + "len(Bus.editable()), len(Bus.settable()), len(Bus.fields())" ] }, { diff --git a/docs/examples/getting_started/04_creating_objects.ipynb b/docs/examples/getting_started/04_creating_objects.ipynb index 87033e3a..3f8f7b14 100644 --- a/docs/examples/getting_started/04_creating_objects.ipynb +++ b/docs/examples/getting_started/04_creating_objects.ipynb @@ -59,41 +59,15 @@ "cell_type": "markdown", "id": "prereqs-md", "metadata": {}, - "source": [ - "## Prerequisites\n", - "\n", - "PowerWorld must be in **EDIT mode** before you can add new objects. Call `pw.edit_mode()` to enter it, and `pw.run_mode()` when you're done.\n", - "\n", - "The DataFrame you write should include all **identifier** fields\n", - "for the object type — that's the union of primary keys and\n", - "secondary keys. You can inspect these with `Bus.identifiers`,\n", - "`Gen.identifiers`, etc. Primary keys uniquely identify the object;\n", - "secondary keys provide the additional context PowerWorld needs to\n", - "fully define it (nominal voltage, area, zone, limits, etc.)." - ] + "source": "## Prerequisites\n\nPowerWorld must be in **EDIT mode** before you can add new objects. Call `pw.edit_mode()` to enter it, and `pw.run_mode()` when you're done.\n\nThe DataFrame you write should include all **identifier** fields\nfor the object type — that's the union of primary keys and\nsecondary keys. You can inspect these with `Bus.identifiers()`,\n`Gen.identifiers()`, etc. Primary keys uniquely identify the object;\nsecondary keys provide the additional context PowerWorld needs to\nfully define it (nominal voltage, area, zone, limits, etc.)." }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "show-keys", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Bus identifiers: {'BusNum', 'BusName_NomVolt', 'BusName', 'ZoneNum', 'BusNomVolt', 'AreaNum'}\n", - "Gen identifiers: {'GenMVRMax', 'GenMvrSetPoint', 'GenID', 'GenMWSetPoint', 'GenStatus', 'GenVoltSet', 'GenMVRMin', 'GenMWMax', 'BusNum', 'GenMWMin', 'GenAGCAble', 'BusName_NomVolt', 'GenAVRAble'}\n", - "Load identifiers: {'LoadSMW', 'BusNum', 'BusName_NomVolt', 'LoadStatus', 'LoadSMVR', 'LoadID'}\n" - ] - } - ], - "source": [ - "# Identifier fields needed for creation\n", - "print(\"Bus identifiers:\", Bus.identifiers)\n", - "print(\"Gen identifiers:\", Gen.identifiers)\n", - "print(\"Load identifiers:\", Load.identifiers)" - ] + "outputs": [], + "source": "# Identifier fields needed for creation\nprint(\"Bus identifiers:\", Bus.identifiers())\nprint(\"Gen identifiers:\", Gen.identifiers())\nprint(\"Load identifiers:\", Load.identifiers())" }, { "cell_type": "markdown", @@ -464,24 +438,7 @@ "cell_type": "markdown", "id": "tips-md", "metadata": {}, - "source": [ - "## Tips\n", - "\n", - "- **Always enter edit mode** before creating objects and return\n", - " to run mode afterward. Forgetting this is the most common\n", - " cause of creation failures.\n", - "\n", - "- **Include all identifier fields** when creating objects. Use\n", - " `ComponentType.identifiers` to see the full set of primary and\n", - " secondary key fields. PowerWorld needs these to fully define\n", - " the object — omitting them may cause unexpected defaults or\n", - " creation failures.\n", - "\n", - "- **The broadcast syntax does not create objects.** Only the\n", - " DataFrame assignment path (`pw[Type] = df`) can create new\n", - " objects. The field-broadcast path\n", - " (`pw[Type, \"Field\"] = value`) only modifies existing ones." - ] + "source": "## Tips\n\n- **Always enter edit mode** before creating objects and return\n to run mode afterward. Forgetting this is the most common\n cause of creation failures.\n\n- **Include all identifier fields** when creating objects. Use\n `ComponentType.identifiers()` to see the full set of primary and\n secondary key fields. PowerWorld needs these to fully define\n the object — omitting them may cause unexpected defaults or\n creation failures.\n\n- **The broadcast syntax does not create objects.** Only the\n DataFrame assignment path (`pw[Type] = df`) can create new\n objects. The field-broadcast path\n (`pw[Type, \"Field\"] = value`) only modifies existing ones." } ], "metadata": { @@ -505,4 +462,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file From 0897aa0538f68dc1e25edb9535025226dc0662d1 Mon Sep 17 00:00:00 2001 From: Luke Lowery Date: Tue, 3 Feb 2026 15:48:41 -0600 Subject: [PATCH 3/3] Minor formatting Corrections --- .../getting_started/02_writing_data.ipynb | 10 +- .../03_components_and_fields.ipynb | 2 +- .../05_workbench_overview.ipynb | 18 ++-- .../06_settings_and_options.ipynb | 98 +++++++++---------- 4 files changed, 64 insertions(+), 64 deletions(-) diff --git a/docs/examples/getting_started/02_writing_data.ipynb b/docs/examples/getting_started/02_writing_data.ipynb index 97f94fcd..cc8e3a5b 100644 --- a/docs/examples/getting_started/02_writing_data.ipynb +++ b/docs/examples/getting_started/02_writing_data.ipynb @@ -26,10 +26,10 @@ "execution_count": 1, "id": "hidden-setup", "metadata": { + "nbsphinx": "hidden", "tags": [ "remove-cell" - ], - "nbsphinx": "hidden" + ] }, "outputs": [ { @@ -96,7 +96,7 @@ "id": "array-md", "metadata": {}, "source": [ - "**Per-element values** — a list or array whose length matches the number\n", + "**Per-element values** - a list or array whose length matches the number\n", "of objects sets each one individually." ] }, @@ -115,7 +115,7 @@ "id": "multifield-md", "metadata": {}, "source": [ - "**Multiple fields** — pass a list of field names and a matching list\n", + "**Multiple fields** - pass a list of field names and a matching list\n", "of values to set several columns at once." ] }, @@ -138,7 +138,7 @@ "\n", "For targeted updates to specific objects, build a DataFrame that\n", "includes the primary-key columns and the fields you want to change.\n", - "Only the rows present in the DataFrame are modified — everything\n", + "Only the rows present in the DataFrame are modified - everything\n", "else stays untouched." ] }, diff --git a/docs/examples/getting_started/03_components_and_fields.ipynb b/docs/examples/getting_started/03_components_and_fields.ipynb index d4d49311..f5e799c6 100644 --- a/docs/examples/getting_started/03_components_and_fields.ipynb +++ b/docs/examples/getting_started/03_components_and_fields.ipynb @@ -143,7 +143,7 @@ "## Transient Stability Fields\n", "\n", "The `TS` class provides constants for transient stability result\n", - "fields, organized by object type — `TS.Gen`, `TS.Bus`, `TS.Branch`,\n", + "fields, organized by object type - `TS.Gen`, `TS.Bus`, `TS.Branch`,\n", "`TS.Load`, etc. Each field is a `TSField` with a `name` and\n", "`description`. Typing `TS.Gen.` in your IDE autocompletes every\n", "available generator result field.\n", diff --git a/docs/examples/getting_started/05_workbench_overview.ipynb b/docs/examples/getting_started/05_workbench_overview.ipynb index a24e7a4f..99164eaa 100644 --- a/docs/examples/getting_started/05_workbench_overview.ipynb +++ b/docs/examples/getting_started/05_workbench_overview.ipynb @@ -10,7 +10,7 @@ "Beyond the indexable interface, `PowerWorld` provides convenience\n", "methods that wrap common workflows into single calls. These cover\n", "data retrieval shortcuts, power flow, voltage analysis, matrix\n", - "extraction, sensitivity factors, and case control — so you spend\n", + "extraction, sensitivity factors, and case control - so you spend\n", "less time assembling field lists and more time on analysis.\n", "\n", "```python\n", @@ -26,10 +26,10 @@ "execution_count": 8, "id": "hidden-setup", "metadata": { + "nbsphinx": "hidden", "tags": [ "remove-cell" - ], - "nbsphinx": "hidden" + ] }, "outputs": [ { @@ -61,7 +61,7 @@ "\n", "These methods return DataFrames of common object types with their\n", "most useful fields pre-selected. They're thin wrappers around the\n", - "indexable interface — `pw.gens()` is equivalent to\n", + "indexable interface - `pw.gens()` is equivalent to\n", "`pw[Gen, [\"GenMW\", \"GenMVR\", \"GenStatus\"]]` but shorter to type.\n", "\n", "| Method | Returns |\n", @@ -427,7 +427,7 @@ "pw.dc_mode = True # DC approximation\n", "```\n", "\n", - "See the :doc:`API Reference ` for the full list of\n", + "See the API Reference for the full list of\n", "solver option descriptors." ] }, @@ -440,7 +440,7 @@ "\n", "Several methods wrap multi-step workflows into single calls.\n", "\n", - "**Branch flows and overloads** — `flows()` retrieves MW, MVR, MVA,\n", + "**Branch flows and overloads** - `flows()` retrieves MW, MVR, MVA,\n", "and percent loading for every branch. `overloads()` filters to\n", "branches exceeding a threshold.\n", "\n", @@ -449,7 +449,7 @@ "| `pw.flows()` | Branch MW, MVR, MVA, and % loading |\n", "| `pw.overloads(threshold=100)` | Branches exceeding a loading threshold |\n", "\n", - "**Sensitivity factors** — `ptdf()` and `lodf()` compute Power\n", + "**Sensitivity factors** - `ptdf()` and `lodf()` compute Power\n", "Transfer Distribution Factors and Line Outage Distribution Factors,\n", "respectively. Both call the underlying SAW sensitivity commands and\n", "return the results as a DataFrame.\n", @@ -459,7 +459,7 @@ "| `pw.ptdf(seller, buyer)` | Power Transfer Distribution Factors |\n", "| `pw.lodf(branch)` | Line Outage Distribution Factors |\n", "\n", - "**Snapshot** — a context manager that saves the current case state\n", + "**Snapshot** - a context manager that saves the current case state\n", "on entry and restores it on exit, even if an exception occurs.\n", "Useful for \"what-if\" analyses where you want to modify the case\n", "temporarily without losing the original state.\n", @@ -511,7 +511,7 @@ "pw.gic.configure() # apply sensible defaults\n", "```\n", "\n", - "See the :doc:`API Reference ` for full documentation\n", + "See the API Reference for full documentation\n", "of each module." ] } diff --git a/docs/examples/getting_started/06_settings_and_options.ipynb b/docs/examples/getting_started/06_settings_and_options.ipynb index 00e78bd3..f8ca9972 100644 --- a/docs/examples/getting_started/06_settings_and_options.ipynb +++ b/docs/examples/getting_started/06_settings_and_options.ipynb @@ -7,7 +7,7 @@ "source": [ "# Settings & Options\n", "\n", - "PowerWorld has many simulation settings — solver iteration limits,\n", + "PowerWorld has many simulation settings - solver iteration limits,\n", "convergence tolerances, control flags, GIC parameters, and more.\n", "Traditionally these require verbose getter/setter calls through\n", "the SimAuto API. ESA++ exposes the most commonly used settings as\n", @@ -27,10 +27,10 @@ "execution_count": 1, "id": "hidden-setup", "metadata": { + "nbsphinx": "hidden", "tags": [ "remove-cell" - ], - "nbsphinx": "hidden" + ] }, "outputs": [ { @@ -68,13 +68,9 @@ }, { "cell_type": "code", + "execution_count": null, "id": "solver-read", "metadata": {}, - "source": [ - "# Read current settings\n", - "pw.max_iterations, pw.convergence_tol, pw.flat_start, pw.dc_mode" - ], - "execution_count": null, "outputs": [ { "data": { @@ -86,25 +82,17 @@ "metadata": {}, "output_type": "execute_result" } + ], + "source": [ + "# Read current settings\n", + "pw.max_iterations, pw.convergence_tol, pw.flat_start, pw.dc_mode" ] }, { "cell_type": "code", + "execution_count": null, "id": "solver-write", "metadata": {}, - "source": [ - "# Change numeric settings\n", - "pw.max_iterations = 200\n", - "pw.convergence_tol = 1e-5\n", - "\n", - "# Toggle boolean settings\n", - "pw.dc_mode = True\n", - "assert pw.dc_mode is True\n", - "pw.dc_mode = False\n", - "\n", - "pw.max_iterations, pw.convergence_tol, pw.dc_mode" - ], - "execution_count": null, "outputs": [ { "data": { @@ -116,6 +104,18 @@ "metadata": {}, "output_type": "execute_result" } + ], + "source": [ + "# Change numeric settings\n", + "pw.max_iterations = 200\n", + "pw.convergence_tol = 1e-5\n", + "\n", + "# Toggle boolean settings\n", + "pw.dc_mode = True\n", + "assert pw.dc_mode is True\n", + "pw.dc_mode = False\n", + "\n", + "pw.max_iterations, pw.convergence_tol, pw.dc_mode" ] }, { @@ -170,13 +170,9 @@ }, { "cell_type": "code", + "execution_count": null, "id": "gic-read", "metadata": {}, - "source": [ - "# Read GIC settings\n", - "pw.gic.pf_include, pw.gic.ts_include, pw.gic.calc_mode" - ], - "execution_count": null, "outputs": [ { "data": { @@ -188,21 +184,17 @@ "metadata": {}, "output_type": "execute_result" } + ], + "source": [ + "# Read GIC settings\n", + "pw.gic.pf_include, pw.gic.ts_include, pw.gic.calc_mode" ] }, { "cell_type": "code", + "execution_count": null, "id": "gic-write", "metadata": {}, - "source": [ - "# Change GIC settings\n", - "pw.gic.pf_include = True\n", - "pw.gic.efield_angle = 90.0\n", - "pw.gic.efield_mag = 1.0\n", - "\n", - "pw.gic.pf_include, pw.gic.efield_angle, pw.gic.efield_mag" - ], - "execution_count": null, "outputs": [ { "data": { @@ -214,6 +206,14 @@ "metadata": {}, "output_type": "execute_result" } + ], + "source": [ + "# Change GIC settings\n", + "pw.gic.pf_include = True\n", + "pw.gic.efield_angle = 90.0\n", + "pw.gic.efield_mag = 1.0\n", + "\n", + "pw.gic.pf_include, pw.gic.efield_angle, pw.gic.efield_mag" ] }, { @@ -228,13 +228,9 @@ }, { "cell_type": "code", + "execution_count": null, "id": "configure", "metadata": {}, - "source": [ - "pw.gic.configure() # defaults: pf_include=True, ts_include=False, calc_mode=\"SnapShot\"\n", - "pw.gic.pf_include, pw.gic.ts_include, pw.gic.calc_mode" - ], - "execution_count": null, "outputs": [ { "data": { @@ -246,6 +242,10 @@ "metadata": {}, "output_type": "execute_result" } + ], + "source": [ + "pw.gic.configure() # defaults: pf_include=True, ts_include=False, calc_mode=\"SnapShot\"\n", + "pw.gic.pf_include, pw.gic.ts_include, pw.gic.calc_mode" ] }, { @@ -259,12 +259,9 @@ }, { "cell_type": "code", + "execution_count": null, "id": "settings", "metadata": {}, - "source": [ - "pw.gic.settings().head(10)" - ], - "execution_count": null, "outputs": [ { "data": { @@ -364,6 +361,9 @@ "metadata": {}, "output_type": "execute_result" } + ], + "source": [ + "pw.gic.settings().head(10)" ] }, { @@ -418,13 +418,9 @@ }, { "cell_type": "code", + "execution_count": null, "id": "descriptor-introspect", "metadata": {}, - "source": [ - "desc = type(pw).max_iterations\n", - "desc.key, desc.is_bool" - ], - "execution_count": null, "outputs": [ { "data": { @@ -436,6 +432,10 @@ "metadata": {}, "output_type": "execute_result" } + ], + "source": [ + "desc = type(pw).max_iterations\n", + "desc.key, desc.is_bool" ] } ],