Skip to content

Commit

Permalink
Merge pull request #107 from CURENT/misc
Browse files Browse the repository at this point in the history
Misc
  • Loading branch information
jinningwang authored Dec 5, 2024
2 parents 42ffff6 + 04bc34d commit c665d88
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 16 deletions.
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ Python Software for Power System Scheduling Modeling and Co-Simulation with Dyna
With the built-in interface with ANDES, AMS enables **Dynamics Incorporated**
**Stability-Constrained Scheduling**.

This package can be helpful for power system engineers, researchers, and students
who need to conduct scheduling studies and transient stability studies at given
operating points.

AMS is a **Modeling Framework** that provides a descriptive way to formulate
scheduling problems. The optimization problems are then handled by **CVXPY**
and solved with third-party solvers.
Expand Down Expand Up @@ -91,15 +95,22 @@ pip install git+https://github.com/CURENT/ams.git

# Example Usage

Using AMS to run a Real-Time Economic Dispatch (RTED) simulation:

```python
import ams

ss = ams.load(ams.get_case('ieee14_uced.xlsx'))
ss.RTED.run()
ss = ams.load(ams.get_case('ieee14/ieee14_uced.xlsx'))

# solve RTED
ss.RTED.run(solver='CLARABEL')

ss.RTED.pg.v
>>> array([1.8743862, 0.3226138, 0.01 , 0.02 , 0.01 ])

print(ss.RTED.pg.v)
# convert to ANDES case
sa = ss.to_andes(addfile=andes.get_case('ieee14/ieee14_full.xlsx'),
setup=True, verify=False)
sa
>>> <andes.system.System at 0x14bd98190>
```

# Sponsors and Contributors
Expand Down
48 changes: 46 additions & 2 deletions ams/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ def sync_adsys(amsys, adsys):
ad_mdl.set(src=param, attr='v', idx=idx,
value=am_mdl.get(src=param, attr='v', idx=idx))
except Exception:
logger.debug(f"Skip updating {mname}.{param}")
continue
return True

Expand Down Expand Up @@ -333,6 +332,27 @@ def parse_addfile(adsys, amsys, addfile):
df[idxn] = df[idxn].replace(idx_map[mdl_guess])
logger.debug(f'Adjust {idxp.class_name} <{name}.{idxp.name}>')

# NOTE: Group TimedEvent needs special treatment
# adjust Toggle and Fault models
toggle_df = df_models.get('Toggle') or df_models.get('Toggler')
if toggle_df is not None:
toggle_df['dev'] = toggle_df.apply(replace_dev, axis=1,
mdl='model', dev='dev',
idx_map=idx_map)

alter_df = df_models.get('Alter')
if alter_df is not None:
alter_df['dev'] = alter_df.apply(replace_dev, axis=1,
mdl='model', dev='dev',
idx_map=idx_map)

# adjust Fault model
fault_df = df_models.get('Fault')
if fault_df is not None:
fault_df['bus'] = fault_df.apply(replace_dev, axis=1,
mdl='bus', dev='bus',
idx_map=idx_map)

# add dynamic models
for name, df in df_models.items():
# drop rows that all nan
Expand Down Expand Up @@ -966,7 +986,7 @@ def make_link_table(adsys):
right=ssa_rg[['stg_idx', 'rg_idx']])

# NOTE: use this instead of fillna to avoid type conversion
idxc = ['stg_idx', 'syg_idx', 'dg_idx', 'rg_idx']
idxc = ['syg_idx', 'dg_idx', 'rg_idx']
ssa_key0[idxc] = ssa_key0[idxc].astype('str').replace({'nan': ''}).astype('bool')

dyr = ssa_key0['syg_idx'] + ssa_key0['dg_idx'] + ssa_key0['rg_idx']
Expand Down Expand Up @@ -1037,3 +1057,27 @@ def verify_pf(amsys, adsys, tol=1e-3):
logger.warning(msg)
logger.warning(diff_msg)
return check


def replace_dev(row, mdl, dev, idx_map):
"""
Replace the device idx in the row based on the idx_map.
Parameters
----------
row : pd.Series
The row of the DataFrame.
mdl : str
The column name for the Model.
dev : str
The column name for the Device idx.
idx_map : dict
The index map for replacement.
Returns
-------
str
The new device idx.
"""
old_idx = row[dev]
return idx_map.get(row[mdl], {}).get(old_idx, old_idx)
13 changes: 5 additions & 8 deletions ams/routines/acopf.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,11 @@ def __init__(self, system, config):
self.type = 'ACED'

self.map1 = OrderedDict() # ACOPF does not receive
self.map2 = OrderedDict([
('Bus', {
'vBus': 'v0',
}),
('StaticGen', {
'pg': 'p0',
}),
])
self.map2.update({
'vBus': ('Bus', 'v0'),
'ug': ('StaticGen', 'u'),
'pg': ('StaticGen', 'p0'),
})

# --- params ---
self.c2 = RParam(info='Gen cost coefficient 2',
Expand Down
47 changes: 47 additions & 0 deletions ams/routines/dcopf.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,53 @@ def __init__(self, system, config):
info='total cost', unit='$',
sense='min', e_str=obj,)

def dc2ac(self, kloss=1.0, **kwargs):
"""
Convert the RTED results with ACOPF.
Parameters
----------
kloss : float, optional
The loss factor for the conversion. Defaults to 1.2.
"""
exec_time = self.exec_time
if self.exec_time == 0 or self.exit_code != 0:
logger.warning(f'{self.class_name} is not executed successfully, quit conversion.')
return False

Check warning on line 161 in ams/routines/dcopf.py

View check run for this annotation

Codecov / codecov/patch

ams/routines/dcopf.py#L160-L161

Added lines #L160 - L161 were not covered by tests

# --- ACOPF ---
# scale up load
pq_idx = self.system.StaticLoad.get_idx()
pd0 = self.system.StaticLoad.get(src='p0', attr='v', idx=pq_idx).copy()
qd0 = self.system.StaticLoad.get(src='q0', attr='v', idx=pq_idx).copy()
self.system.StaticLoad.set(src='p0', idx=pq_idx, attr='v', value=pd0 * kloss)
self.system.StaticLoad.set(src='q0', idx=pq_idx, attr='v', value=qd0 * kloss)
# run ACOPF
ACOPF = self.system.ACOPF
ACOPF.run()
# scale load back
self.system.StaticLoad.set(src='p0', idx=pq_idx, attr='v', value=pd0)
self.system.StaticLoad.set(src='q0', idx=pq_idx, attr='v', value=qd0)
if not ACOPF.exit_code == 0:
logger.warning('<ACOPF> did not converge, conversion failed.')

Check warning on line 177 in ams/routines/dcopf.py

View check run for this annotation

Codecov / codecov/patch

ams/routines/dcopf.py#L177

Added line #L177 was not covered by tests
# NOTE: mock results to fit interface with ANDES
self.vBus = ACOPF.vBus
self.vBus.optz.value = np.ones(self.system.Bus.n)
self.aBus.optz.value = np.zeros(self.system.Bus.n)
return False

Check warning on line 182 in ams/routines/dcopf.py

View check run for this annotation

Codecov / codecov/patch

ams/routines/dcopf.py#L179-L182

Added lines #L179 - L182 were not covered by tests
self.pg.optz.value = ACOPF.pg.v

# NOTE: mock results to fit interface with ANDES
self.vBus.optz.value = ACOPF.vBus.v
self.aBus.optz.value = ACOPF.aBus.v
self.exec_time = exec_time

# --- set status ---
self.system.recent = self
self.converted = True
logger.warning(f'<{self.class_name}> converted to AC.')
return True

def run(self, **kwargs):
"""
Run the routine.
Expand Down
4 changes: 4 additions & 0 deletions ams/routines/dcpf.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ def __init__(self, system, config):
v0=self.pg0)

# --- bus ---
self.vBus = Var(info='Bus voltage magnitude, placeholder',
unit='p.u.',
name='vBus', tex_name=r'v_{Bus}',
src='v', model='Bus',)
self.aBus = Var(info='Bus voltage angle',
unit='rad',
name='aBus', tex_name=r'\theta_{bus}',
Expand Down
5 changes: 5 additions & 0 deletions ams/routines/dcpf0.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ def __init__(self, system, config):
self.info = 'DC Power Flow'
self.type = 'PF'

self.ug = RParam(info='Gen connection status',
name='ug', tex_name=r'u_{g}',
model='StaticGen', src='u',
no_parse=True)

# --- routine data ---
self.x = RParam(info="line reactance",
name='x', tex_name='x',
Expand Down
4 changes: 3 additions & 1 deletion docs/source/release-notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ The APIs before v3.0.0 are in beta and may change without prior notice.
Pre-v1.0.0
==========

v0.9.13 (2024-xx-xx)
v0.9.13 (2024-12-05)
--------------------

- Add a step to report in ``RoutineBase.run``
- Add more tests to cover DG and ES related routines
- Improve formulation for DG and ESD involved routines
- Improve module ``Report`` and method ``RoutineBase.export_csv``
- Support ``TimedEvent`` in ANDES case conversion
- Add Var ``vBus`` in ``DCOPF`` for placeholder

v0.9.12 (2024-11-23)
--------------------
Expand Down
39 changes: 39 additions & 0 deletions tests/test_interop.py → tests/test_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,45 @@ def test_verify_pf(self):
verify=False, tol=1e-3)
self.assertTrue(verify_pf(amsys=sp, adsys=sa, tol=1e-7))

def test_to_andes_toggle(self):
"""
Test conversion when there is Toggle in ANDES case.
"""
sp = ams.load(ams.get_case('5bus/pjm5bus_demo.xlsx'),
setup=True,
no_output=True,
default_config=True)
sa = sp.to_andes(setup=True,
addfile=andes.get_case('5bus/pjm5bus.xlsx'),
verify=False)
self.assertGreater(sa.Toggle.n, 0)

def test_to_andes_alter(self):
"""
Test conversion when there is Alter in ANDES case.
"""
sp = ams.load(ams.get_case('ieee14/ieee14_uced.xlsx'),
setup=True,
no_output=True,
default_config=True)
sa = sp.to_andes(setup=True,
addfile=andes.get_case('ieee14/ieee14_alter.xlsx'),
verify=False)
self.assertGreater(sa.Alter.n, 0)

def test_to_andes_fault(self):
"""
Test conversion when there is Toggle in ANDES case.
"""
sp = ams.load(ams.get_case('ieee14/ieee14_uced.xlsx'),
setup=True,
no_output=True,
default_config=True)
sa = sp.to_andes(setup=True,
addfile=andes.get_case('ieee14/ieee14_fault.xlsx'),
verify=False)
self.assertGreater(sa.Fault.n, 0)


class TestDataExchange(unittest.TestCase):
"""
Expand Down
24 changes: 24 additions & 0 deletions tests/test_rtn_dcopf.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import unittest
import numpy as np

import ams

Expand Down Expand Up @@ -75,3 +76,26 @@ def test_set_load(self):
self.ss.DCOPF.run(solver='CLARABEL')
pgs_pqt2 = self.ss.DCOPF.pg.v.sum()
self.assertLess(pgs_pqt2, pgs_pqt, "Load trip does not take effect!")

def test_dc2ac(self):
"""
Test `DCOPF.dc2ac()` method.
"""
self.ss.DCOPF.run(solver='CLARABEL')
self.ss.DCOPF.dc2ac()
self.assertTrue(self.ss.DCOPF.converted, "AC conversion failed!")
self.assertTrue(self.ss.DCOPF.exec_time > 0, "Execution time is not greater than 0.")

stg_idx = self.ss.StaticGen.get_idx()
pg_dcopf = self.ss.DCOPF.get(src='pg', attr='v', idx=stg_idx)
pg_acopf = self.ss.ACOPF.get(src='pg', attr='v', idx=stg_idx)
np.testing.assert_almost_equal(pg_dcopf, pg_acopf, decimal=3)

bus_idx = self.ss.Bus.get_idx()
v_dcopf = self.ss.DCOPF.get(src='vBus', attr='v', idx=bus_idx)
v_acopf = self.ss.ACOPF.get(src='vBus', attr='v', idx=bus_idx)
np.testing.assert_almost_equal(v_dcopf, v_acopf, decimal=3)

a_dcopf = self.ss.DCOPF.get(src='aBus', attr='v', idx=bus_idx)
a_acopf = self.ss.ACOPF.get(src='aBus', attr='v', idx=bus_idx)
np.testing.assert_almost_equal(a_dcopf, a_acopf, decimal=3)

0 comments on commit c665d88

Please sign in to comment.