Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
build:
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
python-version: ["3.10", "3.14"]
platform: [octave]
os: [ubuntu-latest]

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
build:
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
platform: [octave]
os: [ubuntu-latest]

Expand Down
5 changes: 2 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,19 @@
```sh
uv venv env --python 3.14
source env/bin/activate
uv pip install pip
```

## packages

```sh
pip install pru
uv pip install pru
pru -r requirements-all.txt
```

## Install in development mode

```shell
pip install -e ."[dev]"
uv pip install -e ."[dev]"
```

## Pytest
Expand Down
6 changes: 6 additions & 0 deletions matpowercaseframes/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"dcline",
"dclinecost",
"case",
"reserves",
)

COLUMNS = {
Expand Down Expand Up @@ -112,6 +113,11 @@
"MU_QMINT",
"MU_QMAXT",
],
"reserves": {
"req": ["PREQ"],
"cost": ["C1"],
"qty": ["PQTY"],
},
"if": {
# negative 'BRANCHIDX' defines opposite direction
"map": ["IFNUM", "BRANCHIDX"],
Expand Down
123 changes: 108 additions & 15 deletions matpowercaseframes/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,38 @@
MATPOWER_EXIST = False


class ReservesFrames:
"""A struct-like container for reserves data, similar to CaseFrames."""

def __init__(self, data=None):
"""
Initialize ReservesFrames with optional data.

Args:
data (dict, optional): Dictionary containing reserves DataFrames.
Expected keys: 'zones', 'req', 'cost', 'qty'
"""
self._attributes = []

if data is not None:
if isinstance(data, dict):
for key, value in data.items():
self.setattr(key, value)
else:
raise TypeError(f"ReservesFrames data must be a dict, got {type(data)}")

def setattr(self, name, value):
"""Set attribute and track it in _attributes list."""
if name not in self._attributes:
self._attributes.append(name)
self.__setattr__(name, value)

@property
def attributes(self):
"""List of attributes in this ReservesFrames object."""
return self._attributes


class CaseFrames:
def __init__(
self,
Expand Down Expand Up @@ -281,6 +313,9 @@ def _read_oct2py_struct(self, struct, allow_any_keys=False):
value = list_
elif attribute in ["bus_name", "branch_name", "gen_name"]:
value = pd.Index([name[0] for name in list_], name=attribute)
elif attribute in ["reserves"]:
dfs = reserves_data_to_dataframes(list_)
value = ReservesFrames(dfs)
else: # bus, branch, gen, gencost, dcline, dclinecost
list_ = np.atleast_2d(list_)
n_cols = list_.shape[1]
Expand Down Expand Up @@ -510,37 +545,52 @@ def _update_index(self, allow_any_keys=False):
attribute_data.set_index(attribute_name_data, drop=False, inplace=True)
except AttributeError:
attribute_data.set_index(
pd.RangeIndex(1, len(attribute_data.index) + 1),
pd.RangeIndex(1, len(attribute_data.index) + 1, name=attribute),
drop=False,
inplace=True,
)

# gencost is optional
# NOTE: try except is better than checking hasattr for common possitive
try:
if "gen_name" in self._attributes:
self.gencost.set_index(self.gen_name, drop=False, inplace=True)
else:
self.gencost.set_index(
pd.RangeIndex(1, len(self.gen.index) + 1), drop=False, inplace=True
pd.RangeIndex(1, len(self.gen.index) + 1, name="gen"),
drop=False,
inplace=True,
)
except AttributeError:
# for when self.gencost doesn't exist
pass

# NOTE: try hasattr is better than try except for common negative
if hasattr(self, "reserves"):
self.reserves.zones.columns = pd.RangeIndex(
start=1, stop=len(self.gen.index) + 1, name="gen"
)

# other attributes
if allow_any_keys:
for attribute in self._attributes:
if attribute in ["bus", "branch", "gen", "gencost"]:
continue
attribute_data = getattr(self, attribute)
if isinstance(attribute_data, (pd.DataFrame, pd.Series)):
# check if index is a RangeIndex
if attribute_data.index.dtype == int:
# replace the index with a new RangeIndex starting at 1
attribute_data.set_index(
pd.RangeIndex(start=1, stop=len(attribute_data) + 1),
drop=False,
inplace=True,
)
self._update_index_any()

def _update_index_any(self):
for attribute in self._attributes:
if attribute in ["bus", "branch", "gen", "gencost", "reserves"]:
continue
attribute_data = getattr(self, attribute)
if isinstance(attribute_data, (pd.DataFrame, pd.Series)):
# check if index is a RangeIndex
if attribute_data.index.dtype == int:
# replace the index with a new RangeIndex starting at 1
attribute_data.set_index(
pd.RangeIndex(
start=1, stop=len(attribute_data) + 1, name=attribute
),
drop=False,
inplace=True,
)

def infer_numpy(self):
"""
Expand Down Expand Up @@ -608,6 +658,10 @@ def reset_index(self):
)
self.gen["GEN_BUS"] = self.gen["GEN_BUS"].replace(bus_map)

# TODO:
# Since mpc.reserves.zones columns use cf.gen.index, don't forget to update
# the columns of mpc.reserves.zones if exists.

def add_schema_case(self, F=None):
# add case to follow casefromat/schema
# !WARNING:
Expand Down Expand Up @@ -806,3 +860,42 @@ def to_schema(self, path, prefix="", suffix=""):
if "case" not in self._attributes:
self.add_schema_case()
self.to_csv(path, prefix=prefix, suffix=suffix)


def reserves_data_to_dataframes(reserves):
"""
Convert all mpc.reserves struct data to DataFrames.

Args:
reserves: Octave struct or dictionary of mpc.reserves object from MATPOWER

Returns:
Dictionary containing:
- 'zones': Reserve zones DataFrame
- 'req': Reserve requirements DataFrame
- 'cost': Reserve costs DataFrame (if exists)
- 'qty': Reserve quantities DataFrame (if exists)
"""
dfs = {}
n_zones, n_gens = reserves.zones.shape
dfs["zones"] = pd.DataFrame(
reserves.zones,
index=pd.RangeIndex(start=1, stop=n_zones + 1, name="zone"),
columns=pd.RangeIndex(start=1, stop=n_gens + 1, name="gen"),
)
zone_sum = dfs["zones"].sum(axis=0)
idx_gen_with_reserves = zone_sum[zone_sum > 0].index
dfs["req"] = pd.DataFrame(
reserves.req,
index=pd.RangeIndex(start=1, stop=n_zones + 1, name="zone"),
columns=["PREQ"],
)
if hasattr(reserves, "cost"):
dfs["cost"] = pd.DataFrame(
reserves.cost, index=idx_gen_with_reserves, columns=["C1"]
)
if hasattr(reserves, "qty"):
dfs["qty"] = pd.DataFrame(
reserves.qty, index=idx_gen_with_reserves, columns=["PQTY"]
)
return dfs
Loading