Skip to content
Open
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
162 changes: 119 additions & 43 deletions esa/saw.py
Original file line number Diff line number Diff line change
Expand Up @@ -779,49 +779,84 @@ def get_shunt_admittance(self):
df.fillna(0, inplace=True)
return (df['BusSSMW'].to_numpy() + 1j * df['BusSS'].to_numpy()) / base

def get_jacobian(self, full=False):
"""Helper function to get the Jacobian matrix, by default return a
scipy sparse matrix in the csr format
:param full: Convert the csr_matrix to the numpy array (full matrix).
def get_gmatrix(self, full: bool = False) -> Union[np.ndarray, csr_matrix]:
"""Helper function to get the GIC conductance matrix (G).

By default, this function returns a scipy sparse matrix in csr format.

:param full: If True, convert the csr_matrix to a dense numpy array.
:return: The GIC conductance matrix as a sparse or dense matrix.
"""
jacfile = tempfile.NamedTemporaryFile(mode='w', suffix='.m',
delete=False)
jacfile_path = Path(jacfile.name).as_posix()
jacfile.close()
jidfile = tempfile.NamedTemporaryFile(mode='w', delete=False)
jidfile_path = Path(jidfile.name).as_posix()
jidfile.close()
cmd = f'SaveJacobian("{jacfile_path}","{jidfile_path}",M,R);'
self.RunScriptCommand(cmd)
with open(jacfile_path, 'r') as f:
mat_str = f.read()
os.unlink(jacfile.name)
os.unlink(jidfile.name)
mat_str = re.sub(r'\s', '', mat_str)
lines = re.split(';', mat_str)
ie = r'[0-9]+'
fe = r'-*[0-9]+\.[0-9]+'
dr = re.compile(r'(?:Jac)=(?:sparse\()({ie})'.format(ie=ie))
exp = re.compile(
r'(?:Jac\()({ie}),({ie})(?:\)=)({fe})'.format(
ie=ie, fe=fe))
row = []
col = []
data = []
# Get the dimension from the first line in lines
dim = dr.match(lines[0])[1]
n = int(dim)
for line in lines[1:]:
match = exp.match(line)
if match is None:
continue
idx1, idx2, real = match.groups()
row.append(int(idx1))
col.append(int(idx2))
data.append(float(real))
sparse_matrix = csr_matrix(
(data, (np.asarray(row) - 1, np.asarray(col) - 1)), shape=(n, n))
return sparse_matrix.toarray() if full else sparse_matrix
# Create temporary files for PowerWorld to write to
g_matrix_file = tempfile.NamedTemporaryFile(mode="w", suffix=".m", delete=False)
id_file = tempfile.NamedTemporaryFile(mode="w", delete=False)

try:
g_matrix_path = Path(g_matrix_file.name).as_posix()
id_file_path = Path(id_file.name).as_posix()
g_matrix_file.close()
id_file.close()

# Tell PowerWorld to write the G matrix to the files
cmd = f'GICSaveGMatrix("{g_matrix_path}","{id_file_path}");'

# NOTE: PowerWorld seems to require the command to be called twice
# to ensure all data is written to the file.
self.RunScriptCommand(cmd)
self.RunScriptCommand(cmd)

# Read the matrix data from the temporary file
with open(g_matrix_path, "r") as f:
mat_str = f.read()

# Parse the matrix string
sparse_matrix = self._parse_real_matrix(mat_str, 'GMatrix')

return sparse_matrix.toarray() if full else sparse_matrix

finally:
# Ensure temporary files are deleted.
os.unlink(g_matrix_file.name)
os.unlink(id_file.name)

def get_jacobian(self, full: bool = False) -> Union[np.ndarray, csr_matrix]:
"""Helper function to get the Jacobian matrix.

By default, this function returns a scipy sparse matrix in csr format.

Note: The power flow must be solved before calling this function, otherwise
an error will be raised by PowerWorld.

:param full: If True, convert the csr_matrix to a dense numpy array.
:return: The Jacobian matrix as a sparse or dense matrix.
"""
# Create temporary files for PowerWorld to write to
jac_file = tempfile.NamedTemporaryFile(mode="w", suffix=".m", delete=False)
id_file = tempfile.NamedTemporaryFile(mode="w", delete=False)

try:
jac_file_path = Path(jac_file.name).as_posix()
id_file_path = Path(id_file.name).as_posix()
jac_file.close()
id_file.close()

# Tell PowerWorld to write the Jacobian matrix to the files.
cmd = f'SaveJacobian("{jac_file_path}","{id_file_path}",M,R);'
self.RunScriptCommand(cmd)

# Read the matrix data from the temporary file.
with open(jac_file_path, "r") as f:
mat_str = f.read()

# Parse the matrix string into a sparse matrix.
sparse_matrix = self._parse_real_matrix(mat_str, "Jac")

return sparse_matrix.toarray() if full else sparse_matrix

finally:
# Ensure temporary files are deleted.
os.unlink(jac_file.name)
os.unlink(id_file.name)

def to_graph(self, node: str = 'bus', geographic: bool = False,
directed: bool = False, node_attr=None, edge_attr=None) \
Expand Down Expand Up @@ -3010,7 +3045,7 @@ def CloseOneline(self, OnelineName: str = "") -> None:

:returns: None
"""
script = f'CloseOneline({OnelineName})'
script = f'CloseOneline("{OnelineName}")'
return self.RunScriptCommand(script)

####################################################################
Expand Down Expand Up @@ -3335,6 +3370,47 @@ def _replace_decimal_delimiter(self, data: pd.Series):
except AttributeError:
return data

def _parse_real_matrix(self, mat_str, matrix_name="Jac"):
"""Helper to parse a real-valued sparse matrix from '.m' PowerWorld output.

:param mat_str: String representation of a real-valued sparse
matrix, as returned by PowerWorld.
:param matrix_name: Name of the matrix to be parsed.
:returns: CSR sparse matrix.
"""
# Regex for File Format
mat_str = re.sub(r"\s", "", mat_str)
lines = re.split(";", mat_str)
ie = r"[0-9]+"
fe = r"-*[0-9]+\.[0-9]+"

# Regex for Data Extraction
dr_raw = r"(?:{matrix_name})=(?:sparse\()({ie})".format(ie=ie, matrix_name=matrix_name)
exp_raw = r"(?:{matrix_name}\()({ie}),({ie})(?:\)=)({fe})".format(ie=ie, fe=fe, matrix_name=matrix_name)
dr = re.compile(dr_raw)
exp = re.compile(exp_raw)


# Get the dimension from the first line in lines
dim = dr.match(lines[0])[1]
n = int(dim)

# Empty Lists
row, col, data = [], [], []

for line in lines[1:]:
match = exp.match(line)
if match is None:
continue
idx1, idx2, real = match.groups()
row.append(int(idx1))
col.append(int(idx2))
data.append(float(real))

# Return CSR Sparse Matrix
return csr_matrix(
(data, (np.asarray(row) - 1, np.asarray(col) - 1)), shape=(n, n)
)

def df_to_aux(fp, df, object_name: str):
""" Convert a dataframe to PW aux/axd data section.
Expand Down
4 changes: 2 additions & 2 deletions tests/cases/ieee_14/IEEE 14 bus.pwd
Git LFS file not shown
3 changes: 3 additions & 0 deletions tests/cases/ieee_14/IEEE 14 bus_pws_version_24.PWB
Git LFS file not shown
4 changes: 2 additions & 2 deletions tests/cases/tx2000/tx2000.pwd
Git LFS file not shown
3 changes: 3 additions & 0 deletions tests/cases/tx2000/tx2000_base_pws_version_24.PWB
Git LFS file not shown
4 changes: 2 additions & 2 deletions tests/cases/wscc_9/WSCC 9 bus.pwd
Git LFS file not shown
3 changes: 3 additions & 0 deletions tests/cases/wscc_9/WSCC 9 bus_pws_version_24.PWB
Git LFS file not shown
63 changes: 38 additions & 25 deletions tests/test_saw.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ def test_3wxformer(self):

# Key fields have changed for 3 winding transformers between
# versions.
if VERSION in [21, 22, 23]:
if VERSION in [21, 22, 23, 24]:
expected = ['BusIdentifier', 'BusIdentifier:1', 'BusIdentifier:2',
'LineCircuit']
elif VERSION == 17:
Expand Down Expand Up @@ -551,7 +551,7 @@ class IdentifyNumericFieldsTestCase(unittest.TestCase):
# noinspection PyMethodMayBeStatic
def test_correct(self):
# Intentionally make the fields out of alphabetical order.
if VERSION in [21, 22, 23]:
if VERSION in [21, 22, 23, 24]:
fields = ['LineStatus', 'LockOut', 'LineR', 'LineX', 'BusNum']
expected = np.array([False, False, True, True, True])
elif VERSION == 17:
Expand Down Expand Up @@ -745,6 +745,29 @@ def test_get_jacobian_full(self):
self.assertIsInstance(self.saw.get_jacobian(full=True), np.ndarray)


class GetGMatrixTestCase(unittest.TestCase):
"""Test get_gmatrix function."""

@classmethod
def setUpClass(cls) -> None:
cls.saw = SAW(PATH_14, early_bind=True)

@classmethod
def tearDownClass(cls) -> None:
# noinspection PyUnresolvedReferences
cls.saw.exit()

def test_get_gmatrix_default(self):
"""It should return a scipy csr_matrix.
"""
self.assertIsInstance(self.saw.get_gmatrix(), csr_matrix)

def test_get_gmatrix_full(self):
"""It should return a numpy array of full matrix.
"""
self.assertIsInstance(self.saw.get_gmatrix(full=True), np.ndarray)


class ChangeToTemperatureTestCase(unittest.TestCase):
"""Test change_to_temperature function."""

Expand Down Expand Up @@ -2460,23 +2483,23 @@ def test_create_if_not_found(self):
self.assertFalse(saw_14.CreateIfNotFound)

def test_program_information(self):
# Program Information introduced in version 21.
if VERSION < 21:
with self.assertLogs(logger=saw_14.log, level='WARN'):
self.assertFalse(saw_14.ProgramInformation)
return

# Check returned values are variant arrays
result = saw_14.ProgramInformation
result_list = list(result) # convert tuple of tuples to list of tuples
self.assertIsInstance(saw_14.ProgramInformation, tuple)

# Checking first entry of each tuple
values = ['version', 'addons', 'executable']
k = 0
first_element = map(lambda x: x[0], result_list)
for i in first_element:
self.assertEqual(i, values[k])
k = k + 1
# Get the first element of each sub-tuple
first_elements = [item[0] for item in result]

# Program Information introduced in version 21.
if VERSION < 21:
self.assertFalse(saw_14.ProgramInformation)
else:
self.assertIsInstance(saw_14.ProgramInformation, tuple)
# Check that the expected values are present at the start.
# The property may return more data in future versions.
expected_values = ['version', 'addons', 'executable']
self.assertListEqual(first_elements[:len(expected_values)], expected_values)


########################################################################
Expand Down Expand Up @@ -2562,16 +2585,6 @@ def test_close_with_wrong_name(self):
'Cannot find Oneline'):
saw_14.CloseOneline("A file that cannot be found")

def test_close_with_invalid_identifier(self):
"""Close the oneline diagram with invalid identifier should
raise a PowerWorld Error
"""
saw_14.OpenOneLine(PATH_14_PWD)
with self.assertRaisesRegex(PowerWorldError,
'invalid identifier character'):
saw_14.CloseOneline(PATH_14_PWD)


########################################################################
# Misc tests
########################################################################
Expand Down