"""Data retrieval and modification functions (SimAuto data access layer)."""
import re
from typing import List, Tuple, Union
import numpy as np
import pandas as pd
import pythoncom
from ._enums import FieldListColumn, SpecificFieldListColumn
from ._exceptions import Error
from ._helpers import (
convert_df_to_variant,
convert_list_to_variant,
convert_nested_list_to_variant,
)
[docs]
class DataMixin:
"""Mixin for data retrieval, modification, enumeration, and export."""
[docs]
def ChangeParametersMultipleElement(self, ObjectType: str, ParamList: list, ValueList: list) -> None:
"""Modifies parameters for multiple objects using a nested list of values.
This method is suitable for updating a moderate number of objects where
the data is structured as a list of lists. For very large datasets,
`ChangeParametersMultipleElementRect` is generally more efficient.
Parameters
----------
ObjectType : str
The PowerWorld object type.
ParamList : List[str]
A list of internal field names to modify. This list must include the
primary key fields for the `ObjectType` to identify the target objects.
ValueList : List[List[Any]]
A list of lists, where each inner list contains values for one object.
The order of values in each inner list must match `ParamList`.
Returns
-------
None
Raises
------
PowerWorldError
If the SimAuto call fails.
"""
return self._com_call(
"ChangeParametersMultipleElement",
ObjectType,
convert_list_to_variant(ParamList),
convert_nested_list_to_variant(ValueList),
)
[docs]
def ChangeParametersMultipleElementRect(self, ObjectType: str, ParamList: list, df: pd.DataFrame) -> None:
"""
Modifies parameters for multiple objects using a pandas DataFrame (rectangular data structure).
This is generally the most efficient way to update a large number of objects at once.
The DataFrame must include the primary key fields for the object type to identify
which objects to update.
Parameters
----------
ObjectType : str
The PowerWorld object type.
ParamList : List[str]
A list of internal field names being updated. These must correspond to the
column names in the `df`.
df : pandas.DataFrame
A DataFrame containing the data to update. The column names of `df` must
match the `ParamList`, and it must contain primary key columns.
Returns
-------
None
Raises
------
PowerWorldError
If the SimAuto call fails.
"""
return self._com_call(
"ChangeParametersMultipleElementRect",
ObjectType,
convert_list_to_variant(ParamList),
convert_df_to_variant(df),
)
[docs]
def GetFieldList(self, ObjectType: str, copy=False) -> pd.DataFrame:
"""Retrieves the complete list of available fields for a given PowerWorld object type.
This method queries PowerWorld for all fields associated with an object type
and caches the result for performance.
Parameters
----------
ObjectType : str
The PowerWorld object type (e.g., 'Bus', 'Gen').
copy : bool, optional
If True, returns a deep copy of the cached field list DataFrame.
Defaults to False.
Returns
-------
pandas.DataFrame
A DataFrame containing columns like 'key_field', 'internal_field_name',
'field_data_type', 'description', 'display_name', and 'enterable'.
Raises
------
PowerWorldError
If the SimAuto call fails (e.g., invalid object type).
"""
object_type = ObjectType.lower()
try:
output = self._object_fields[object_type]
except KeyError:
result = self._com_call("GetFieldList", ObjectType)
result_arr = np.array(result)
# Try standard 5-column format first, fall back to old/new formats
base_cols = FieldListColumn.base_columns()
old_cols = FieldListColumn.old_columns()
new_cols = FieldListColumn.new_columns()
try:
output = pd.DataFrame(result_arr, columns=base_cols)
except ValueError as e:
exp_base = r"\([0-9]+,\s"
exp_end = r"{}\)"
r1 = re.search(exp_base + exp_end.format(len(old_cols)), e.args[0])
r2 = re.search(exp_base + exp_end.format(len(base_cols)), e.args[0])
r3 = re.search(exp_base + exp_end.format(len(new_cols)), e.args[0])
if (r1 is None) or (r2 is None):
if r3 is None:
raise e
else:
output = pd.DataFrame(result_arr, columns=new_cols)
else:
output = pd.DataFrame(result_arr, columns=old_cols)
output.sort_values(by=[FieldListColumn.INTERNAL_FIELD_NAME.value], inplace=True)
self._object_fields[object_type] = output
return output.copy(deep=True) if copy else output
[docs]
def GetParametersMultipleElement(
self, ObjectType: str, ParamList: list, FilterName: str = ""
) -> Union[pd.DataFrame, None]:
"""Retrieves parameters for multiple objects of a specific type, optionally filtered.
This method is commonly used to fetch data for all objects of a given type
or a subset defined by a PowerWorld filter.
Parameters
----------
ObjectType : str
The PowerWorld object type (e.g., 'Bus', 'Gen').
ParamList : List[str]
A list of internal field names to retrieve.
FilterName : str, optional
Optional name of a PowerWorld filter to restrict the result set.
Defaults to an empty string, meaning no filter is applied.
Returns
-------
Union[pandas.DataFrame, None]
A pandas DataFrame where columns correspond to `ParamList`.
Returns None if no objects are found matching the criteria.
Raises
------
PowerWorldError
If the SimAuto call fails (e.g., invalid object type or field names).
"""
output = self._com_call(
"GetParametersMultipleElement",
ObjectType,
convert_list_to_variant(ParamList),
FilterName,
)
if output is None:
return output
return pd.DataFrame(np.array(output).transpose(), columns=ParamList)
[docs]
def GetParamsRectTyped(
self, ObjectType: str, ParamList: list, FilterName: str = ""
) -> Union[pd.DataFrame, None]:
"""Retrieves data in a rectangular format with PowerWorld's native variant typing preserved.
This method is similar to `GetParametersMultipleElement` but attempts to preserve
the original data types as returned by SimAuto, which can sometimes be more efficient
or necessary for specific use cases.
Parameters
----------
ObjectType : str
The PowerWorld object type.
ParamList : List[str]
A list of internal field names to retrieve.
FilterName : str, optional
Optional name of a PowerWorld filter to apply. Defaults to an empty string.
Returns
-------
Union[pandas.DataFrame, None]
A pandas DataFrame containing the requested data. Returns None if no objects found.
Raises
------
PowerWorldError
If the SimAuto call fails.
"""
output = self._com_call(
"GetParamsRectTyped",
ObjectType,
convert_list_to_variant(ParamList),
FilterName,
pythoncom.VT_VARIANT,
)
if output is None:
return output
return pd.DataFrame(output, columns=ParamList)
[docs]
def GetParametersMultipleElementFlatOutput(
self, ObjectType: str, ParamList: list, FilterName: str = ""
) -> Union[None, Tuple[str]]:
"""Retrieves data for multiple elements in a flat, 1-D output format.
The data is returned as a single tuple of strings, where values for each
object are concatenated. This format can be less convenient for direct
DataFrame conversion but might be useful for specific parsing needs.
Parameters
----------
ObjectType : str
The PowerWorld object type.
ParamList : List[str]
A list of internal field names to retrieve.
FilterName : str, optional
Optional name of a PowerWorld filter to apply. Defaults to an empty string.
Returns
-------
Union[None, Tuple[str]]
A tuple of strings containing the data. Returns None if no data is found.
Raises
------
PowerWorldError
If the SimAuto call fails.
"""
result = self._com_call(
"GetParametersMultipleElementFlatOutput",
ObjectType,
convert_list_to_variant(ParamList),
FilterName,
)
if len(result) == 0:
return None
else:
return result
[docs]
def GetSpecificFieldList(self, ObjectType: str, FieldList: List[str]) -> pd.DataFrame:
"""Retrieves detailed metadata for a specific subset of fields for a given object type.
This method provides more detailed information about specific fields,
including their display names and whether they are enterable.
Parameters
----------
ObjectType : str
The PowerWorld object type.
FieldList : List[str]
A list of internal field names to query metadata for.
Returns
-------
pandas.DataFrame
A DataFrame with columns like 'variablename:location', 'field',
'column header', 'field description', and 'enterable'.
Raises
------
PowerWorldError
If the SimAuto call fails.
"""
base_cols = SpecificFieldListColumn.base_columns()
new_cols = SpecificFieldListColumn.new_columns()
sort_col = SpecificFieldListColumn.VARIABLENAME_LOCATION.value
try:
df = (
pd.DataFrame(
self._com_call("GetSpecificFieldList", ObjectType, convert_list_to_variant(FieldList)),
columns=base_cols,
)
.sort_values(by=sort_col)
.reset_index(drop=True)
)
except ValueError:
df = (
pd.DataFrame(
self._com_call("GetSpecificFieldList", ObjectType, convert_list_to_variant(FieldList)),
columns=new_cols,
)
.sort_values(by=sort_col)
.reset_index(drop=True)
)
return df
[docs]
def GetSpecificFieldMaxNum(self, ObjectType: str, Field: str) -> int:
"""Retrieves the maximum index for a field that supports multiple entries (e.g., CustomFloat).
Some PowerWorld fields, like 'CustomFloat', can have multiple instances
(e.g., 'CustomFloat:1', 'CustomFloat:2'). This method returns the highest
available index for such a field.
Parameters
----------
ObjectType : str
The PowerWorld object type.
Field : str
The base field name (e.g., 'CustomFloat').
Returns
-------
int
The maximum integer index available for the specified field.
Raises
------
PowerWorldError
If the SimAuto call fails.
"""
return self._com_call("GetSpecificFieldMaxNum", ObjectType, Field)
[docs]
def ListOfDevices(self, ObjType: str, FilterName="") -> Union[None, pd.DataFrame]:
"""Retrieves a list of all objects of a specific type and their primary keys.
This method is useful for getting an inventory of all objects of a certain type
in the case, or a filtered subset.
Parameters
----------
ObjType : str
The PowerWorld object type (e.g., 'Bus', 'Gen').
FilterName : str, optional
Optional name of a PowerWorld filter to apply. Defaults to an empty string.
Returns
-------
Union[None, pandas.DataFrame]
A pandas DataFrame containing the primary key fields for the objects.
Returns None if no objects are found.
Raises
------
PowerWorldError
If the SimAuto call fails.
"""
# Get key field metadata to know column names
key_col = FieldListColumn.KEY_FIELD.value
name_col = FieldListColumn.INTERNAL_FIELD_NAME.value
field_list = self.GetFieldList(ObjectType=ObjType, copy=False)
key_field_mask = field_list[key_col].str.match(r"\*[0-9]+[A-Z]*\*").to_numpy()
key_field_df = field_list.loc[key_field_mask].copy()
key_field_df[key_col] = key_field_df[key_col].str.replace(r"\*", "", regex=True)
key_field_df[key_col] = key_field_df[key_col].str.replace("[A-Z]*", "", regex=True)
key_field_series = key_field_df[key_col]
if self.decimal_delimiter != ".":
try:
key_field_series = key_field_series.str.replace(self.decimal_delimiter, ".")
except AttributeError:
pass
key_field_df["key_field_index"] = pd.to_numeric(key_field_series, errors='coerce').fillna(key_field_df[key_col]) - 1
key_field_df.sort_values(by="key_field_index", inplace=True)
column_names = key_field_df[name_col].to_numpy()
output = self._com_call("ListOfDevices", ObjType, FilterName)
all_none = all(i is None for i in output)
if all_none:
return None
df = pd.DataFrame(output).transpose()
df.columns = column_names
return df
[docs]
def ListOfDevicesAsVariantStrings(self, ObjType: str, FilterName="") -> tuple:
"""Retrieves a list of devices where primary keys are returned as variant strings.
This method returns the primary keys as a tuple of strings, which might be
useful for direct use in other SimAuto commands that expect string identifiers.
Parameters
----------
ObjType : str
The PowerWorld object type.
FilterName : str, optional
Optional name of a PowerWorld filter to apply. Defaults to an empty string.
Returns
-------
tuple
A tuple of strings, where each string represents the primary key(s) of an object.
Raises
------
PowerWorldError
If the SimAuto call fails.
"""
return self._com_call("ListOfDevicesAsVariantStrings", ObjType, FilterName)
[docs]
def ListOfDevicesFlatOutput(self, ObjType: str, FilterName="") -> tuple:
"""Retrieves a list of devices in a flat, 1-D output format.
Similar to `ListOfDevicesAsVariantStrings`, but the output format might differ
slightly depending on the SimAuto version.
Parameters
----------
ObjType : str
The PowerWorld object type.
FilterName : str, optional
Optional name of a PowerWorld filter to apply. Defaults to an empty string.
Returns
-------
tuple
A tuple of strings.
Raises
------
PowerWorldError
If the SimAuto call fails.
"""
return self._com_call("ListOfDevicesFlatOutput", ObjType, FilterName)
[docs]
def SendToExcel(self, ObjectType: str, FilterName: str, FieldList) -> None:
"""Exports data for the specified objects directly to Microsoft Excel.
This method requires Microsoft Excel to be installed on the system.
Parameters
----------
ObjectType : str
The PowerWorld object type (e.g., 'Bus', 'Gen').
FilterName : str
Optional PowerWorld filter name to apply.
FieldList : List[str]
A list of internal field names to export.
Returns
-------
None
Raises
------
PowerWorldError
If the SimAuto call fails (e.g., Excel not installed, invalid parameters).
"""
return self._com_call("SendToExcel", ObjectType, FilterName, FieldList)