From 400d10458babfb8e26859ab69dcca6883c31493a Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 23 Nov 2024 22:03:24 -0500 Subject: [PATCH 01/33] Add a step to report in RoutineBase.run --- ams/routines/routine.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ams/routines/routine.py b/ams/routines/routine.py index 1abbef33..f77d014a 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -422,6 +422,7 @@ def run(self, **kwargs): logger.warning(msg) self.unpack(**kwargs) self._post_solve() + self.system.report() return True else: msg = f"{self.class_name} failed as {status} in " From eddd48375d746749f198d7668fcc52343205438f Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 23 Nov 2024 22:12:43 -0500 Subject: [PATCH 02/33] Add a step to report in ACOPF.run --- ams/routines/dcpf0.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ams/routines/dcpf0.py b/ams/routines/dcpf0.py index d3a7403c..45ab6b82 100644 --- a/ams/routines/dcpf0.py +++ b/ams/routines/dcpf0.py @@ -169,6 +169,7 @@ def run(self, **kwargs): except Exception as e: logger.error(f"Failed to unpack results from {self.class_name}.\n{e}") return False + self.system.report() return True else: msg = f"{self.class_name} failed in " From a3d94ac04d5057f7d9e1c0b3ece4e3a63c48f1fb Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 23 Nov 2024 22:34:51 -0500 Subject: [PATCH 03/33] Minor enhancement --- ams/report.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/ams/report.py b/ams/report.py index abc3c0a7..cc4383d9 100644 --- a/ams/report.py +++ b/ams/report.py @@ -15,6 +15,9 @@ logger = logging.getLogger(__name__) +DECIMALS = 6 + + def report_info(system) -> list: info = list() info.append('AMS' + ' ' + version + '\n') @@ -112,7 +115,7 @@ def collect(self, rtn, horizon=None): owner_name = var.owner.class_name idx_v = owners[owner_name]['idx'] header_v = key if var.unit is None else f'{key} ({var.unit})' - data_v = rtn.get(src=key, attr='v', idx=idx_v, horizon=horizon).round(6) + data_v = rtn.get(src=key, attr='v', idx=idx_v, horizon=horizon).round(DECIMALS) owners[owner_name]['header'].append(header_v) owners[owner_name]['data'].append(data_v) @@ -182,22 +185,22 @@ def write(self): ['Generation', 'Load']) if hasattr(rtn, 'pd'): - pd = rtn.pd.v.sum().round(6) + pd = rtn.pd.v.sum().round(DECIMALS) else: - pd = rtn.system.PQ.p0.v.sum().round(6) + pd = rtn.system.PQ.p0.v.sum().round(DECIMALS) if hasattr(rtn, 'qd'): - qd = rtn.qd.v.sum().round(6) + qd = rtn.qd.v.sum().round(DECIMALS) else: - qd = rtn.system.PQ.q0.v.sum().round(6) + qd = rtn.system.PQ.q0.v.sum().round(DECIMALS) if rtn.type in ['ACED', 'PF']: header.append(['P (p.u.)', 'Q (p.u.)']) - Pcol = [rtn.pg.v.sum().round(6), pd] - Qcol = [rtn.qg.v.sum().round(6), qd] + Pcol = [rtn.pg.v.sum().round(DECIMALS), pd] + Qcol = [rtn.qg.v.sum().round(DECIMALS), qd] data.append([Pcol, Qcol]) else: header.append(['P (p.u.)']) - Pcol = [rtn.pg.v.sum().round(6), pd] + Pcol = [rtn.pg.v.sum().round(DECIMALS), pd] data.append([Pcol]) # --- routine data --- From db3a7190bafffa6286899c6cdcfa354938e61208 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 23 Nov 2024 22:48:41 -0500 Subject: [PATCH 04/33] Add timeslot for pmaxe in routines ED and UC --- ams/routines/ed.py | 2 ++ ams/routines/uc.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/ams/routines/ed.py b/ams/routines/ed.py index 944408e4..e7e5db40 100644 --- a/ams/routines/ed.py +++ b/ams/routines/ed.py @@ -160,8 +160,10 @@ def __init__(self, system, config): self.nctrle.u2 = self.ugt pmaxe = 'mul(mul(nctrle, pg0), tlv) + mul(mul(ctrle, tlv), pmax)' self.pmaxe.e_str = pmaxe + self.pmaxe.horizon = self.timeslot pmine = 'mul(mul(nctrle, pg0), tlv) + mul(mul(ctrle, tlv), pmin)' self.pmine.e_str = pmine + self.pmine.horizon = self.timeslot self.pglb.e_str = '-pg + pmine' self.pgub.e_str = 'pg - pmaxe' diff --git a/ams/routines/uc.py b/ams/routines/uc.py index f3f49df3..70063ac4 100644 --- a/ams/routines/uc.py +++ b/ams/routines/uc.py @@ -155,8 +155,10 @@ def __init__(self, system, config): self.nctrle.info = 'Reshaped non-controllability' pmaxe = 'mul(mul(nctrl, pg0), ugd) + mul(mul(ctrl, pmax), ugd)' self.pmaxe.e_str = pmaxe + self.pmaxe.horizon = self.timeslot pmine = 'mul(mul(ctrl, pmin), ugd) + mul(mul(nctrl, pg0), ugd)' self.pmine.e_str = pmine + self.pmine.horizon = self.timeslot self.pglb.e_str = '-pg + pmine' self.pgub.e_str = 'pg - pmaxe' From c5b01dba4e62d3b5ee350578380e684b5c34e2ab Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 23 Nov 2024 23:02:24 -0500 Subject: [PATCH 05/33] Suspend UC.pi.model as it is invalid in UC --- ams/routines/uc.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ams/routines/uc.py b/ams/routines/uc.py index 70063ac4..7cc3a149 100644 --- a/ams/routines/uc.py +++ b/ams/routines/uc.py @@ -195,6 +195,10 @@ def __init__(self, system, config): info='initial shutdown action', e_str='-ugd[:, 0] + ug[:, 0] - wgd[:, 0]',) + # NOTE: suspend pi.owner as it is empty in this routine + self.pi.model = None + self.pi.info = 'Place holder of LMP' + self.prs.horizon = self.timeslot self.prs.info = '2D Spinning reserve' From db14a049311dee6a835571c0a61770f265be2779 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 23 Nov 2024 23:02:49 -0500 Subject: [PATCH 06/33] Include ExpressionCalc and Expression in Report --- ams/report.py | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/ams/report.py b/ams/report.py index cc4383d9..fdaf0b4f 100644 --- a/ams/report.py +++ b/ams/report.py @@ -87,10 +87,17 @@ def collect(self, rtn, horizon=None): # initialize data section by model owners_all = ['Bus', 'Line', 'StaticGen', 'PV', 'Slack', 'RenGen', - 'DG', 'ESD1', 'PVD1'] + 'DG', 'ESD1', 'PVD1', + 'StaticLoad'] # Filter owners that exist in the system - owners_e = [var.owner.class_name for var in rtn.vars.values() if var.owner is not None] + owners_e = list({ + var.owner.class_name for var in rtn.vars.values() if var.owner is not None + }.union( + expr.owner.class_name for expr in rtn.exprs.values() if expr.owner is not None + ).union( + exprc.owner.class_name for exprc in rtn.exprcs.values() if exprc.owner is not None + )) # Use a dictionary comprehension to create vars_by_owner owners = { @@ -112,6 +119,8 @@ def collect(self, rtn, horizon=None): # --- variables data --- for key, var in rtn.vars.items(): + if var.owner is None: + continue owner_name = var.owner.class_name idx_v = owners[owner_name]['idx'] header_v = key if var.unit is None else f'{key} ({var.unit})' @@ -119,6 +128,28 @@ def collect(self, rtn, horizon=None): owners[owner_name]['header'].append(header_v) owners[owner_name]['data'].append(data_v) + # --- Expressions data --- + for key, expr in rtn.exprs.items(): + if expr.owner is None: + continue + owner_name = expr.owner.class_name + idx_v = owners[owner_name]['idx'] + header_v = key if expr.unit is None else f'{key} ({expr.unit})' + data_v = rtn.get(src=key, attr='v', idx=idx_v, horizon=horizon).round(DECIMALS) + owners[owner_name]['header'].append(header_v) + owners[owner_name]['data'].append(data_v) + + # --- ExpressionCalc data --- + for key, exprc in rtn.exprcs.items(): + if exprc.owner is None: + continue + owner_name = exprc.owner.class_name + idx_v = owners[owner_name]['idx'] + header_v = key if exprc.unit is None else f'{key} ({exprc.unit})' + data_v = rtn.get(src=key, attr='v', idx=idx_v, horizon=horizon).round(DECIMALS) + owners[owner_name]['header'].append(header_v) + owners[owner_name]['data'].append(data_v) + # --- dump data --- for key, val in owners.items(): text.append([f'{key} DATA:\n']) From 7f6166031552febc1b765f6ab26f7427c448a624 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 23 Nov 2024 23:19:55 -0500 Subject: [PATCH 07/33] Fix empty element values get in report --- ams/report.py | 15 ++++++++++++--- ams/routines/uc.py | 4 ---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/ams/report.py b/ams/report.py index fdaf0b4f..3f69053a 100644 --- a/ams/report.py +++ b/ams/report.py @@ -124,7 +124,10 @@ def collect(self, rtn, horizon=None): owner_name = var.owner.class_name idx_v = owners[owner_name]['idx'] header_v = key if var.unit is None else f'{key} ({var.unit})' - data_v = rtn.get(src=key, attr='v', idx=idx_v, horizon=horizon).round(DECIMALS) + try: + data_v = rtn.get(src=key, attr='v', idx=idx_v, horizon=horizon).round(DECIMALS) + except Exception: + data_v = [np.nan] * len(idx_v) owners[owner_name]['header'].append(header_v) owners[owner_name]['data'].append(data_v) @@ -135,7 +138,10 @@ def collect(self, rtn, horizon=None): owner_name = expr.owner.class_name idx_v = owners[owner_name]['idx'] header_v = key if expr.unit is None else f'{key} ({expr.unit})' - data_v = rtn.get(src=key, attr='v', idx=idx_v, horizon=horizon).round(DECIMALS) + try: + data_v = rtn.get(src=key, attr='v', idx=idx_v, horizon=horizon).round(DECIMALS) + except Exception: + data_v = [np.nan] * len(idx_v) owners[owner_name]['header'].append(header_v) owners[owner_name]['data'].append(data_v) @@ -146,7 +152,10 @@ def collect(self, rtn, horizon=None): owner_name = exprc.owner.class_name idx_v = owners[owner_name]['idx'] header_v = key if exprc.unit is None else f'{key} ({exprc.unit})' - data_v = rtn.get(src=key, attr='v', idx=idx_v, horizon=horizon).round(DECIMALS) + try: + data_v = rtn.get(src=key, attr='v', idx=idx_v, horizon=horizon).round(DECIMALS) + except Exception: + data_v = [np.nan] * len(idx_v) owners[owner_name]['header'].append(header_v) owners[owner_name]['data'].append(data_v) diff --git a/ams/routines/uc.py b/ams/routines/uc.py index 7cc3a149..70063ac4 100644 --- a/ams/routines/uc.py +++ b/ams/routines/uc.py @@ -195,10 +195,6 @@ def __init__(self, system, config): info='initial shutdown action', e_str='-ugd[:, 0] + ug[:, 0] - wgd[:, 0]',) - # NOTE: suspend pi.owner as it is empty in this routine - self.pi.model = None - self.pi.info = 'Place holder of LMP' - self.prs.horizon = self.timeslot self.prs.info = '2D Spinning reserve' From d8d28a1c0bd30322b1be50e1c814ba122f1da813 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 23 Nov 2024 23:35:23 -0500 Subject: [PATCH 08/33] Fix DCPF report error --- ams/report.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ams/report.py b/ams/report.py index 3f69053a..a60f3985 100644 --- a/ams/report.py +++ b/ams/report.py @@ -233,15 +233,15 @@ def write(self): else: qd = rtn.system.PQ.q0.v.sum().round(DECIMALS) - if rtn.type in ['ACED', 'PF']: + if not hasattr(rtn, 'qg'): + header.append(['P (p.u.)']) + Pcol = [rtn.pg.v.sum().round(DECIMALS), pd] + data.append([Pcol]) + else: header.append(['P (p.u.)', 'Q (p.u.)']) Pcol = [rtn.pg.v.sum().round(DECIMALS), pd] Qcol = [rtn.qg.v.sum().round(DECIMALS), qd] data.append([Pcol, Qcol]) - else: - header.append(['P (p.u.)']) - Pcol = [rtn.pg.v.sum().round(DECIMALS), pd] - data.append([Pcol]) # --- routine data --- text.extend(text_sum) From f38c2aa9eb283508772f274b8ec247b2dca3e2b4 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 24 Nov 2024 00:24:11 -0500 Subject: [PATCH 09/33] Add timeslot for pmaxe in routine EDDG --- ams/routines/ed.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ams/routines/ed.py b/ams/routines/ed.py index e7e5db40..3dcccda7 100644 --- a/ams/routines/ed.py +++ b/ams/routines/ed.py @@ -249,6 +249,8 @@ def __init__(self, system, config): # NOTE: extend vars to 2D self.pgdg.horizon = self.timeslot + self.pmaxe.horizon = self.timeslot + self.pmine.horizon = self.timeslot class ESD1MPBase(ESD1Base): From daaf11ceeba4a9f50fc623bd47f6b0b33f3e20c9 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 24 Nov 2024 00:24:39 -0500 Subject: [PATCH 10/33] Update case pjm5bus_demo.xlsx to include models ESD and DG --- ams/cases/5bus/pjm5bus_demo.xlsx | Bin 27546 -> 28087 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/ams/cases/5bus/pjm5bus_demo.xlsx b/ams/cases/5bus/pjm5bus_demo.xlsx index 66635679a0e7bda1c03a99483ba333abe3e3b816..e605f58d183da575f7cf6e7f35a0de99d06d9feb 100644 GIT binary patch delta 21432 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 Date: Sun, 24 Nov 2024 00:36:47 -0500 Subject: [PATCH 11/33] Typo --- ams/cases/5bus/pjm5bus_demo.xlsx | Bin 28087 -> 28150 bytes tests/test_rtn_rted.py | 16 ++++++++-------- tests/test_rtn_uc.py | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/ams/cases/5bus/pjm5bus_demo.xlsx b/ams/cases/5bus/pjm5bus_demo.xlsx index e605f58d183da575f7cf6e7f35a0de99d06d9feb..0c406687614c7aba69f33e84882b2996263de627 100644 GIT binary patch delta 4596 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 delta 4560 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 diff --git a/tests/test_rtn_rted.py b/tests/test_rtn_rted.py index 8dcd650d..32929b9c 100644 --- a/tests/test_rtn_rted.py +++ b/tests/test_rtn_rted.py @@ -61,23 +61,23 @@ def test_set_load(self): Test setting and tripping load. """ self.ss.RTED.run(solver='CLARABEL') - obj = self.ss.RTED.obj.v + pgs = self.ss.RTED.pg.v.sum() # --- set load --- self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0.1) self.ss.RTED.update() self.ss.RTED.run(solver='CLARABEL') - obj_pqt = self.ss.RTED.obj.v - self.assertLess(obj_pqt, obj, "Load set does not take effect!") + pgs_pqt = self.ss.RTED.pg.v.sum() + self.assertLess(pgs_pqt, pgs, "Load set does not take effect!") # --- trip load --- self.ss.PQ.set(src='u', attr='v', idx='PQ_2', value=0) self.ss.RTED.update() self.ss.RTED.run(solver='CLARABEL') - obj_pqt2 = self.ss.RTED.obj.v - self.assertLess(obj_pqt2, obj_pqt, "Load trip does not take effect!") + pgs_pqt2 = self.ss.RTED.pg.v.sum() + self.assertLess(pgs_pqt2, pgs_pqt, "Load trip does not take effect!") def test_dc2ac(self): """ @@ -157,14 +157,14 @@ def test_set_load(self): Test setting and tripping load. """ self.ss.RTEDES.run(solver='SCIP') - pgs = self.ss.RTEDES.obj.v.sum() + pgs = self.ss.RTEDES.pg.v.sum() # --- set load --- self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=1) self.ss.RTEDES.update() self.ss.RTEDES.run(solver='SCIP') - pgs_pqt = self.ss.RTEDES.obj.v.sum() + pgs_pqt = self.ss.RTEDES.pg.v.sum() self.assertLess(pgs_pqt, pgs, "Load set does not take effect!") # --- trip load --- @@ -172,5 +172,5 @@ def test_set_load(self): self.ss.RTEDES.update() self.ss.RTEDES.run(solver='SCIP') - pgs_pqt2 = self.ss.RTEDES.obj.v.sum() + pgs_pqt2 = self.ss.RTEDES.pg.v.sum() self.assertLess(pgs_pqt2, pgs_pqt, "Load trip does not take effect!") diff --git a/tests/test_rtn_uc.py b/tests/test_rtn_uc.py index df15a6b2..bbcfb45f 100644 --- a/tests/test_rtn_uc.py +++ b/tests/test_rtn_uc.py @@ -51,14 +51,14 @@ def test_set_load(self): Test setting and tripping load. """ self.ss.UC.run(solver='SCIP') - pgs = self.ss.UC.obj.v.sum() + pgs = self.ss.UC.pg.v.sum() # --- set load --- self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0.1) self.ss.UC.update() self.ss.UC.run(solver='SCIP') - pgs_pqt = self.ss.UC.obj.v.sum() + pgs_pqt = self.ss.UC.pg.v.sum() self.assertLess(pgs_pqt, pgs, "Load set does not take effect!") # --- trip load --- @@ -66,7 +66,7 @@ def test_set_load(self): self.ss.UC.update() self.ss.UC.run(solver='SCIP') - pgs_pqt2 = self.ss.UC.obj.v.sum() + pgs_pqt2 = self.ss.UC.pg.v.sum() self.assertLess(pgs_pqt2, pgs_pqt, "Load trip does not take effect!") @skip_unittest_without_MIP From fb17ce2acfedea1d3610be01483f563b488d7635 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 24 Nov 2024 07:52:35 -0500 Subject: [PATCH 12/33] Add test for routine DCPF --- tests/test_rtn_dcpf.py | 77 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 tests/test_rtn_dcpf.py diff --git a/tests/test_rtn_dcpf.py b/tests/test_rtn_dcpf.py new file mode 100644 index 00000000..9466fa77 --- /dev/null +++ b/tests/test_rtn_dcpf.py @@ -0,0 +1,77 @@ +import unittest + +import ams + + +class TestDCPF(unittest.TestCase): + """ + Test routine `DCPF`. + """ + + def setUp(self) -> None: + self.ss = ams.load(ams.get_case("5bus/pjm5bus_demo.xlsx"), + setup=True, default_config=True, no_output=True) + # decrease load first + self.ss.PQ.set(src='p0', attr='v', idx=['PQ_1', 'PQ_2'], value=[0.3, 0.3]) + + def test_init(self): + """ + Test initialization. + """ + self.ss.DCPF.init() + self.assertTrue(self.ss.DCPF.initialized, "DCPF initialization failed!") + + def test_trip_gen(self): + """ + Test generator tripping. + """ + stg = 'PV_1' + self.ss.StaticGen.set(src='u', idx=stg, attr='v', value=0) + + self.ss.DCPF.update() + self.ss.DCPF.run(solver='CLARABEL') + self.assertTrue(self.ss.DCPF.converged, "DCPF did not converge under generator trip!") + self.assertAlmostEqual(self.ss.DCPF.get(src='pg', attr='v', idx=stg), + 0, places=6, + msg="Generator trip does not take effect!") + + self.ss.StaticGen.set(src='u', idx=stg, attr='v', value=1) # reset + + def test_trip_line(self): + """ + Test line tripping. + """ + self.ss.Line.set(src='u', attr='v', idx='Line_3', value=0) + + self.ss.DCPF.update() + self.ss.DCPF.run(solver='CLARABEL') + self.assertTrue(self.ss.DCPF.converged, "DCPF did not converge under line trip!") + self.assertAlmostEqual(self.ss.DCPF.get(src='plf', attr='v', idx='Line_3'), + 0, places=6, + msg="Line trip does not take effect!") + + self.ss.Line.alter(src='u', idx='Line_3', value=1) # reset + + def test_set_load(self): + """ + Test setting and tripping load. + """ + # --- run DCPF --- + self.ss.DCPF.run(solver='CLARABEL') + pgs = self.ss.DCPF.pg.v.sum() + + # --- set load --- + self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0.1) + self.ss.DCPF.update() + + self.ss.DCPF.run(solver='CLARABEL') + pgs_pqt = self.ss.DCPF.pg.v.sum() + self.assertLess(pgs_pqt, pgs, "Load set does not take effect!") + + # --- trip load --- + self.ss.PQ.set(src='u', attr='v', idx='PQ_2', value=0) + self.ss.DCPF.update() + + self.ss.DCPF.run(solver='CLARABEL') + pgs_pqt2 = self.ss.DCPF.pg.v.sum() + self.assertLess(pgs_pqt2, pgs_pqt, "Load trip does not take effect!") From c92a6ab8a807e8be02bcd0be5b824c2de5c4d76a Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 24 Nov 2024 08:21:38 -0500 Subject: [PATCH 13/33] Add test for routine EDDG --- tests/test_rtn_ed.py | 107 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 99 insertions(+), 8 deletions(-) diff --git a/tests/test_rtn_ed.py b/tests/test_rtn_ed.py index cadefd3f..8f41ba24 100644 --- a/tests/test_rtn_ed.py +++ b/tests/test_rtn_ed.py @@ -95,6 +95,97 @@ def test_set_load(self): self.assertLess(pgs_pqt2, pgs_pqt, "Load trip does not take effect!") +class TestEDDG(unittest.TestCase): + """ + Test routine `EDDG`. + """ + + def setUp(self) -> None: + self.ss = ams.load(ams.get_case("5bus/pjm5bus_demo.xlsx"), + setup=True, default_config=True, no_output=True) + # decrease load first + self.ss.PQ.set(src='p0', attr='v', idx=['PQ_1', 'PQ_2'], value=[0.3, 0.3]) + + def test_init(self): + """ + Test initialization. + """ + self.ss.EDDG.init() + self.assertTrue(self.ss.EDDG.initialized, "EDDG initialization failed!") + + def test_trip_gen(self): + """ + Test generator tripping. + """ + # a) ensure EDTSlot.ug takes effect + # NOTE: manually chang ug.v for testing purpose + stg = 'PV_1' + stg_uid = self.ss.EDDG.pg.get_idx().index(stg) + loc_offtime = np.array([0, 2, 4]) + self.ss.EDTSlot.ug.v[loc_offtime, stg_uid] = 0 + + self.ss.EDDG.run(solver='CLARABEL') + self.assertTrue(self.ss.EDDG.converged, "ED did not converge under generator trip!") + pg_pv1 = self.ss.EDDG.get(src='pg', attr='v', idx=stg) + np.testing.assert_almost_equal(np.zeros_like(loc_offtime), + pg_pv1[loc_offtime], + decimal=6, + err_msg="Generator trip does not take effect!") + + self.ss.EDTSlot.ug.v[...] = 1 + + # b) ensure StaticGen.u does not take effect + # NOTE: in EDDG, `EDTSlot.ug` is used instead of `StaticGen.u` + self.ss.StaticGen.set(src='u', idx=stg, attr='v', value=0) + self.ss.EDDG.update() + + self.ss.EDDG.run(solver='CLARABEL') + self.assertTrue(self.ss.EDDG.converged, "ED did not converge under generator trip!") + pg_pv1 = self.ss.EDDG.get(src='pg', attr='v', idx=stg) + np.testing.assert_array_less(np.zeros_like(pg_pv1), pg_pv1, + err_msg="Generator trip take effect, which is unexpected!") + + self.ss.StaticGen.set(src='u', idx=stg, attr='v', value=1) # reset + + def test_trip_line(self): + """ + Test line tripping. + """ + self.ss.Line.set(src='u', attr='v', idx='Line_3', value=0) + self.ss.EDDG.update() + + self.ss.EDDG.run(solver='CLARABEL') + self.assertTrue(self.ss.EDDG.converged, "ED did not converge under line trip!") + plf_l3 = self.ss.EDDG.get(src='plf', attr='v', idx='Line_3') + np.testing.assert_almost_equal(np.zeros_like(plf_l3), + plf_l3, decimal=6) + + self.ss.Line.alter(src='u', idx='Line_3', value=1) + + def test_set_load(self): + """ + Test setting and tripping load. + """ + self.ss.EDDG.run(solver='CLARABEL') + pgs = self.ss.EDDG.pg.v.sum() + + # --- set load --- + self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0.1) + self.ss.EDDG.update() + + self.ss.EDDG.run(solver='CLARABEL') + pgs_pqt = self.ss.EDDG.pg.v.sum() + self.assertLess(pgs_pqt, pgs, "Load set does not take effect!") + + # --- trip load --- + self.ss.PQ.alter(src='u', idx='PQ_2', value=0) + self.ss.EDDG.update() + + self.ss.EDDG.run(solver='CLARABEL') + pgs_pqt2 = self.ss.EDDG.pg.v.sum() + self.assertLess(pgs_pqt2, pgs_pqt, "Load trip does not take effect!") + + class TestEDES(unittest.TestCase): """ Test routine `EDES`. @@ -120,13 +211,13 @@ def test_trip_gen(self): # a) ensure EDTSlot.ug takes effect # NOTE: manually chang ug.v for testing purpose stg = 'PV_1' - stg_uid = self.ss.ED.pg.get_idx().index(stg) + stg_uid = self.ss.EDES.pg.get_idx().index(stg) loc_offtime = np.array([0, 2]) self.ss.EDTSlot.ug.v[loc_offtime, stg_uid] = 0 - self.ss.ED.run(solver='CLARABEL') - self.assertTrue(self.ss.ED.converged, "ED did not converge under generator trip!") - pg_pv1 = self.ss.ED.get(src='pg', attr='v', idx=stg) + self.ss.EDES.run(solver='SCIP') + self.assertTrue(self.ss.EDES.converged, "ED did not converge under generator trip!") + pg_pv1 = self.ss.EDES.get(src='pg', attr='v', idx=stg) np.testing.assert_almost_equal(np.zeros_like(loc_offtime), pg_pv1[loc_offtime], decimal=6, @@ -137,11 +228,11 @@ def test_trip_gen(self): # b) ensure StaticGen.u does not take effect # NOTE: in ED, `EDTSlot.ug` is used instead of `StaticGen.u` self.ss.StaticGen.set(src='u', idx=stg, attr='v', value=0) - self.ss.ED.update() + self.ss.EDES.update() - self.ss.ED.run(solver='CLARABEL') - self.assertTrue(self.ss.ED.converged, "ED did not converge under generator trip!") - pg_pv1 = self.ss.ED.get(src='pg', attr='v', idx=stg) + self.ss.EDES.run(solver='SCIP') + self.assertTrue(self.ss.EDES.converged, "ED did not converge under generator trip!") + pg_pv1 = self.ss.EDES.get(src='pg', attr='v', idx=stg) np.testing.assert_array_less(np.zeros_like(pg_pv1), pg_pv1, err_msg="Generator trip take effect, which is unexpected!") From 8c2d3424f0cb4a990f1c23a68edaf93225b9c002 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 24 Nov 2024 08:27:09 -0500 Subject: [PATCH 14/33] Add test for routine RTEDDG --- tests/test_rtn_rted.py | 94 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/tests/test_rtn_rted.py b/tests/test_rtn_rted.py index 32929b9c..73f0407a 100644 --- a/tests/test_rtn_rted.py +++ b/tests/test_rtn_rted.py @@ -102,6 +102,100 @@ def test_dc2ac(self): np.testing.assert_almost_equal(a_rted, a_acopf, decimal=3) +class TestRTEDDG(unittest.TestCase): + """ + Test routine `RTEDDG`. + """ + + def setUp(self) -> None: + self.ss = ams.load(ams.get_case("5bus/pjm5bus_demo.xlsx"), + setup=True, default_config=True, no_output=True) + self.ss.RTEDDG.run(solver='CLARABEL') + + def test_init(self): + """ + Test initialization. + """ + self.ss.RTEDDG.init() + self.assertTrue(self.ss.RTEDDG.initialized, "RTEDDG initialization failed!") + + def test_trip_gen(self): + """ + Test generator tripping. + """ + stg = 'PV_1' + self.ss.StaticGen.set(src='u', idx=stg, attr='v', value=0) + + self.ss.RTEDDG.update() + self.ss.RTEDDG.run(solver='CLARABEL') + self.assertTrue(self.ss.RTEDDG.converged, "RTEDDG did not converge under generator trip!") + self.assertAlmostEqual(self.ss.RTEDDG.get(src='pg', attr='v', idx=stg), + 0, places=6, + msg="Generator trip does not take effect!") + + self.ss.StaticGen.set(src='u', idx=stg, attr='v', value=1) + + def test_trip_line(self): + """ + Test line tripping. + """ + self.ss.Line.set(src='u', attr='v', idx='Line_3', value=0) + + self.ss.RTEDDG.update() + self.ss.RTEDDG.run(solver='CLARABEL') + self.assertTrue(self.ss.RTEDDG.converged, "RTEDDG did not converge under line trip!") + self.assertAlmostEqual(self.ss.RTEDDG.get(src='plf', attr='v', idx='Line_3'), + 0, places=6, + msg="Line trip does not take effect!") + + self.ss.Line.alter(src='u', idx='Line_3', value=1) + + def test_set_load(self): + """ + Test setting and tripping load. + """ + self.ss.RTEDDG.run(solver='CLARABEL') + pgs = self.ss.RTEDDG.pg.v.sum() + + # --- set load --- + self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0.1) + self.ss.RTEDDG.update() + + self.ss.RTEDDG.run(solver='CLARABEL') + pgs_pqt = self.ss.RTEDDG.pg.v.sum() + self.assertLess(pgs_pqt, pgs, "Load set does not take effect!") + + # --- trip load --- + self.ss.PQ.set(src='u', attr='v', idx='PQ_2', value=0) + self.ss.RTEDDG.update() + + self.ss.RTEDDG.run(solver='CLARABEL') + pgs_pqt2 = self.ss.RTEDDG.pg.v.sum() + self.assertLess(pgs_pqt2, pgs_pqt, "Load trip does not take effect!") + + def test_dc2ac(self): + """ + Test `RTEDDG.dc2ac()` method. + """ + self.ss.RTEDDG.dc2ac() + self.assertTrue(self.ss.RTEDDG.converted, "AC conversion failed!") + self.assertTrue(self.ss.RTEDDG.exec_time > 0, "Execution time is not greater than 0.") + + stg_idx = self.ss.StaticGen.get_idx() + pg_rted = self.ss.RTEDDG.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_rted, pg_acopf, decimal=3) + + bus_idx = self.ss.Bus.get_idx() + v_rted = self.ss.RTEDDG.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_rted, v_acopf, decimal=3) + + a_rted = self.ss.RTEDDG.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_rted, a_acopf, decimal=3) + + class TestRTEDES(unittest.TestCase): """ Test routine `RTEDES`. From a214e2174cd3808f6a251c0a03bcc7a3e299645c Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 24 Nov 2024 08:47:18 -0500 Subject: [PATCH 15/33] Add test for routines RTEDDG UCDG --- tests/test_rtn_rted.py | 17 +++++---- tests/test_rtn_uc.py | 83 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 7 deletions(-) diff --git a/tests/test_rtn_rted.py b/tests/test_rtn_rted.py index 73f0407a..316e6430 100644 --- a/tests/test_rtn_rted.py +++ b/tests/test_rtn_rted.py @@ -12,11 +12,9 @@ class TestRTED(unittest.TestCase): def setUp(self) -> None: self.ss = ams.load(ams.get_case("5bus/pjm5bus_demo.xlsx"), - setup=True, - default_config=True, - no_output=True, - ) - self.ss.RTED.run(solver='CLARABEL') + setup=True, default_config=True, no_output=True) + # decrease load first + self.ss.PQ.set(src='p0', attr='v', idx=['PQ_1', 'PQ_2'], value=[0.3, 0.3]) def test_init(self): """ @@ -83,6 +81,7 @@ def test_dc2ac(self): """ Test `RTED.dc2ac()` method. """ + self.ss.RTED.run(solver='CLARABEL') self.ss.RTED.dc2ac() self.assertTrue(self.ss.RTED.converted, "AC conversion failed!") self.assertTrue(self.ss.RTED.exec_time > 0, "Execution time is not greater than 0.") @@ -110,7 +109,8 @@ class TestRTEDDG(unittest.TestCase): def setUp(self) -> None: self.ss = ams.load(ams.get_case("5bus/pjm5bus_demo.xlsx"), setup=True, default_config=True, no_output=True) - self.ss.RTEDDG.run(solver='CLARABEL') + # decrease load first + self.ss.PQ.set(src='p0', attr='v', idx=['PQ_1', 'PQ_2'], value=[0.3, 0.3]) def test_init(self): """ @@ -177,6 +177,7 @@ def test_dc2ac(self): """ Test `RTEDDG.dc2ac()` method. """ + self.ss.RTEDDG.run(solver='CLARABEL') self.ss.RTEDDG.dc2ac() self.assertTrue(self.ss.RTEDDG.converted, "AC conversion failed!") self.assertTrue(self.ss.RTEDDG.exec_time > 0, "Execution time is not greater than 0.") @@ -204,6 +205,8 @@ class TestRTEDES(unittest.TestCase): def setUp(self) -> None: self.ss = ams.load(ams.get_case("5bus/pjm5bus_uced_esd1.xlsx"), setup=True, default_config=True, no_output=True) + # decrease load first + self.ss.PQ.set(src='p0', attr='v', idx=['PQ_1', 'PQ_2'], value=[0.3, 0.3]) def test_init(self): """ @@ -254,7 +257,7 @@ def test_set_load(self): pgs = self.ss.RTEDES.pg.v.sum() # --- set load --- - self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=1) + self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0.05) self.ss.RTEDES.update() self.ss.RTEDES.run(solver='SCIP') diff --git a/tests/test_rtn_uc.py b/tests/test_rtn_uc.py index bbcfb45f..ab863808 100644 --- a/tests/test_rtn_uc.py +++ b/tests/test_rtn_uc.py @@ -86,6 +86,87 @@ def test_trip_line(self): self.ss.Line.alter(src='u', idx='Line_3', value=1) +class TestUCDG(unittest.TestCase): + """ + Test routine `UCDG`. + """ + + def setUp(self) -> None: + self.ss = ams.load(ams.get_case("5bus/pjm5bus_demo.xlsx"), + setup=True, default_config=True, no_output=True) + # decrease load first + self.ss.PQ.set(src='p0', attr='v', idx=['PQ_1', 'PQ_2'], value=[0.3, 0.3]) + # run `_initial_guess()` + self.off_gen = self.ss.UCDG._initial_guess() + + def test_initial_guess(self): + """ + Test initial guess. + """ + u_off_gen = self.ss.StaticGen.get(src='u', idx=self.off_gen) + np.testing.assert_equal(u_off_gen, np.zeros_like(u_off_gen), + err_msg="UCDG._initial_guess() failed!") + + def test_init(self): + """ + Test initialization. + """ + self.ss.UCDG.init() + self.assertTrue(self.ss.UCDG.initialized, "UCDG initialization failed!") + + @skip_unittest_without_MIP + def test_trip_gen(self): + """ + Test generator tripping. + """ + self.ss.UCDG.run(solver='SCIP') + self.assertTrue(self.ss.UCDG.converged, "UCDG did not converge!") + pg_off_gen = self.ss.UCDG.get(src='pg', attr='v', idx=self.off_gen) + np.testing.assert_almost_equal(np.zeros_like(pg_off_gen), + pg_off_gen, decimal=6, + err_msg="Off generators are not turned off!") + + @skip_unittest_without_MIP + def test_set_load(self): + """ + Test setting and tripping load. + """ + self.ss.UCDG.run(solver='SCIP') + pgs = self.ss.UCDG.pg.v.sum() + + # --- set load --- + self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0.1) + self.ss.UCDG.update() + + self.ss.UCDG.run(solver='SCIP') + pgs_pqt = self.ss.UCDG.pg.v.sum() + self.assertLess(pgs_pqt, pgs, "Load set does not take effect!") + + # --- trip load --- + self.ss.PQ.alter(src='u', idx='PQ_2', value=0) + self.ss.UCDG.update() + + self.ss.UCDG.run(solver='SCIP') + pgs_pqt2 = self.ss.UCDG.pg.v.sum() + self.assertLess(pgs_pqt2, pgs_pqt, "Load trip does not take effect!") + + @skip_unittest_without_MIP + def test_trip_line(self): + """ + Test line tripping. + """ + self.ss.Line.set(src='u', attr='v', idx='Line_3', value=0) + self.ss.UCDG.update() + + self.ss.UCDG.run(solver='SCIP') + self.assertTrue(self.ss.UCDG.converged, "UCDG did not converge under line trip!") + plf_l3 = self.ss.UCDG.get(src='plf', attr='v', idx='Line_3') + np.testing.assert_almost_equal(np.zeros_like(plf_l3), + plf_l3, decimal=6) + + self.ss.Line.alter(src='u', idx='Line_3', value=1) + + class TestUCES(unittest.TestCase): """ Test routine `UCES`. @@ -94,6 +175,8 @@ class TestUCES(unittest.TestCase): def setUp(self) -> None: self.ss = ams.load(ams.get_case("5bus/pjm5bus_uced_esd1.xlsx"), setup=True, default_config=True, no_output=True) + # decrease load first + self.ss.PQ.set(src='p0', attr='v', idx=['PQ_1', 'PQ_2'], value=[0.3, 0.3]) # run `_initial_guess()` self.off_gen = self.ss.UCES._initial_guess() From 85e83e53e062ba0b597bfce888144fd5c68de05f Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 24 Nov 2024 08:52:24 -0500 Subject: [PATCH 16/33] Update release notes --- docs/source/release-notes.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 3f6503df..a79b5ad7 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -9,6 +9,12 @@ 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) +-------------------- + +- Add a step to report in ``RoutineBase.run`` +- Add more tests to cover DG and ES related routines + v0.9.12 (2024-11-23) -------------------- From 16129eba94689b21c40bd83c741764572ad05e05 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 24 Nov 2024 09:22:51 -0500 Subject: [PATCH 17/33] Add a known limitations section in release notes --- docs/source/release-notes.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index a79b5ad7..20e10e74 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -320,6 +320,13 @@ v0.4 (2023-01) This release outlines the package. +Known Limitations +================= + +- For PYPOWER-based ACOPF, the largest converged case is "pglib_opf_case1354_pegase.m" +- There is no routine to handle the cases where both ``PVD1`` and ``ESD1`` exist +- Batch processing is not supported yet + Roadmap ======= From c57b26f5b34e891a9d7cbc37fe9c11e94c191b0b Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 24 Nov 2024 09:50:43 -0500 Subject: [PATCH 18/33] Include ExpressionCalc and Expression in routine exported CSV --- ams/routines/routine.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/ams/routines/routine.py b/ams/routines/routine.py index f77d014a..e749a646 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -461,7 +461,7 @@ def export_csv(self, path=None): file_name += f'_{self.class_name}' path = os.path.join(os.getcwd(), file_name + '.csv') - idxes = [var.get_idx() for var in self.vars.values()] + var_idxes = [var.get_idx() for var in self.vars.values()] var_names = [var for var in self.vars.keys()] if hasattr(self, 'timeslot'): @@ -471,11 +471,33 @@ def export_csv(self, path=None): timeslot = None data_dict = OrderedDict([('Time', 'T1')]) - for var, idx in zip(var_names, idxes): + for var, idx in zip(var_names, var_idxes): header = [f'{var} {dev}' for dev in idx] data = self.get(src=var, idx=idx, horizon=timeslot).round(6) data_dict.update(OrderedDict(zip(header, data))) + expr_idxes = [expr.get_idx() for expr in self.exprs.values()] + expr_names = [expr for expr in self.exprs.keys()] + + for expr, idx in zip(expr_names, expr_idxes): + header = [f'{expr} {dev}' for dev in idx] + try: + data = self.get(src=expr, attr='v', idx=idx).round(6) + except Exception: + data = [np.nan] * len(idx) + data_dict.update(OrderedDict(zip(header, data))) + + exprc_idxes = [exprc.get_idx() for exprc in self.exprcs.values()] + exprc_names = [exprc for exprc in self.exprcs.keys()] + + for exprc, idx in zip(exprc_names, exprc_idxes): + header = [f'{exprc} {dev}' for dev in idx] + try: + data = self.get(src=exprc, attr='v', idx=idx).round(6) + except Exception: + data = [np.nan] * len(idx) + data_dict.update(OrderedDict(zip(header, data))) + if timeslot is None: data_dict = OrderedDict([(k, [v]) for k, v in data_dict.items()]) From 831df0aa0a37f86bbda0bae1f6596439379262cb Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 24 Nov 2024 11:08:22 -0500 Subject: [PATCH 19/33] Fix horizon issue in routines ED RTED and UC --- ams/routines/ed.py | 2 -- ams/routines/rted.py | 33 ++++++++++++++++++--------------- ams/routines/uc.py | 1 + 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/ams/routines/ed.py b/ams/routines/ed.py index 3dcccda7..e7e5db40 100644 --- a/ams/routines/ed.py +++ b/ams/routines/ed.py @@ -249,8 +249,6 @@ def __init__(self, system, config): # NOTE: extend vars to 2D self.pgdg.horizon = self.timeslot - self.pmaxe.horizon = self.timeslot - self.pmine.horizon = self.timeslot class ESD1MPBase(ESD1Base): diff --git a/ams/routines/rted.py b/ams/routines/rted.py index 66aa1fe9..37601fb4 100644 --- a/ams/routines/rted.py +++ b/ams/routines/rted.py @@ -349,6 +349,7 @@ class ESD1Base(DGBase): def __init__(self): DGBase.__init__(self) + # --- params --- self.En = RParam(info='Rated energy capacity', name='En', src='En', @@ -374,14 +375,6 @@ def __init__(self): name='EtaD', src='EtaD', tex_name=r'\eta_d', unit='%', model='ESD1', no_parse=True,) - self.genesd = RParam(info='gen of ESD1', - name='genesd', tex_name=r'g_{ESD}', - model='ESD1', src='gen', - no_parse=True,) - info = 'Ratio of ESD1.pge w.r.t to that of static generator' - self.gammapesd = RParam(name='gammapesd', tex_name=r'\gamma_{p,ESD}', - model='ESD1', src='gammap', - no_parse=True, info=info) # --- service --- self.REtaD = NumOp(name='REtaD', tex_name=r'\frac{1}{\eta_d}', @@ -426,17 +419,27 @@ def __init__(self): self.zde.info = 'Aux var for discharging, ' self.zde.info += ':math:`z_{d,ESD}=u_{d,ESD}*p_{d,ESD}`' + # NOTE: to ensure consistency with DG based routiens, + # here we select ESD1 power from DG rather than StaticGen + self.genesd = RParam(info='gen of ESD1', + name='genesd', tex_name=r'g_{ESD}', + model='ESD1', src='idx', + no_parse=True,) + self.ces = VarSelect(u=self.pgdg, indexer='genesd', + name='ces', tex_name=r'C_{ESD}', + info='Select ESD power from DG', + no_parse=True) + self.cescb = Constraint(name='cescb', is_eq=True, + info='Select pce from DG', + e_str='ces @ pgdg - pce',) + self.cesdb = Constraint(name='cesdb', is_eq=True, + info='Select pde from DG', + e_str='ces @ pgdg - pde',) + # --- constraints --- self.cdb = Constraint(name='cdb', is_eq=True, info='Charging decision bound', e_str='uce + ude - 1',) - self.ces = VarSelect(u=self.pg, indexer='genesd', - name='ce', tex_name=r'C_{ESD}', - info='Select zue from pg', - gamma='gammapesd', no_parse=True,) - self.cesb = Constraint(name='cesb', is_eq=True, - info='Select ESD1 power from pg', - e_str='ces @ pg + zce - zde',) self.zce1 = Constraint(name='zce1', is_eq=False, info='zce bound 1', e_str='-zce + pce',) diff --git a/ams/routines/uc.py b/ams/routines/uc.py index 70063ac4..06b98938 100644 --- a/ams/routines/uc.py +++ b/ams/routines/uc.py @@ -366,6 +366,7 @@ def __init__(self, system, config): self.info = 'unit commitment with energy storage' self.type = 'DCUC' + self.pgdg.horizon = self.timeslot self.SOC.horizon = self.timeslot self.pce.horizon = self.timeslot self.pde.horizon = self.timeslot From 6ac4392690494f0b2958df81dee154aa80a9fb59 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 24 Nov 2024 11:27:11 -0500 Subject: [PATCH 20/33] Update release notes --- docs/source/release-notes.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 20e10e74..66067ca3 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -14,6 +14,7 @@ v0.9.13 (2024-xx-xx) - 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 v0.9.12 (2024-11-23) -------------------- @@ -323,8 +324,7 @@ This release outlines the package. Known Limitations ================= -- For PYPOWER-based ACOPF, the largest converged case is "pglib_opf_case1354_pegase.m" -- There is no routine to handle the cases where both ``PVD1`` and ``ESD1`` exist +- For builit-in PYPOWER-based ACOPF, the known largest solvable case is "pglib_opf_case1354_pegase.m" - Batch processing is not supported yet Roadmap From c551978472ecf8083bd8a1de09b01ce77dc61388 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 24 Nov 2024 11:36:22 -0500 Subject: [PATCH 21/33] Update known limitations --- docs/source/release-notes.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 66067ca3..9db44e7c 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -326,6 +326,8 @@ Known Limitations - For builit-in PYPOWER-based ACOPF, the known largest solvable case is "pglib_opf_case1354_pegase.m" - Batch processing is not supported yet +- Routine ``DCOPF`` has been extensively benchmarked with pandapower and MATPOWER. +- Routines besides above mentioned are not fully benchmarked yet. Roadmap ======= From 42ba79e66287178db26d11edc85a7e1d4292bccf Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 24 Nov 2024 11:54:53 -0500 Subject: [PATCH 22/33] [WIP] Refactor Report.collect, add three helper functions to collect Var, Expr and ExprCalc separately --- ams/report.py | 122 +++++++++++++++++++++++++++++--------------------- 1 file changed, 72 insertions(+), 50 deletions(-) diff --git a/ams/report.py b/ams/report.py index a60f3985..4a36a178 100644 --- a/ams/report.py +++ b/ams/report.py @@ -108,56 +108,9 @@ def collect(self, rtn, horizon=None): for name in owners_all if name in owners_e and getattr(system, name).n > 0 } - # --- owner data: idx and name --- - for key, val in owners.items(): - owner = getattr(system, key) - idx_v = owner.get_idx() - val['idx'] = idx_v - val['name'] = owner.get(src='name', attr='v', idx=idx_v) - val['header'].append('Name') - val['data'].append(val['name']) - - # --- variables data --- - for key, var in rtn.vars.items(): - if var.owner is None: - continue - owner_name = var.owner.class_name - idx_v = owners[owner_name]['idx'] - header_v = key if var.unit is None else f'{key} ({var.unit})' - try: - data_v = rtn.get(src=key, attr='v', idx=idx_v, horizon=horizon).round(DECIMALS) - except Exception: - data_v = [np.nan] * len(idx_v) - owners[owner_name]['header'].append(header_v) - owners[owner_name]['data'].append(data_v) - - # --- Expressions data --- - for key, expr in rtn.exprs.items(): - if expr.owner is None: - continue - owner_name = expr.owner.class_name - idx_v = owners[owner_name]['idx'] - header_v = key if expr.unit is None else f'{key} ({expr.unit})' - try: - data_v = rtn.get(src=key, attr='v', idx=idx_v, horizon=horizon).round(DECIMALS) - except Exception: - data_v = [np.nan] * len(idx_v) - owners[owner_name]['header'].append(header_v) - owners[owner_name]['data'].append(data_v) - - # --- ExpressionCalc data --- - for key, exprc in rtn.exprcs.items(): - if exprc.owner is None: - continue - owner_name = exprc.owner.class_name - idx_v = owners[owner_name]['idx'] - header_v = key if exprc.unit is None else f'{key} ({exprc.unit})' - try: - data_v = rtn.get(src=key, attr='v', idx=idx_v, horizon=horizon).round(DECIMALS) - except Exception: - data_v = [np.nan] * len(idx_v) - owners[owner_name]['header'].append(header_v) - owners[owner_name]['data'].append(data_v) + _collect_vars(system=system, owners=owners, rtn=rtn, horizon=horizon, DECIMALS=DECIMALS) + _collect_exprs(owners=owners, rtn=rtn, horizon=horizon, DECIMALS=DECIMALS) + _collect_exprcs(owners=owners, rtn=rtn, horizon=horizon, DECIMALS=DECIMALS) # --- dump data --- for key, val in owners.items(): @@ -252,3 +205,72 @@ def write(self): _, s = elapsed(t) logger.info(f'Report saved to "{system.files.txt}" in {s}.') + + +def _collect_exprcs(owners, rtn, horizon, DECIMALS): + """ + Collect expression calculations and populate the data dictionary. + """ + for key, exprc in rtn.exprcs.items(): + if exprc.owner is None: + continue + owner_name = exprc.owner.class_name + idx_v = owners[owner_name]['idx'] + header_v = key if exprc.unit is None else f'{key} ({exprc.unit})' + try: + data_v = rtn.get(src=key, attr='v', idx=idx_v, horizon=horizon).round(DECIMALS) + except Exception: + data_v = [np.nan] * len(idx_v) + owners[owner_name]['header'].append(header_v) + owners[owner_name]['data'].append(data_v) + + return owners + + +def _collect_exprs(owners, rtn, horizon, DECIMALS): + """ + Collect expressions and populate the data dictionary. + """ + for key, expr in rtn.exprs.items(): + if expr.owner is None: + continue + owner_name = expr.owner.class_name + idx_v = owners[owner_name]['idx'] + header_v = key if expr.unit is None else f'{key} ({expr.unit})' + try: + data_v = rtn.get(src=key, attr='v', idx=idx_v, horizon=horizon).round(DECIMALS) + except Exception: + data_v = [np.nan] * len(idx_v) + owners[owner_name]['header'].append(header_v) + owners[owner_name]['data'].append(data_v) + + return owners + + +def _collect_vars(system, owners, rtn, horizon, DECIMALS): + """ + Collect variables and populate the data dictionary. + """ + # --- owner data: idx and name --- + for key, val in owners.items(): + owner = getattr(system, key) + idx_v = owner.get_idx() + val['idx'] = idx_v + val['name'] = owner.get(src='name', attr='v', idx=idx_v) + val['header'].append('Name') + val['data'].append(val['name']) + + for key, var in rtn.vars.items(): + if var.owner is None: + continue + owner_name = var.owner.class_name + idx_v = owners[owner_name]['idx'] + header_v = key if var.unit is None else f'{key} ({var.unit})' + try: + data_v = rtn.get(src=key, attr='v', idx=idx_v, horizon=horizon).round(DECIMALS) + except Exception: + data_v = [np.nan] * len(idx_v) + owners[owner_name]['header'].append(header_v) + owners[owner_name]['data'].append(data_v) + + return owners From 4ae92299fdaccac1fc1fdf9918296dc13d6fb00a Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 24 Nov 2024 13:59:27 -0500 Subject: [PATCH 23/33] [WIP] Refactor Report.collect, minor fix on _collect_vars --- ams/report.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ams/report.py b/ams/report.py index 4a36a178..b4656f27 100644 --- a/ams/report.py +++ b/ams/report.py @@ -108,7 +108,7 @@ def collect(self, rtn, horizon=None): for name in owners_all if name in owners_e and getattr(system, name).n > 0 } - _collect_vars(system=system, owners=owners, rtn=rtn, horizon=horizon, DECIMALS=DECIMALS) + _collect_vars(owners=owners, rtn=rtn, horizon=horizon, DECIMALS=DECIMALS) _collect_exprs(owners=owners, rtn=rtn, horizon=horizon, DECIMALS=DECIMALS) _collect_exprcs(owners=owners, rtn=rtn, horizon=horizon, DECIMALS=DECIMALS) @@ -247,13 +247,12 @@ def _collect_exprs(owners, rtn, horizon, DECIMALS): return owners -def _collect_vars(system, owners, rtn, horizon, DECIMALS): +def _collect_vars(owners, rtn, horizon, DECIMALS): """ Collect variables and populate the data dictionary. """ - # --- owner data: idx and name --- for key, val in owners.items(): - owner = getattr(system, key) + owner = getattr(rtn.system, key) idx_v = owner.get_idx() val['idx'] = idx_v val['name'] = owner.get(src='name', attr='v', idx=idx_v) From 1ca465776e235e339c56088d748c4c446c74634d Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 24 Nov 2024 14:13:30 -0500 Subject: [PATCH 24/33] [WIP] Refactor Report.collect, add a func _collect_owners --- ams/report.py | 73 ++++++++++++++++++++++++++++----------------------- 1 file changed, 40 insertions(+), 33 deletions(-) diff --git a/ams/report.py b/ams/report.py index b4656f27..c12b9288 100644 --- a/ams/report.py +++ b/ams/report.py @@ -74,8 +74,6 @@ def collect(self, rtn, horizon=None): horizon : str, optional Timeslot to collect data from. Only single timeslot is supported. """ - system = self.system - text = list() header = list() row_name = list() @@ -84,30 +82,7 @@ def collect(self, rtn, horizon=None): if not rtn.converged: return text, header, row_name, data - # initialize data section by model - owners_all = ['Bus', 'Line', 'StaticGen', - 'PV', 'Slack', 'RenGen', - 'DG', 'ESD1', 'PVD1', - 'StaticLoad'] - - # Filter owners that exist in the system - owners_e = list({ - var.owner.class_name for var in rtn.vars.values() if var.owner is not None - }.union( - expr.owner.class_name for expr in rtn.exprs.values() if expr.owner is not None - ).union( - exprc.owner.class_name for exprc in rtn.exprcs.values() if exprc.owner is not None - )) - - # Use a dictionary comprehension to create vars_by_owner - owners = { - name: {'idx': [], - 'name': [], - 'header': [], - 'data': [], } - for name in owners_all if name in owners_e and getattr(system, name).n > 0 - } - + owners = _collect_owners(rtn=rtn) _collect_vars(owners=owners, rtn=rtn, horizon=horizon, DECIMALS=DECIMALS) _collect_exprs(owners=owners, rtn=rtn, horizon=horizon, DECIMALS=DECIMALS) _collect_exprcs(owners=owners, rtn=rtn, horizon=horizon, DECIMALS=DECIMALS) @@ -251,13 +226,6 @@ def _collect_vars(owners, rtn, horizon, DECIMALS): """ Collect variables and populate the data dictionary. """ - for key, val in owners.items(): - owner = getattr(rtn.system, key) - idx_v = owner.get_idx() - val['idx'] = idx_v - val['name'] = owner.get(src='name', attr='v', idx=idx_v) - val['header'].append('Name') - val['data'].append(val['name']) for key, var in rtn.vars.items(): if var.owner is None: @@ -273,3 +241,42 @@ def _collect_vars(owners, rtn, horizon, DECIMALS): owners[owner_name]['data'].append(data_v) return owners + + +def _collect_owners(rtn): + """ + Initialize an owners dictionary for data collection. + """ + # initialize data section by model + owners_all = ['Bus', 'Line', 'StaticGen', + 'PV', 'Slack', 'RenGen', + 'DG', 'ESD1', 'PVD1', + 'StaticLoad'] + + # Filter owners that exist in the system + owners_e = list({ + var.owner.class_name for var in rtn.vars.values() if var.owner is not None + }.union( + expr.owner.class_name for expr in rtn.exprs.values() if expr.owner is not None + ).union( + exprc.owner.class_name for exprc in rtn.exprcs.values() if exprc.owner is not None + )) + + # Use a dictionary comprehension to create vars_by_owner + owners = { + name: {'idx': [], + 'name': [], + 'header': [], + 'data': [], } + for name in owners_all if name in owners_e and getattr(rtn.system, name).n > 0 + } + + for key, val in owners.items(): + owner = getattr(rtn.system, key) + idx_v = owner.get_idx() + val['idx'] = idx_v + val['name'] = owner.get(src='name', attr='v', idx=idx_v) + val['header'].append('Name') + val['data'].append(val['name']) + + return owners From 8f3ddd4286fa06c6542cc502d82249c5c7cd8593 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 24 Nov 2024 14:17:31 -0500 Subject: [PATCH 25/33] [WIP] Refactor Report.collect, add a func dump_collected_data --- ams/report.py | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/ams/report.py b/ams/report.py index c12b9288..9051e6ad 100644 --- a/ams/report.py +++ b/ams/report.py @@ -4,6 +4,7 @@ import logging from collections import OrderedDict from time import strftime +from typing import List from andes.io.txt import dump_data from andes.shared import np @@ -86,13 +87,8 @@ def collect(self, rtn, horizon=None): _collect_vars(owners=owners, rtn=rtn, horizon=horizon, DECIMALS=DECIMALS) _collect_exprs(owners=owners, rtn=rtn, horizon=horizon, DECIMALS=DECIMALS) _collect_exprcs(owners=owners, rtn=rtn, horizon=horizon, DECIMALS=DECIMALS) + dump_collected_data(owners, text, header, row_name, data) - # --- dump data --- - for key, val in owners.items(): - text.append([f'{key} DATA:\n']) - row_name.append(val['idx']) - header.append(val['header']) - data.append(val['data']) return text, header, row_name, data def write(self): @@ -182,6 +178,30 @@ def write(self): logger.info(f'Report saved to "{system.files.txt}" in {s}.') +def dump_collected_data(owners: dict, text: List, header: List, row_name: List, data: List) -> None: + """ + Dump collected data into the provided lists. + + Parameters + ---------- + owners : dict + Dictionary of owners. + text : list + List to append text data to. + header : list + List to append header data to. + row_name : list + List to append row names to. + data : list + List to append data to. + """ + for key, val in owners.items(): + text.append([f'{key} DATA:\n']) + row_name.append(val['idx']) + header.append(val['header']) + data.append(val['data']) + + def _collect_exprcs(owners, rtn, horizon, DECIMALS): """ Collect expression calculations and populate the data dictionary. From 046dc3d613c76a2249a1af46d2245cd41fb71c72 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 24 Nov 2024 14:25:56 -0500 Subject: [PATCH 26/33] [WIP] Refactor Report.collect, enhancement --- ams/report.py | 62 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/ams/report.py b/ams/report.py index 9051e6ad..8f5bdcb2 100644 --- a/ams/report.py +++ b/ams/report.py @@ -4,7 +4,7 @@ import logging from collections import OrderedDict from time import strftime -from typing import List +from typing import List, Dict, Optional from andes.io.txt import dump_data from andes.shared import np @@ -83,10 +83,10 @@ def collect(self, rtn, horizon=None): if not rtn.converged: return text, header, row_name, data - owners = _collect_owners(rtn=rtn) - _collect_vars(owners=owners, rtn=rtn, horizon=horizon, DECIMALS=DECIMALS) - _collect_exprs(owners=owners, rtn=rtn, horizon=horizon, DECIMALS=DECIMALS) - _collect_exprcs(owners=owners, rtn=rtn, horizon=horizon, DECIMALS=DECIMALS) + owners = collect_owners(rtn=rtn) + collect_vars(owners=owners, rtn=rtn, horizon=horizon, decimals=DECIMALS) + collect_exprs(owners=owners, rtn=rtn, horizon=horizon, decimals=DECIMALS) + collect_exprcs(owners=owners, rtn=rtn, horizon=horizon, decimals=DECIMALS) dump_collected_data(owners, text, header, row_name, data) return text, header, row_name, data @@ -202,7 +202,7 @@ def dump_collected_data(owners: dict, text: List, header: List, row_name: List, data.append(val['data']) -def _collect_exprcs(owners, rtn, horizon, DECIMALS): +def collect_exprcs(owners, rtn, horizon, decimals): """ Collect expression calculations and populate the data dictionary. """ @@ -213,18 +213,32 @@ def _collect_exprcs(owners, rtn, horizon, DECIMALS): idx_v = owners[owner_name]['idx'] header_v = key if exprc.unit is None else f'{key} ({exprc.unit})' try: - data_v = rtn.get(src=key, attr='v', idx=idx_v, horizon=horizon).round(DECIMALS) + data_v = rtn.get(src=key, attr='v', idx=idx_v, horizon=horizon).round(decimals) except Exception: data_v = [np.nan] * len(idx_v) owners[owner_name]['header'].append(header_v) owners[owner_name]['data'].append(data_v) - return owners - -def _collect_exprs(owners, rtn, horizon, DECIMALS): +def collect_exprs(owners: Dict, rtn, horizon: Optional[str], decimals: int) -> Dict: """ Collect expressions and populate the data dictionary. + + Parameters + ---------- + owners : dict + Dictionary of owners. + rtn : Routine + Routine object to collect data from. + horizon : str, optional + Timeslot to collect data from. Only single timeslot is supported. + decimals : int + Number of decimal places to round the data. + + Returns + ------- + dict + Updated dictionary of owners with collected expression data. """ for key, expr in rtn.exprs.items(): if expr.owner is None: @@ -233,18 +247,32 @@ def _collect_exprs(owners, rtn, horizon, DECIMALS): idx_v = owners[owner_name]['idx'] header_v = key if expr.unit is None else f'{key} ({expr.unit})' try: - data_v = rtn.get(src=key, attr='v', idx=idx_v, horizon=horizon).round(DECIMALS) + data_v = rtn.get(src=key, attr='v', idx=idx_v, horizon=horizon).round(decimals) except Exception: data_v = [np.nan] * len(idx_v) owners[owner_name]['header'].append(header_v) owners[owner_name]['data'].append(data_v) - return owners - -def _collect_vars(owners, rtn, horizon, DECIMALS): +def collect_vars(owners: Dict, rtn, horizon: Optional[str], decimals: int) -> Dict: """ Collect variables and populate the data dictionary. + + Parameters + ---------- + owners : dict + Dictionary of owners. + rtn : Routine + Routine object to collect data from. + horizon : str, optional + Timeslot to collect data from. Only single timeslot is supported. + decimals : int + Number of decimal places to round the data. + + Returns + ------- + dict + Updated dictionary of owners with collected variable data. """ for key, var in rtn.vars.items(): @@ -254,16 +282,14 @@ def _collect_vars(owners, rtn, horizon, DECIMALS): idx_v = owners[owner_name]['idx'] header_v = key if var.unit is None else f'{key} ({var.unit})' try: - data_v = rtn.get(src=key, attr='v', idx=idx_v, horizon=horizon).round(DECIMALS) + data_v = rtn.get(src=key, attr='v', idx=idx_v, horizon=horizon).round(decimals) except Exception: data_v = [np.nan] * len(idx_v) owners[owner_name]['header'].append(header_v) owners[owner_name]['data'].append(data_v) - return owners - -def _collect_owners(rtn): +def collect_owners(rtn): """ Initialize an owners dictionary for data collection. """ From 3f6e9e8a5f727fa44fac00b084a73a04a5cf102c Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 24 Nov 2024 14:30:47 -0500 Subject: [PATCH 27/33] [WIP] Refactor Report.collect, enhancement --- ams/report.py | 43 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/ams/report.py b/ams/report.py index 8f5bdcb2..42fe0a84 100644 --- a/ams/report.py +++ b/ams/report.py @@ -83,10 +83,11 @@ def collect(self, rtn, horizon=None): if not rtn.converged: return text, header, row_name, data - owners = collect_owners(rtn=rtn) - collect_vars(owners=owners, rtn=rtn, horizon=horizon, decimals=DECIMALS) - collect_exprs(owners=owners, rtn=rtn, horizon=horizon, decimals=DECIMALS) - collect_exprcs(owners=owners, rtn=rtn, horizon=horizon, decimals=DECIMALS) + owners = collect_owners(rtn) + owners = collect_vars(owners, rtn, horizon, DECIMALS) + owners = collect_exprs(owners, rtn, horizon, DECIMALS) + owners = collect_exprcs(owners, rtn, horizon, DECIMALS) + dump_collected_data(owners, text, header, row_name, data) return text, header, row_name, data @@ -202,9 +203,26 @@ def dump_collected_data(owners: dict, text: List, header: List, row_name: List, data.append(val['data']) -def collect_exprcs(owners, rtn, horizon, decimals): + +def collect_exprcs(owners: Dict, rtn, horizon: Optional[str], decimals: int) -> Dict: """ Collect expression calculations and populate the data dictionary. + + Parameters + ---------- + owners : dict + Dictionary of owners. + rtn : Routine + Routine object to collect data from. + horizon : str, optional + Timeslot to collect data from. Only single timeslot is supported. + decimals : int + Number of decimal places to round the data. + + Returns + ------- + dict + Updated dictionary of owners with collected ExpressionCalc data. """ for key, exprc in rtn.exprcs.items(): if exprc.owner is None: @@ -219,6 +237,8 @@ def collect_exprcs(owners, rtn, horizon, decimals): owners[owner_name]['header'].append(header_v) owners[owner_name]['data'].append(data_v) + return owners + def collect_exprs(owners: Dict, rtn, horizon: Optional[str], decimals: int) -> Dict: """ @@ -238,7 +258,7 @@ def collect_exprs(owners: Dict, rtn, horizon: Optional[str], decimals: int) -> D Returns ------- dict - Updated dictionary of owners with collected expression data. + Updated dictionary of owners with collected Expression data. """ for key, expr in rtn.exprs.items(): if expr.owner is None: @@ -253,6 +273,8 @@ def collect_exprs(owners: Dict, rtn, horizon: Optional[str], decimals: int) -> D owners[owner_name]['header'].append(header_v) owners[owner_name]['data'].append(data_v) + return owners + def collect_vars(owners: Dict, rtn, horizon: Optional[str], decimals: int) -> Dict: """ @@ -272,7 +294,7 @@ def collect_vars(owners: Dict, rtn, horizon: Optional[str], decimals: int) -> Di Returns ------- dict - Updated dictionary of owners with collected variable data. + Updated dictionary of owners with collected Var data. """ for key, var in rtn.vars.items(): @@ -288,10 +310,17 @@ def collect_vars(owners: Dict, rtn, horizon: Optional[str], decimals: int) -> Di owners[owner_name]['header'].append(header_v) owners[owner_name]['data'].append(data_v) + return owners + def collect_owners(rtn): """ Initialize an owners dictionary for data collection. + + Returns + ------- + dict + A dictionary of initialized owners. """ # initialize data section by model owners_all = ['Bus', 'Line', 'StaticGen', From b45b6e15ac8794cfd3383eb85acb83bf447d510a Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 24 Nov 2024 14:43:56 -0500 Subject: [PATCH 28/33] Format --- ams/report.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ams/report.py b/ams/report.py index 42fe0a84..b0f7adb0 100644 --- a/ams/report.py +++ b/ams/report.py @@ -203,7 +203,6 @@ def dump_collected_data(owners: dict, text: List, header: List, row_name: List, data.append(val['data']) - def collect_exprcs(owners: Dict, rtn, horizon: Optional[str], decimals: int) -> Dict: """ Collect expression calculations and populate the data dictionary. From b1c751956b68a6d6a85a40465ae2d9c64d66a5e0 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 24 Nov 2024 14:44:25 -0500 Subject: [PATCH 29/33] Refactor RoutineBase.export_csv --- ams/routines/routine.py | 135 ++++++++++++++++++++++++++-------------- 1 file changed, 88 insertions(+), 47 deletions(-) diff --git a/ams/routines/routine.py b/ams/routines/routine.py index e749a646..b1a8fced 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -4,7 +4,7 @@ import logging import os -from typing import Optional, Union, Type, Iterable +from typing import Optional, Union, Type, Iterable, Dict from collections import OrderedDict import numpy as np @@ -446,63 +446,26 @@ def export_csv(self, path=None): Returns ------- - str + export_path The path of the exported csv file """ if not self.converged: logger.warning("Routine did not converge, aborting export.") return None - if not path: - if self.system.files.fullname is None: - logger.info("Input file name not detacted. Using `Untitled`.") - file_name = f'Untitled_{self.class_name}' - else: - file_name = os.path.splitext(self.system.files.fullname)[0] - file_name += f'_{self.class_name}' - path = os.path.join(os.getcwd(), file_name + '.csv') - - var_idxes = [var.get_idx() for var in self.vars.values()] - var_names = [var for var in self.vars.keys()] - - if hasattr(self, 'timeslot'): - timeslot = self.timeslot.v.copy() - data_dict = OrderedDict([('Time', timeslot)]) - else: - timeslot = None - data_dict = OrderedDict([('Time', 'T1')]) - - for var, idx in zip(var_names, var_idxes): - header = [f'{var} {dev}' for dev in idx] - data = self.get(src=var, idx=idx, horizon=timeslot).round(6) - data_dict.update(OrderedDict(zip(header, data))) - - expr_idxes = [expr.get_idx() for expr in self.exprs.values()] - expr_names = [expr for expr in self.exprs.keys()] - - for expr, idx in zip(expr_names, expr_idxes): - header = [f'{expr} {dev}' for dev in idx] - try: - data = self.get(src=expr, attr='v', idx=idx).round(6) - except Exception: - data = [np.nan] * len(idx) - data_dict.update(OrderedDict(zip(header, data))) - exprc_idxes = [exprc.get_idx() for exprc in self.exprcs.values()] - exprc_names = [exprc for exprc in self.exprcs.keys()] + export_path = get_export_path(self, path) + data_dict = initialize_data_dict(self) - for exprc, idx in zip(exprc_names, exprc_idxes): - header = [f'{exprc} {dev}' for dev in idx] - try: - data = self.get(src=exprc, attr='v', idx=idx).round(6) - except Exception: - data = [np.nan] * len(idx) - data_dict.update(OrderedDict(zip(header, data))) + collect_data(self, data_dict, self.vars, 'v') + collect_data(self, data_dict, self.exprs, 'v') + collect_data(self, data_dict, self.exprcs, 'v') - if timeslot is None: + if 'T1' in data_dict['Time']: data_dict = OrderedDict([(k, [v]) for k, v in data_dict.items()]) pd.DataFrame(data_dict).to_csv(path, index=False) - return file_name + '.csv' + + return export_path def summary(self, **kwargs): """ @@ -1011,3 +974,81 @@ def _initial_guess(self): Generate initial guess for the optimization model. """ raise NotImplementedError + + +def get_export_path(rtn: RoutineBase, path: Optional[str]): + """ + Get the export path for the csv file. + + Parameters + ---------- + rtn : ams.routines.routine.RoutineBase + The routine to export. + path : str + Path of the csv file to save. + + Returns + ------- + str + The path of the exported csv file. + """ + if path: + return path + + if rtn.system.files.fullname is None: + logger.info("Input file name not detected. Using `Untitled`.") + file_name = f'Untitled_{rtn.class_name}' + else: + file_name = os.path.splitext(rtn.system.files.fullname)[0] + file_name += f'_{rtn.class_name}' + + return os.path.join(os.getcwd(), file_name + '.csv') + + +def initialize_data_dict(rtn: RoutineBase): + """ + Initialize the data dictionary for export. + + Parameters + ---------- + rtn : ams.routines.routine.RoutineBase + The routine to collect data from + + Returns + ------- + OrderedDict + The initialized data dictionary. + """ + if hasattr(rtn, 'timeslot'): + timeslot = rtn.timeslot.v.copy() + return OrderedDict([('Time', timeslot)]) + else: + return OrderedDict([('Time', 'T1')]) + + +def collect_data(rtn: RoutineBase, data_dict: Dict, items: Dict, attr: str): + """ + Collect data for export. + + Parameters + ---------- + rtn : ams.routines.routine.RoutineBase + The routine to collect data from. + data_dict : OrderedDict + The data dictionary to populate. + items : dict + Dictionary of items to collect data from. + attr : str + Attribute to collect data for. + """ + for key, item in items.items(): + if item.owner is None: + continue + idx_v = item.get_idx() + try: + data_v = rtn.get(src=key, attr=attr, idx=idx_v, + horizon=rtn.timeslot.v if hasattr(rtn, 'timeslot') else None).round(6) + except Exception as e: + logger.error(f"Error collecting data for '{key}': {e}") + data_v = [np.nan] * len(idx_v) + data_dict.update(OrderedDict(zip([f'{key} {dev}' for dev in idx_v], data_v))) From f0f4dc576cdc3d7c5d41282ef745f7ae64822b48 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 24 Nov 2024 15:16:30 -0500 Subject: [PATCH 30/33] Add more tests for module report --- tests/test_report.py | 179 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 174 insertions(+), 5 deletions(-) diff --git a/tests/test_report.py b/tests/test_report.py index f62cbb2d..404b8bf4 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -7,6 +7,11 @@ import ams +import logging + +logger = logging.getLogger(__name__) + + class TestReport(unittest.TestCase): """ Tests for Report class. @@ -53,24 +58,188 @@ def test_DCOPF_report(self): with open(self.expected_report, "r") as report_file: file_contents = report_file.read() self.assertIn("DCOPF", file_contents) + os.remove(self.expected_report) + + self.ss.DCOPF.export_csv('./DCOPF.csv') + self.assertTrue(os.path.exists('./DCOPF.csv')) + os.remove('./DCOPF.csv') + + def test_DCPF_report(self): + """ + Test report with DCPF solved. + """ + self.ss.files.no_output = False + self.ss.DCPF.run(solver='CLARABEL') + self.assertTrue(self.ss.report()) + self.assertTrue(os.path.exists(self.expected_report)) + with open(self.expected_report, "r") as report_file: + file_contents = report_file.read() + self.assertIn("DCPF", file_contents) os.remove(self.expected_report) - def test_multi_report(self): + self.ss.DCPF.export_csv('./DCPF.csv') + self.assertTrue(os.path.exists('./DCPF.csv')) + os.remove('./DCPF.csv') + + def test_RTED_report(self): """ - Test report with multiple solved routines. + Test report with RTED solved. """ self.ss.files.no_output = False - self.ss.DCOPF.run(solver='CLARABEL') self.ss.RTED.run(solver='CLARABEL') - self.ss.ED.run(solver='CLARABEL') self.assertTrue(self.ss.report()) self.assertTrue(os.path.exists(self.expected_report)) with open(self.expected_report, "r") as report_file: file_contents = report_file.read() - self.assertIn("DCOPF", file_contents) self.assertIn("RTED", file_contents) + os.remove(self.expected_report) + + self.ss.RTED.export_csv('./RTED.csv') + self.assertTrue(os.path.exists('./RTED.csv')) + os.remove('./RTED.csv') + + def test_RTEDDG_report(self): + """ + Test report with RTEDDG solved. + """ + self.ss.files.no_output = False + self.ss.RTEDDG.run(solver='CLARABEL') + self.assertTrue(self.ss.report()) + self.assertTrue(os.path.exists(self.expected_report)) + + with open(self.expected_report, "r") as report_file: + file_contents = report_file.read() + self.assertIn("RTEDDG", file_contents) + os.remove(self.expected_report) + + self.ss.RTEDDG.export_csv('./RTEDDG.csv') + self.assertTrue(os.path.exists('./RTEDDG.csv')) + os.remove('./RTEDDG.csv') + + def test_RTEDES_report(self): + """ + Test report with RTEDES solved. + """ + self.ss.files.no_output = False + self.ss.RTEDES.run(solver='SCIP') + self.assertTrue(self.ss.report()) + self.assertTrue(os.path.exists(self.expected_report)) + + with open(self.expected_report, "r") as report_file: + file_contents = report_file.read() + self.assertIn("RTEDES", file_contents) + os.remove(self.expected_report) + + self.ss.RTEDES.export_csv('./RTEDES.csv') + self.assertTrue(os.path.exists('./RTEDES.csv')) + os.remove('./RTEDES.csv') + + def test_ED_report(self): + """ + Test report with ED solved. + """ + self.ss.files.no_output = False + self.ss.ED.run(solver='CLARABEL') + self.assertTrue(self.ss.report()) + self.assertTrue(os.path.exists(self.expected_report)) + + with open(self.expected_report, "r") as report_file: + file_contents = report_file.read() self.assertIn("ED", file_contents) + os.remove(self.expected_report) + + self.ss.ED.export_csv('./ED.csv') + self.assertTrue(os.path.exists('./ED.csv')) + os.remove('./ED.csv') + def test_EDDG_report(self): + """ + Test report with EDDG solved. + """ + self.ss.files.no_output = False + self.ss.EDDG.run(solver='CLARABEL') + self.assertTrue(self.ss.report()) + self.assertTrue(os.path.exists(self.expected_report)) + + with open(self.expected_report, "r") as report_file: + file_contents = report_file.read() + self.assertIn("EDDG", file_contents) os.remove(self.expected_report) + + self.ss.EDDG.export_csv('./EDDG.csv') + self.assertTrue(os.path.exists('./EDDG.csv')) + os.remove('./EDDG.csv') + + def test_EDES_report(self): + """ + Test report with EDES solved. + """ + self.ss.files.no_output = False + self.ss.EDES.run(solver='SCIP') + self.assertTrue(self.ss.report()) + self.assertTrue(os.path.exists(self.expected_report)) + + with open(self.expected_report, "r") as report_file: + file_contents = report_file.read() + self.assertIn("EDES", file_contents) + os.remove(self.expected_report) + + self.ss.EDES.export_csv('./EDES.csv') + self.assertTrue(os.path.exists('./EDES.csv')) + os.remove('./EDES.csv') + + def test_UC_report(self): + """ + Test report with UC solved. + """ + self.ss.files.no_output = False + self.ss.UC.run(solver='SCIP') + self.assertTrue(self.ss.report()) + self.assertTrue(os.path.exists(self.expected_report)) + + with open(self.expected_report, "r") as report_file: + file_contents = report_file.read() + self.assertIn("UC", file_contents) + os.remove(self.expected_report) + + self.ss.UC.export_csv('./UC.csv') + self.assertTrue(os.path.exists('./UC.csv')) + os.remove('./UC.csv') + + def test_UCDG_report(self): + """ + Test report with UCDG solved. + """ + self.ss.files.no_output = False + self.ss.UCDG.run(solver='SCIP') + self.assertTrue(self.ss.report()) + self.assertTrue(os.path.exists(self.expected_report)) + + with open(self.expected_report, "r") as report_file: + file_contents = report_file.read() + self.assertIn("UCDG", file_contents) + os.remove(self.expected_report) + + self.ss.UCDG.export_csv('./UCDG.csv') + self.assertTrue(os.path.exists('./UCDG.csv')) + os.remove('./UCDG.csv') + + def test_UCES_report(self): + """ + Test report with UCES solved. + """ + self.ss.files.no_output = False + self.ss.UCES.run(solver='SCIP') + self.assertTrue(self.ss.report()) + self.assertTrue(os.path.exists(self.expected_report)) + + with open(self.expected_report, "r") as report_file: + file_contents = report_file.read() + self.assertIn("UCES", file_contents) + os.remove(self.expected_report) + + self.ss.UCES.export_csv('./UCES.csv') + self.assertTrue(os.path.exists('./UCES.csv')) + os.remove('./UCES.csv') From 1dc7c6ef9858e89fcf6367d1d76a2c629adce31b Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 24 Nov 2024 15:17:08 -0500 Subject: [PATCH 31/33] 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 9db44e7c..c1e222f0 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -15,6 +15,7 @@ v0.9.13 (2024-xx-xx) - 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`` v0.9.12 (2024-11-23) -------------------- From fe1d1797b39fa514e9c56638912e1cbd0a2e4f6f Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 24 Nov 2024 15:23:00 -0500 Subject: [PATCH 32/33] Fix test_export_csv --- tests/test_export_csv.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/test_export_csv.py b/tests/test_export_csv.py index 0d64228b..a3409b9b 100644 --- a/tests/test_export_csv.py +++ b/tests/test_export_csv.py @@ -35,7 +35,7 @@ def test_export_DCOPF(self): Test export DCOPF to CSV. """ self.ss.DCOPF.run(solver='CLARABEL') - self.assertTrue(self.ss.DCOPF.export_csv()) + self.ss.DCOPF.export_csv(self.expected_csv_DCOPF) self.assertTrue(os.path.exists(self.expected_csv_DCOPF)) n_rows = 0 @@ -48,7 +48,9 @@ def test_export_DCOPF(self): if n_cols == 0 or len(row) > n_cols: n_cols = len(row) - n_cols_expected = np.sum([v.shape[0] for v in self.ss.DCOPF.vars.values()]) + n_cols_expected = np.sum([v.owner.n for v in self.ss.DCOPF.vars.values()]) + n_cols_expected += np.sum([v.owner.n for v in self.ss.DCOPF.exprs.values()]) + n_cols_expected += np.sum([v.owner.n for v in self.ss.DCOPF.exprcs.values()]) # cols number plus one for the index column self.assertEqual(n_cols, n_cols_expected + 1) # header row plus data row @@ -62,7 +64,7 @@ def test_export_ED(self): Test export ED to CSV. """ self.ss.ED.run(solver='CLARABEL') - self.assertTrue(self.ss.ED.export_csv()) + self.ss.ED.export_csv(self.expected_csv_ED) self.assertTrue(os.path.exists(self.expected_csv_ED)) n_rows = 0 @@ -75,7 +77,9 @@ def test_export_ED(self): if n_cols == 0 or len(row) > n_cols: n_cols = len(row) - n_cols_expected = np.sum([v.shape[0] for v in self.ss.ED.vars.values()]) + n_cols_expected = np.sum([v.owner.n for v in self.ss.ED.vars.values()]) + n_cols_expected += np.sum([v.owner.n for v in self.ss.ED.exprs.values()]) + n_cols_expected += np.sum([v.owner.n for v in self.ss.ED.exprcs.values()]) # cols number plus one for the index column self.assertEqual(n_cols, n_cols_expected + 1) # header row plus data row From bd4067849866092e007ba742879fabd039f551b4 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 24 Nov 2024 16:28:16 -0500 Subject: [PATCH 33/33] Fix RoutineBase.export_csv --- ams/routines/routine.py | 41 ++++++------------------- tests/test_export_csv.py | 4 +-- tests/test_report.py | 66 ++++++++++++++++++++-------------------- 3 files changed, 45 insertions(+), 66 deletions(-) diff --git a/ams/routines/routine.py b/ams/routines/routine.py index b1a8fced..d58dd175 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -453,7 +453,15 @@ def export_csv(self, path=None): logger.warning("Routine did not converge, aborting export.") return None - export_path = get_export_path(self, path) + if not path: + if self.system.files.fullname is None: + logger.info("Input file name not detacted. Using `Untitled`.") + file_name = f'Untitled_{self.class_name}' + else: + file_name = os.path.splitext(self.system.files.fullname)[0] + file_name += f'_{self.class_name}' + path = os.path.join(os.getcwd(), file_name + '.csv') + data_dict = initialize_data_dict(self) collect_data(self, data_dict, self.vars, 'v') @@ -465,7 +473,7 @@ def export_csv(self, path=None): pd.DataFrame(data_dict).to_csv(path, index=False) - return export_path + return file_name + '.csv' def summary(self, **kwargs): """ @@ -976,35 +984,6 @@ def _initial_guess(self): raise NotImplementedError -def get_export_path(rtn: RoutineBase, path: Optional[str]): - """ - Get the export path for the csv file. - - Parameters - ---------- - rtn : ams.routines.routine.RoutineBase - The routine to export. - path : str - Path of the csv file to save. - - Returns - ------- - str - The path of the exported csv file. - """ - if path: - return path - - if rtn.system.files.fullname is None: - logger.info("Input file name not detected. Using `Untitled`.") - file_name = f'Untitled_{rtn.class_name}' - else: - file_name = os.path.splitext(rtn.system.files.fullname)[0] - file_name += f'_{rtn.class_name}' - - return os.path.join(os.getcwd(), file_name + '.csv') - - def initialize_data_dict(rtn: RoutineBase): """ Initialize the data dictionary for export. diff --git a/tests/test_export_csv.py b/tests/test_export_csv.py index a3409b9b..5b0d9300 100644 --- a/tests/test_export_csv.py +++ b/tests/test_export_csv.py @@ -35,7 +35,7 @@ def test_export_DCOPF(self): Test export DCOPF to CSV. """ self.ss.DCOPF.run(solver='CLARABEL') - self.ss.DCOPF.export_csv(self.expected_csv_DCOPF) + self.ss.DCOPF.export_csv() self.assertTrue(os.path.exists(self.expected_csv_DCOPF)) n_rows = 0 @@ -64,7 +64,7 @@ def test_export_ED(self): Test export ED to CSV. """ self.ss.ED.run(solver='CLARABEL') - self.ss.ED.export_csv(self.expected_csv_ED) + self.ss.ED.export_csv() self.assertTrue(os.path.exists(self.expected_csv_ED)) n_rows = 0 diff --git a/tests/test_report.py b/tests/test_report.py index 404b8bf4..fdac15ba 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -60,9 +60,9 @@ def test_DCOPF_report(self): self.assertIn("DCOPF", file_contents) os.remove(self.expected_report) - self.ss.DCOPF.export_csv('./DCOPF.csv') - self.assertTrue(os.path.exists('./DCOPF.csv')) - os.remove('./DCOPF.csv') + self.ss.DCOPF.export_csv() + self.assertTrue(os.path.exists('pjm5bus_demo_DCOPF.csv')) + os.remove('pjm5bus_demo_DCOPF.csv') def test_DCPF_report(self): """ @@ -78,9 +78,9 @@ def test_DCPF_report(self): self.assertIn("DCPF", file_contents) os.remove(self.expected_report) - self.ss.DCPF.export_csv('./DCPF.csv') - self.assertTrue(os.path.exists('./DCPF.csv')) - os.remove('./DCPF.csv') + self.ss.DCPF.export_csv() + self.assertTrue(os.path.exists('pjm5bus_demo_DCPF.csv')) + os.remove('pjm5bus_demo_DCPF.csv') def test_RTED_report(self): """ @@ -96,9 +96,9 @@ def test_RTED_report(self): self.assertIn("RTED", file_contents) os.remove(self.expected_report) - self.ss.RTED.export_csv('./RTED.csv') - self.assertTrue(os.path.exists('./RTED.csv')) - os.remove('./RTED.csv') + self.ss.RTED.export_csv() + self.assertTrue(os.path.exists('pjm5bus_demo_RTED.csv')) + os.remove('pjm5bus_demo_RTED.csv') def test_RTEDDG_report(self): """ @@ -114,9 +114,9 @@ def test_RTEDDG_report(self): self.assertIn("RTEDDG", file_contents) os.remove(self.expected_report) - self.ss.RTEDDG.export_csv('./RTEDDG.csv') - self.assertTrue(os.path.exists('./RTEDDG.csv')) - os.remove('./RTEDDG.csv') + self.ss.RTEDDG.export_csv() + self.assertTrue(os.path.exists('pjm5bus_demo_RTEDDG.csv')) + os.remove('pjm5bus_demo_RTEDDG.csv') def test_RTEDES_report(self): """ @@ -132,9 +132,9 @@ def test_RTEDES_report(self): self.assertIn("RTEDES", file_contents) os.remove(self.expected_report) - self.ss.RTEDES.export_csv('./RTEDES.csv') - self.assertTrue(os.path.exists('./RTEDES.csv')) - os.remove('./RTEDES.csv') + self.ss.RTEDES.export_csv() + self.assertTrue(os.path.exists('pjm5bus_demo_RTEDES.csv')) + os.remove('pjm5bus_demo_RTEDES.csv') def test_ED_report(self): """ @@ -150,9 +150,9 @@ def test_ED_report(self): self.assertIn("ED", file_contents) os.remove(self.expected_report) - self.ss.ED.export_csv('./ED.csv') - self.assertTrue(os.path.exists('./ED.csv')) - os.remove('./ED.csv') + self.ss.ED.export_csv() + self.assertTrue(os.path.exists('pjm5bus_demo_ED.csv')) + os.remove('pjm5bus_demo_ED.csv') def test_EDDG_report(self): """ @@ -168,9 +168,9 @@ def test_EDDG_report(self): self.assertIn("EDDG", file_contents) os.remove(self.expected_report) - self.ss.EDDG.export_csv('./EDDG.csv') - self.assertTrue(os.path.exists('./EDDG.csv')) - os.remove('./EDDG.csv') + self.ss.EDDG.export_csv() + self.assertTrue(os.path.exists('pjm5bus_demo_EDDG.csv')) + os.remove('pjm5bus_demo_EDDG.csv') def test_EDES_report(self): """ @@ -186,9 +186,9 @@ def test_EDES_report(self): self.assertIn("EDES", file_contents) os.remove(self.expected_report) - self.ss.EDES.export_csv('./EDES.csv') - self.assertTrue(os.path.exists('./EDES.csv')) - os.remove('./EDES.csv') + self.ss.EDES.export_csv() + self.assertTrue(os.path.exists('pjm5bus_demo_EDES.csv')) + os.remove('pjm5bus_demo_EDES.csv') def test_UC_report(self): """ @@ -204,9 +204,9 @@ def test_UC_report(self): self.assertIn("UC", file_contents) os.remove(self.expected_report) - self.ss.UC.export_csv('./UC.csv') - self.assertTrue(os.path.exists('./UC.csv')) - os.remove('./UC.csv') + self.ss.UC.export_csv() + self.assertTrue(os.path.exists('pjm5bus_demo_UC.csv')) + os.remove('pjm5bus_demo_UC.csv') def test_UCDG_report(self): """ @@ -222,9 +222,9 @@ def test_UCDG_report(self): self.assertIn("UCDG", file_contents) os.remove(self.expected_report) - self.ss.UCDG.export_csv('./UCDG.csv') - self.assertTrue(os.path.exists('./UCDG.csv')) - os.remove('./UCDG.csv') + self.ss.UCDG.export_csv() + self.assertTrue(os.path.exists('pjm5bus_demo_UCDG.csv')) + os.remove('pjm5bus_demo_UCDG.csv') def test_UCES_report(self): """ @@ -240,6 +240,6 @@ def test_UCES_report(self): self.assertIn("UCES", file_contents) os.remove(self.expected_report) - self.ss.UCES.export_csv('./UCES.csv') - self.assertTrue(os.path.exists('./UCES.csv')) - os.remove('./UCES.csv') + self.ss.UCES.export_csv() + self.assertTrue(os.path.exists('pjm5bus_demo_UCES.csv')) + os.remove('pjm5bus_demo_UCES.csv')