From a52afadf25846287f0fcb05a625e48d155949225 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Tue, 3 Dec 2024 22:14:57 -0500 Subject: [PATCH 1/9] [WIP] Add plot func --- ams/plot.py | 52 +++++++++++++++++++++++++++++++++++++++++ ams/routines/routine.py | 3 +++ 2 files changed, 55 insertions(+) create mode 100644 ams/plot.py diff --git a/ams/plot.py b/ams/plot.py new file mode 100644 index 00000000..57af8c2a --- /dev/null +++ b/ams/plot.py @@ -0,0 +1,52 @@ +""" +Module for plotting functions. +""" + +import logging + +from andes.shared import plt + +from ams.shared import np + +logger = logging.getLogger(__name__) +DPI = None + + +class Plotter: + """ + Class for routine plotting functions. + """ + + def __init__(self, system): + self.system = system + + def bar(self, yidx, xidx=None, *, a=None, ytimes=None, ycalc=None, + left=None, right=None, ymin=None, ymax=None, + xlabel=None, ylabel=None, xheader=None, yheader=None, + legend=None, grid=False, greyscale=False, latex=True, + dpi=DPI, line_width=1.0, font_size=12, savefig=None, save_format=None, show=True, + title=None, linestyles=None, + hline1=None, hline2=None, vline1=None, vline2=None, hline=None, vline=None, + fig=None, ax=None, + set_xlim=True, set_ylim=True, autoscale=False, + legend_bbox=None, legend_loc=None, legend_ncol=1, + figsize=None, color=None, + **kwargs): + """ + Plot the variable in a bar plot. + """ + + if len(yidx) == 0: + logger.error("No variables to plot.") + return None + + y_idx = yidx.get_idx() + + if xidx is None: + xidx = yidx.horizon.v + + x_idx = [yidx.horizon.v.index(x) for x in xidx] + + yvalue = yidx.v[np.ix_(y_idx, x_idx)] + + x_loc = np.arange(len(x_idx)) diff --git a/ams/routines/routine.py b/ams/routines/routine.py index d58dd175..1c0c7b20 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -21,6 +21,8 @@ from ams.shared import pd +from ams.plot import Plotter + logger = logging.getLogger(__name__) @@ -114,6 +116,7 @@ def __init__(self, system=None, config=None): self.initialized = False # initialization flag self.type = "UndefinedType" # routine type self.docum = RDocumenter(self) # documentation generator + self.plt = Plotter(self) # plotter instance # --- sync mapping --- self.map1 = OrderedDict() # from ANDES From e589a48e6be1ba9b26ae8f579e654337c8d68b68 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 5 Dec 2024 13:41:09 -0500 Subject: [PATCH 2/9] Suspend plotter development for now --- ams/plot.py | 52 ----------------------------------------- ams/routines/routine.py | 3 --- 2 files changed, 55 deletions(-) delete mode 100644 ams/plot.py diff --git a/ams/plot.py b/ams/plot.py deleted file mode 100644 index 57af8c2a..00000000 --- a/ams/plot.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -Module for plotting functions. -""" - -import logging - -from andes.shared import plt - -from ams.shared import np - -logger = logging.getLogger(__name__) -DPI = None - - -class Plotter: - """ - Class for routine plotting functions. - """ - - def __init__(self, system): - self.system = system - - def bar(self, yidx, xidx=None, *, a=None, ytimes=None, ycalc=None, - left=None, right=None, ymin=None, ymax=None, - xlabel=None, ylabel=None, xheader=None, yheader=None, - legend=None, grid=False, greyscale=False, latex=True, - dpi=DPI, line_width=1.0, font_size=12, savefig=None, save_format=None, show=True, - title=None, linestyles=None, - hline1=None, hline2=None, vline1=None, vline2=None, hline=None, vline=None, - fig=None, ax=None, - set_xlim=True, set_ylim=True, autoscale=False, - legend_bbox=None, legend_loc=None, legend_ncol=1, - figsize=None, color=None, - **kwargs): - """ - Plot the variable in a bar plot. - """ - - if len(yidx) == 0: - logger.error("No variables to plot.") - return None - - y_idx = yidx.get_idx() - - if xidx is None: - xidx = yidx.horizon.v - - x_idx = [yidx.horizon.v.index(x) for x in xidx] - - yvalue = yidx.v[np.ix_(y_idx, x_idx)] - - x_loc = np.arange(len(x_idx)) diff --git a/ams/routines/routine.py b/ams/routines/routine.py index 1c0c7b20..d58dd175 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -21,8 +21,6 @@ from ams.shared import pd -from ams.plot import Plotter - logger = logging.getLogger(__name__) @@ -116,7 +114,6 @@ def __init__(self, system=None, config=None): self.initialized = False # initialization flag self.type = "UndefinedType" # routine type self.docum = RDocumenter(self) # documentation generator - self.plt = Plotter(self) # plotter instance # --- sync mapping --- self.map1 = OrderedDict() # from ANDES From 963a83b8f49bf7f099e7bba299e9d2edc8c3d614 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 5 Dec 2024 16:03:34 -0500 Subject: [PATCH 3/9] Add tests for ANDES convertion when there is TimedEvent --- ams/interface.py | 48 +++++++++++++++++++- docs/source/release-notes.rst | 1 + tests/{test_interop.py => test_interface.py} | 39 ++++++++++++++++ 3 files changed, 86 insertions(+), 2 deletions(-) rename tests/{test_interop.py => test_interface.py} (81%) diff --git a/ams/interface.py b/ams/interface.py index d019d752..6789051c 100644 --- a/ams/interface.py +++ b/ams/interface.py @@ -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 @@ -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 @@ -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'] @@ -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) diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 15fc5797..bc2e8dac 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -16,6 +16,7 @@ v0.9.13 (2024-xx-xx) - 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 v0.9.12 (2024-11-23) -------------------- diff --git a/tests/test_interop.py b/tests/test_interface.py similarity index 81% rename from tests/test_interop.py rename to tests/test_interface.py index 5dd8e709..5c8d37a3 100644 --- a/tests/test_interop.py +++ b/tests/test_interface.py @@ -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): """ From abbc614ca1865fc009216cb838df48d0b109ce4a Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 5 Dec 2024 16:14:17 -0500 Subject: [PATCH 4/9] Add placeholder var vBus to DCBase and method dc2ac --- ams/routines/dcopf.py | 47 +++++++++++++++++++++++++++++++++++++++++++ ams/routines/dcpf.py | 4 ++++ 2 files changed, 51 insertions(+) diff --git a/ams/routines/dcopf.py b/ams/routines/dcopf.py index 846ff0dd..38edb7b9 100644 --- a/ams/routines/dcopf.py +++ b/ams/routines/dcopf.py @@ -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 + + # --- 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(' did not converge, conversion failed.') + # 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 + 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. diff --git a/ams/routines/dcpf.py b/ams/routines/dcpf.py index 11fd8e3f..6a5e503e 100644 --- a/ams/routines/dcpf.py +++ b/ams/routines/dcpf.py @@ -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}', From 75e750dc123d0e285e47899cb9dcb912b9338365 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 5 Dec 2024 16:18:13 -0500 Subject: [PATCH 5/9] Supplement RParam ug to routine DCPF0 --- ams/routines/acopf.py | 13 +++++-------- ams/routines/dcpf0.py | 5 +++++ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/ams/routines/acopf.py b/ams/routines/acopf.py index e4403493..69f5f784 100644 --- a/ams/routines/acopf.py +++ b/ams/routines/acopf.py @@ -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', diff --git a/ams/routines/dcpf0.py b/ams/routines/dcpf0.py index 45ab6b82..5102ee7d 100644 --- a/ams/routines/dcpf0.py +++ b/ams/routines/dcpf0.py @@ -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', From b1ec1e5629fea441735f17ff9187c8154a02bddd Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 5 Dec 2024 16:21:10 -0500 Subject: [PATCH 6/9] Add test for DCOPF dc2ac --- tests/test_rtn_dcopf.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_rtn_dcopf.py b/tests/test_rtn_dcopf.py index 7903f553..765907c7 100644 --- a/tests/test_rtn_dcopf.py +++ b/tests/test_rtn_dcopf.py @@ -1,4 +1,5 @@ import unittest +import numpy as np import ams @@ -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) From 3db20745d11d965505f3cdd430f2b4994045d3aa Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 5 Dec 2024 16:22:05 -0500 Subject: [PATCH 7/9] Update release notes --- docs/source/release-notes.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index bc2e8dac..4bc3eea8 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -17,6 +17,7 @@ v0.9.13 (2024-xx-xx) - 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) -------------------- From 88cf23a60fa81394148ec63693b92fb36d21906c Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 5 Dec 2024 16:31:22 -0500 Subject: [PATCH 8/9] Add target audience and example usage in README --- README.md | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 87585317..93751cbe 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 +>>> ``` # Sponsors and Contributors From 04bc34d9a84e9080b393711b1aead604dd27b8c5 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 5 Dec 2024 16:32:27 -0500 Subject: [PATCH 9/9] Update release notes --- docs/source/release-notes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 4bc3eea8..933ac41d 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -9,7 +9,7 @@ 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``