From 007618d232af002215f06c818890e38aa3caef6e Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 28 Oct 2023 06:00:19 -0400 Subject: [PATCH 01/24] Revise for json and xlsx output file --- ams/models/timeslot.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/ams/models/timeslot.py b/ams/models/timeslot.py index 6385d697..c57b5290 100644 --- a/ams/models/timeslot.py +++ b/ams/models/timeslot.py @@ -3,10 +3,19 @@ """ from andes.core import (ModelData, NumParam, DataParam) # NOQA -from andes.models.timeseries import (str_list_iconv, str_list_oconv) # NOQA +from andes.models.timeseries import (str_list_iconv) # NOQA from ams.core.model import Model # NOQA +def str_list_oconv(x): + """ + Convert list into a list literal. + """ + # NOTE: convert elements to string from number first, then join them + str_x = [str(i) for i in x] + return ','.join(str_x) + + class TimeSlot(ModelData, Model): """ Time slot data for rolling horizon. From 69588b6af4f073a70f61602fa928c1b89b5ab023 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 28 Oct 2023 06:04:13 -0400 Subject: [PATCH 02/24] Fix json and xlsx output --- ams/cases/5bus/pjm5bus_uced.json | 1012 ++++++++++++++++++++++++++++++ ams/io/json.py | 39 +- ams/io/xlsx.py | 26 +- 3 files changed, 1045 insertions(+), 32 deletions(-) create mode 100644 ams/cases/5bus/pjm5bus_uced.json diff --git a/ams/cases/5bus/pjm5bus_uced.json b/ams/cases/5bus/pjm5bus_uced.json new file mode 100644 index 00000000..6b7baeb4 --- /dev/null +++ b/ams/cases/5bus/pjm5bus_uced.json @@ -0,0 +1,1012 @@ +{ + "Summary": [ + { + "field": "About", + "comment": "PJM 5-bus test case", + "comment2": null, + "comment3": null, + "comment4": null + }, + { + "field": "Load", + "comment": "Load factor in EDTSlot and UCTSlot sourced from PJM 5-minutes load on August 30, 2023", + "comment2": null, + "comment3": null, + "comment4": null + }, + { + "field": "StaticGen", + "comment": "Minimum on/off time of generators were made up", + "comment2": null, + "comment3": null, + "comment4": null + } + ], + "Bus": [ + { + "idx": 0, + "u": 1.0, + "name": "A", + "Vn": 230.0, + "vmax": 1.1, + "vmin": 0.9, + "v0": 1.0, + "a0": 0.0, + "xcoord": 0, + "ycoord": 0, + "area": 1, + "zone": "ZONE_1", + "owner": null + }, + { + "idx": 1, + "u": 1.0, + "name": "B", + "Vn": 230.0, + "vmax": 1.1, + "vmin": 0.9, + "v0": 1.0, + "a0": 0.0, + "xcoord": 0, + "ycoord": 0, + "area": 1, + "zone": "ZONE_1", + "owner": null + }, + { + "idx": 2, + "u": 1.0, + "name": "C", + "Vn": 230.0, + "vmax": 1.1, + "vmin": 0.9, + "v0": 1.0, + "a0": 0.0, + "xcoord": 0, + "ycoord": 0, + "area": 2, + "zone": "ZONE_1", + "owner": null + }, + { + "idx": 3, + "u": 1.0, + "name": "D", + "Vn": 230.0, + "vmax": 1.1, + "vmin": 0.9, + "v0": 1.0, + "a0": 0.0, + "xcoord": 0, + "ycoord": 0, + "area": 2, + "zone": "ZONE_1", + "owner": null + }, + { + "idx": 4, + "u": 1.0, + "name": "E", + "Vn": 230.0, + "vmax": 1.1, + "vmin": 0.9, + "v0": 1.0, + "a0": 0.0, + "xcoord": 0, + "ycoord": 0, + "area": 3, + "zone": "ZONE_1", + "owner": null + } + ], + "PQ": [ + { + "idx": "PQ_1", + "u": 1.0, + "name": "PQ 1", + "bus": 1, + "Vn": 230.0, + "p0": 3.0, + "q0": 0.9861, + "vmax": 1.1, + "vmin": 0.9, + "owner": null + }, + { + "idx": "PQ_2", + "u": 1.0, + "name": "PQ 2", + "bus": 2, + "Vn": 230.0, + "p0": 3.0, + "q0": 0.9861, + "vmax": 1.1, + "vmin": 0.9, + "owner": null + }, + { + "idx": "PQ_3", + "u": 1.0, + "name": "PQ 3", + "bus": 3, + "Vn": 230.0, + "p0": 4.0, + "q0": 1.3147, + "vmax": 1.1, + "vmin": 0.9, + "owner": null + } + ], + "PV": [ + { + "idx": "PV_1", + "u": 1.0, + "name": "Alta", + "Sn": 100.0, + "Vn": 230.0, + "bus": 0, + "busr": null, + "p0": 1.0, + "q0": 0.0, + "pmax": 2.1, + "pmin": 0.2, + "qmax": 0.3, + "qmin": -0.3, + "v0": 1.0, + "vmax": 1.4, + "vmin": 0.6, + "ra": 0.01, + "xs": 0.3, + "ctrl": 1.0, + "Pc1": 0.0, + "Pc2": 0.0, + "Qc1min": 0.0, + "Qc1max": 0.0, + "Qc2min": 0.0, + "Qc2max": 0.0, + "Ragc": 999.0, + "R10": 999.0, + "R30": 999.0, + "Rq": 999.0, + "apf": 0.0, + "pg0": 0.0, + "td1": 0.5, + "td2": 0.0 + }, + { + "idx": "PV_3", + "u": 1.0, + "name": "Solitude", + "Sn": 100.0, + "Vn": 230.0, + "bus": 2, + "busr": null, + "p0": 3.2349, + "q0": 0.0, + "pmax": 5.2, + "pmin": 0.5, + "qmax": 1.275, + "qmin": -1.275, + "v0": 1.0, + "vmax": 1.4, + "vmin": 0.6, + "ra": 0.01, + "xs": 0.3, + "ctrl": 1.0, + "Pc1": 0.0, + "Pc2": 0.0, + "Qc1min": 0.0, + "Qc1max": 0.0, + "Qc2min": 0.0, + "Qc2max": 0.0, + "Ragc": 999.0, + "R10": 999.0, + "R30": 999.0, + "Rq": 999.0, + "apf": 0.0, + "pg0": 0.0, + "td1": 0.5, + "td2": 0.0 + }, + { + "idx": "PV_5", + "u": 1.0, + "name": "Brighton", + "Sn": 100.0, + "Vn": 230.0, + "bus": 4, + "busr": null, + "p0": 4.6651, + "q0": 0.0, + "pmax": 6.0, + "pmin": 0.6, + "qmax": 4.5, + "qmin": -4.5, + "v0": 1.0, + "vmax": 1.4, + "vmin": 0.6, + "ra": 0.01, + "xs": 0.3, + "ctrl": 1.0, + "Pc1": 0.0, + "Pc2": 0.0, + "Qc1min": 0.0, + "Qc1max": 0.0, + "Qc2min": 0.0, + "Qc2max": 0.0, + "Ragc": 999.0, + "R10": 999.0, + "R30": 999.0, + "Rq": 999.0, + "apf": 0.0, + "pg0": 0.0, + "td1": 0.5, + "td2": 0.0 + } + ], + "Slack": [ + { + "idx": "Slack_4", + "u": 1.0, + "name": "Sundance", + "Sn": 100.0, + "Vn": 230.0, + "bus": 3, + "busr": null, + "p0": 0.0, + "q0": 0.0, + "pmax": 2.0, + "pmin": 0.2, + "qmax": 1.5, + "qmin": -1.5, + "v0": 1.0, + "vmax": 1.4, + "vmin": 0.6, + "ra": 0.01, + "xs": 0.3, + "a0": 0.0, + "ctrl": 1.0, + "Pc1": 0.0, + "Pc2": 0.0, + "Qc1min": 0.0, + "Qc1max": 0.0, + "Qc2min": 0.0, + "Qc2max": 0.0, + "Ragc": 999.0, + "R10": 999.0, + "R30": 999.0, + "Rq": 999.0, + "apf": 0.0, + "pg0": 0.0, + "td1": 0.5, + "td2": 0.0 + } + ], + "Line": [ + { + "idx": 0, + "u": 1.0, + "name": "Line 1-2", + "bus1": 0, + "bus2": 1, + "Sn": 100.0, + "fn": 60.0, + "Vn1": 230.0, + "Vn2": 230.0, + "r": 0.00281, + "x": 0.0281, + "b": 0.00712, + "g": 0.0, + "b1": 0.0, + "g1": 0.0, + "b2": 0.0, + "g2": 0.0, + "trans": 0.0, + "tap": 1.0, + "phi": 0.0, + "rate_a": 999.0, + "rate_b": 999.0, + "rate_c": 999.0, + "owner": null, + "xcoord": null, + "ycoord": null, + "amin": -6.283185307179586, + "amax": 6.283185307179586 + }, + { + "idx": 1, + "u": 1.0, + "name": "Line 1-4", + "bus1": 0, + "bus2": 3, + "Sn": 100.0, + "fn": 60.0, + "Vn1": 230.0, + "Vn2": 230.0, + "r": 0.00304, + "x": 0.0304, + "b": 0.00658, + "g": 0.0, + "b1": 0.0, + "g1": 0.0, + "b2": 0.0, + "g2": 0.0, + "trans": 0.0, + "tap": 1.0, + "phi": 0.0, + "rate_a": 999.0, + "rate_b": 999.0, + "rate_c": 999.0, + "owner": null, + "xcoord": null, + "ycoord": null, + "amin": -6.283185307179586, + "amax": 6.283185307179586 + }, + { + "idx": 2, + "u": 1.0, + "name": "Line 1-5", + "bus1": 0, + "bus2": 4, + "Sn": 100.0, + "fn": 60.0, + "Vn1": 230.0, + "Vn2": 230.0, + "r": 0.00064, + "x": 0.0064, + "b": 0.03126, + "g": 0.0, + "b1": 0.0, + "g1": 0.0, + "b2": 0.0, + "g2": 0.0, + "trans": 0.0, + "tap": 1.0, + "phi": 0.0, + "rate_a": 999.0, + "rate_b": 999.0, + "rate_c": 999.0, + "owner": null, + "xcoord": null, + "ycoord": null, + "amin": -6.283185307179586, + "amax": 6.283185307179586 + }, + { + "idx": 3, + "u": 1.0, + "name": "Line 2-3", + "bus1": 1, + "bus2": 2, + "Sn": 100.0, + "fn": 60.0, + "Vn1": 230.0, + "Vn2": 230.0, + "r": 0.00108, + "x": 0.0108, + "b": 0.01852, + "g": 0.0, + "b1": 0.0, + "g1": 0.0, + "b2": 0.0, + "g2": 0.0, + "trans": 0.0, + "tap": 1.0, + "phi": 0.0, + "rate_a": 999.0, + "rate_b": 999.0, + "rate_c": 999.0, + "owner": null, + "xcoord": null, + "ycoord": null, + "amin": -6.283185307179586, + "amax": 6.283185307179586 + }, + { + "idx": 4, + "u": 1.0, + "name": "Line 3-4", + "bus1": 2, + "bus2": 3, + "Sn": 100.0, + "fn": 60.0, + "Vn1": 230.0, + "Vn2": 230.0, + "r": 0.00297, + "x": 0.0297, + "b": 0.00674, + "g": 0.0, + "b1": 0.0, + "g1": 0.0, + "b2": 0.0, + "g2": 0.0, + "trans": 0.0, + "tap": 1.0, + "phi": 0.0, + "rate_a": 999.0, + "rate_b": 999.0, + "rate_c": 999.0, + "owner": null, + "xcoord": null, + "ycoord": null, + "amin": -6.283185307179586, + "amax": 6.283185307179586 + }, + { + "idx": 5, + "u": 1.0, + "name": "Line 4-5", + "bus1": 3, + "bus2": 4, + "Sn": 100.0, + "fn": 60.0, + "Vn1": 230.0, + "Vn2": 230.0, + "r": 0.00297, + "x": 0.0297, + "b": 0.00674, + "g": 0.0, + "b1": 0.0, + "g1": 0.0, + "b2": 0.0, + "g2": 0.0, + "trans": 0.0, + "tap": 1.0, + "phi": 0.0, + "rate_a": 999.0, + "rate_b": 999.0, + "rate_c": 999.0, + "owner": null, + "xcoord": null, + "ycoord": null, + "amin": -6.283185307179586, + "amax": 6.283185307179586 + }, + { + "idx": 6, + "u": 1.0, + "name": "Line 1-2 (2)", + "bus1": 0, + "bus2": 1, + "Sn": 100.0, + "fn": 60.0, + "Vn1": 230.0, + "Vn2": 230.0, + "r": 0.00281, + "x": 0.0281, + "b": 0.00712, + "g": 0.0, + "b1": 0.0, + "g1": 0.0, + "b2": 0.0, + "g2": 0.0, + "trans": 0.0, + "tap": 1.0, + "phi": 0.0, + "rate_a": 999.0, + "rate_b": 999.0, + "rate_c": 999.0, + "owner": null, + "xcoord": null, + "ycoord": null, + "amin": -6.283185307179586, + "amax": 6.283185307179586 + } + ], + "Area": [ + { + "idx": 1, + "u": 1.0, + "name": 1 + }, + { + "idx": 2, + "u": 1.0, + "name": 2 + }, + { + "idx": 3, + "u": 1.0, + "name": 3 + } + ], + "Region": [ + { + "idx": "ZONE_1", + "u": 1.0, + "name": "ZONE1" + }, + { + "idx": "ZONE_2", + "u": 1.0, + "name": "ZONE2" + } + ], + "SFR": [ + { + "idx": "SFR1", + "u": 1.0, + "name": "SFR1", + "zone": "ZONE1", + "du": 0.03, + "dd": 0.03 + }, + { + "idx": "SFR2", + "u": 1.0, + "name": "SFR2", + "zone": "ZONE2", + "du": 0.03, + "dd": 0.03 + } + ], + "SR": [ + { + "idx": "SR1", + "u": 1.0, + "name": "SR1", + "zone": "ZONE1", + "demand": 0.03 + }, + { + "idx": "SR2", + "u": 1.0, + "name": "SR2", + "zone": "ZONE2", + "demand": 0.03 + } + ], + "NSR": [ + { + "idx": "NSR1", + "u": 1.0, + "name": "NSR1", + "zone": "ZONE1", + "demand": 0.01 + }, + { + "idx": "NSR2", + "u": 1.0, + "name": "NSR2", + "zone": "ZONE2", + "demand": 0.01 + } + ], + "GCost": [ + { + "idx": "GCost_1", + "u": 1.0, + "name": "GCost_1", + "gen": "PV_1", + "type": 2.0, + "csu": 0.0, + "csd": 0.0, + "c2": 0.0, + "c1": 1450.0, + "c0": 0.0 + }, + { + "idx": "GCost_2", + "u": 1.0, + "name": "GCost_2", + "gen": "PV_3", + "type": 2.0, + "csu": 0.0, + "csd": 0.0, + "c2": 0.0, + "c1": 3000.0, + "c0": 0.0 + }, + { + "idx": "GCost_3", + "u": 1.0, + "name": "GCost_3", + "gen": "PV_5", + "type": 2.0, + "csu": 0.0, + "csd": 0.0, + "c2": 0.0, + "c1": 4000.0, + "c0": 0.0 + }, + { + "idx": "GCost_4", + "u": 1.0, + "name": "GCost_4", + "gen": "Slack_4", + "type": 2.0, + "csu": 0.0, + "csd": 0.0, + "c2": 0.0, + "c1": 3000.0, + "c0": 0.0 + } + ], + "SFRCost": [ + { + "idx": "SFRC_1", + "u": 1.0, + "name": "SFRC_1", + "gen": "PV_1", + "cru": 0.0, + "crd": 0.0 + }, + { + "idx": "SFRC_2", + "u": 1.0, + "name": "SFRC_2", + "gen": "PV_3", + "cru": 0.0, + "crd": 0.0 + }, + { + "idx": "SFRC_3", + "u": 1.0, + "name": "SFRC_3", + "gen": "PV_5", + "cru": 0.0, + "crd": 0.0 + }, + { + "idx": "SFRC_4", + "u": 1.0, + "name": "SFRC_4", + "gen": "Slack_4", + "cru": 0.0, + "crd": 0.0 + } + ], + "SRCost": [ + { + "idx": "SRC_1", + "u": 1.0, + "name": "SRC_1", + "gen": "PV_1", + "csr": 0.1 + }, + { + "idx": "SRC_2", + "u": 1.0, + "name": "SRC_2", + "gen": "PV_3", + "csr": 0.1 + }, + { + "idx": "SRC_3", + "u": 1.0, + "name": "SRC_3", + "gen": "PV_5", + "csr": 0.1 + }, + { + "idx": "SRC_4", + "u": 1.0, + "name": "SRC_4", + "gen": "Slack_4", + "csr": 0.1 + } + ], + "NSRCost": [ + { + "idx": "NSRC_1", + "u": 1.0, + "name": "NSRC_1", + "gen": "PV_1", + "cnsr": 0.1 + }, + { + "idx": "NSRC_2", + "u": 1.0, + "name": "NSRC_2", + "gen": "PV_3", + "cnsr": 0.1 + }, + { + "idx": "NSRC_3", + "u": 1.0, + "name": "NSRC_3", + "gen": "PV_5", + "cnsr": 0.1 + }, + { + "idx": "NSRC_4", + "u": 1.0, + "name": "NSRC_4", + "gen": "Slack_4", + "cnsr": 0.1 + } + ], + "EDTSlot": [ + { + "idx": "EDT1", + "u": 1.0, + "name": "EDT1", + "sd": "0.793,0.0" + }, + { + "idx": "EDT2", + "u": 1.0, + "name": "EDT2", + "sd": "0.756,0.0" + }, + { + "idx": "EDT3", + "u": 1.0, + "name": "EDT3", + "sd": "0.723,0.0" + }, + { + "idx": "EDT4", + "u": 1.0, + "name": "EDT4", + "sd": "0.708,0.0" + }, + { + "idx": "EDT5", + "u": 1.0, + "name": "EDT5", + "sd": "0.7,0.0" + }, + { + "idx": "EDT6", + "u": 1.0, + "name": "EDT6", + "sd": "0.706,0.0" + }, + { + "idx": "EDT7", + "u": 1.0, + "name": "EDT7", + "sd": "0.75,0.0" + }, + { + "idx": "EDT8", + "u": 1.0, + "name": "EDT8", + "sd": "0.802,0.0" + }, + { + "idx": "EDT9", + "u": 1.0, + "name": "EDT9", + "sd": "0.828,0.0" + }, + { + "idx": "EDT10", + "u": 1.0, + "name": "EDT10", + "sd": "0.851,0.0" + }, + { + "idx": "EDT11", + "u": 1.0, + "name": "EDT11", + "sd": "0.874,0.0" + }, + { + "idx": "EDT12", + "u": 1.0, + "name": "EDT12", + "sd": "0.898,0.0" + }, + { + "idx": "EDT13", + "u": 1.0, + "name": "EDT13", + "sd": "0.919,0.0" + }, + { + "idx": "EDT14", + "u": 1.0, + "name": "EDT14", + "sd": "0.947,0.0" + }, + { + "idx": "EDT15", + "u": 1.0, + "name": "EDT15", + "sd": "0.97,0.0" + }, + { + "idx": "EDT16", + "u": 1.0, + "name": "EDT16", + "sd": "0.987,0.0" + }, + { + "idx": "EDT17", + "u": 1.0, + "name": "EDT17", + "sd": "1.0,0.0" + }, + { + "idx": "EDT18", + "u": 1.0, + "name": "EDT18", + "sd": "1.0,0.0" + }, + { + "idx": "EDT19", + "u": 1.0, + "name": "EDT19", + "sd": "0.991,0.0" + }, + { + "idx": "EDT20", + "u": 1.0, + "name": "EDT20", + "sd": "0.956,0.0" + }, + { + "idx": "EDT21", + "u": 1.0, + "name": "EDT21", + "sd": "0.93,0.0" + }, + { + "idx": "EDT22", + "u": 1.0, + "name": "EDT22", + "sd": "0.905,0.0" + }, + { + "idx": "EDT23", + "u": 1.0, + "name": "EDT23", + "sd": "0.849,0.0" + }, + { + "idx": "EDT24", + "u": 1.0, + "name": "EDT24", + "sd": "0.784,0.0" + } + ], + "UCTSlot": [ + { + "idx": "UCT1", + "u": 1.0, + "name": "UCT1", + "sd": "0.793,0.0" + }, + { + "idx": "UCT2", + "u": 1.0, + "name": "UCT2", + "sd": "0.756,0.0" + }, + { + "idx": "UCT3", + "u": 1.0, + "name": "UCT3", + "sd": "0.723,0.0" + }, + { + "idx": "UCT4", + "u": 1.0, + "name": "UCT4", + "sd": "0.708,0.0" + }, + { + "idx": "UCT5", + "u": 1.0, + "name": "UCT5", + "sd": "0.7,0.0" + }, + { + "idx": "UCT6", + "u": 1.0, + "name": "UCT6", + "sd": "0.706,0.0" + }, + { + "idx": "UCT7", + "u": 1.0, + "name": "UCT7", + "sd": "0.75,0.0" + }, + { + "idx": "UCT8", + "u": 1.0, + "name": "UCT8", + "sd": "0.802,0.0" + }, + { + "idx": "UCT9", + "u": 1.0, + "name": "UCT9", + "sd": "0.828,0.0" + }, + { + "idx": "UCT10", + "u": 1.0, + "name": "UCT10", + "sd": "0.851,0.0" + }, + { + "idx": "UCT11", + "u": 1.0, + "name": "UCT11", + "sd": "0.874,0.0" + }, + { + "idx": "UCT12", + "u": 1.0, + "name": "UCT12", + "sd": "0.898,0.0" + }, + { + "idx": "UCT13", + "u": 1.0, + "name": "UCT13", + "sd": "0.919,0.0" + }, + { + "idx": "UCT14", + "u": 1.0, + "name": "UCT14", + "sd": "0.947,0.0" + }, + { + "idx": "UCT15", + "u": 1.0, + "name": "UCT15", + "sd": "0.97,0.0" + }, + { + "idx": "UCT16", + "u": 1.0, + "name": "UCT16", + "sd": "0.987,0.0" + }, + { + "idx": "UCT17", + "u": 1.0, + "name": "UCT17", + "sd": "1.0,0.0" + }, + { + "idx": "UCT18", + "u": 1.0, + "name": "UCT18", + "sd": "1.0,0.0" + }, + { + "idx": "UCT19", + "u": 1.0, + "name": "UCT19", + "sd": "0.991,0.0" + }, + { + "idx": "UCT20", + "u": 1.0, + "name": "UCT20", + "sd": "0.956,0.0" + }, + { + "idx": "UCT21", + "u": 1.0, + "name": "UCT21", + "sd": "0.93,0.0" + }, + { + "idx": "UCT22", + "u": 1.0, + "name": "UCT22", + "sd": "0.905,0.0" + }, + { + "idx": "UCT23", + "u": 1.0, + "name": "UCT23", + "sd": "0.849,0.0" + }, + { + "idx": "UCT24", + "u": 1.0, + "name": "UCT24", + "sd": "0.784,0.0" + } + ] +} \ No newline at end of file diff --git a/ams/io/json.py b/ams/io/json.py index e2538758..df0a09ab 100644 --- a/ams/io/json.py +++ b/ams/io/json.py @@ -10,6 +10,7 @@ from andes.io.json import (testlines, read) # NOQA from andes.utils.paths import confirm_overwrite + from andes.system import System as andes_system logger = logging.getLogger(__name__) @@ -19,9 +20,11 @@ def write(system, outfile, skip_empty=True, overwrite=None, to_andes=False, ): """ - Write loaded AMS system data into an xlsx file + Write loaded AMS system data into an json file. + If to_andes is True, only write models that are in ANDES, + but the outfile might not be able to be read back into AMS. - Revised function ``andes.io.xlsx.write`` to skip non-andes models. + Revise function ``andes.io.json.write`` to skip non-andes models. Parameters ---------- @@ -33,6 +36,8 @@ def write(system, outfile, skip_empty=True, overwrite=None, Skip output of empty models (n = 0) overwrite : bool, optional None to prompt for overwrite selection; True to overwrite; False to not overwrite + to_andes : bool, optional + Write to an ANDES system, where non-ANDES models are skipped Returns ------- @@ -46,42 +51,40 @@ def write(system, outfile, skip_empty=True, overwrite=None, outfile.write(_dump_system(system, skip_empty, to_andes=to_andes)) else: with open(outfile, 'w') as writer: - writer.write(_dump_system(system, skip_empty)) + writer.write(_dump_system(system, skip_empty, to_andes=to_andes)) logger.info('JSON file written to "%s"', outfile) return True -def _dump_system(system, skip_empty, orient='records', to_andes=True): +def _dump_system(system, skip_empty, orient='records', to_andes=False): """ Dump parameters of each model into a json string and return them all in an OrderedDict. """ - na_models = [] + ad_models = [] if to_andes: - # Initialize an ANDES system + # Instantiate an ANDES system sa = andes_system(setup=False, default_config=True, codegen=False, autogen_stale=False, - no_undill=True, - ) + no_undill=True,) + ad_models = list(sa.models.keys()) out = OrderedDict() for name, instance in system.models.items(): - skip_params = [] if skip_empty and instance.n == 0: continue - if to_andes: - if name not in sa.models.keys(): - na_models.append(name) - continue - if to_andes: + if to_andes and name in ad_models: + # NOTE: ommit parameters that are not in ANDES + skip_params = [] ams_params = list(instance.params.keys()) andes_params = list(sa.models[name].params.keys()) skip_params = list(set(ams_params) - set(andes_params)) - if skip_params: df = instance.cache.df_in.drop(skip_params, axis=1, errors='ignore') - else: - df = instance.cache.df_in - out[name] = df.to_dict(orient=orient) + out[name] = df.to_dict(orient=orient) + continue + na_models.append(name) + df = instance.cache.df_in + out[name] = df.to_dict(orient=orient) return json.dumps(out, indent=2) diff --git a/ams/io/xlsx.py b/ams/io/xlsx.py index 7d80303f..1e23ab7c 100644 --- a/ams/io/xlsx.py +++ b/ams/io/xlsx.py @@ -34,6 +34,8 @@ def write(system, outfile, None to prompt for overwrite selection; True to overwrite; False to not overwrite add_book : str, optional An optional model to be added to the output spreadsheet + to_andes : bool, optional + Write to an ANDES system, where non-ANDES models are skipped Returns ------- @@ -53,35 +55,31 @@ def write(system, outfile, return True -def _write_system(system, writer, skip_empty, to_andes=True): +def _write_system(system, writer, skip_empty, to_andes=False): """ Write the system to pandas ExcelWriter Rewrite function ``andes.io.xlsx._write_system`` to skip non-andes sheets. """ - na_models = [] skip_params = [] + ad_models = [] if to_andes: - # Initialize an ANDES system + # Instantiate an ANDES system sa = andes_system(setup=False, default_config=True, codegen=False, autogen_stale=False, - no_undill=True, - ) + no_undill=True,) + ad_models = list(sa.models.keys()) for name, instance in system.models.items(): if skip_empty and instance.n == 0: continue - if to_andes: - if name not in sa.models.keys(): - na_models.append(name) - continue instance.cache.refresh("df_in") - if to_andes: + df = instance.cache.df_in + if to_andes and name in ad_models: + # NOTE: ommit parameters that are not in ANDES + skip_params = [] ams_params = list(instance.params.keys()) andes_params = list(sa.models[name].params.keys()) skip_params = list(set(ams_params) - set(andes_params)) - if skip_params: - df = instance.cache.df_in.drop(skip_params, axis=1) - else: - df = instance.cache.df_in + df = instance.cache.df_in.drop(skip_params, axis=1, errors='ignore') df.to_excel(writer, sheet_name=name, freeze_panes=(1, 0)) return writer From f1c65063daca74f6f1b29b4b0a02a2c13726714b Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 28 Oct 2023 07:30:22 -0400 Subject: [PATCH 03/24] Outline unittests --- ams/cli.py | 28 +++- ams/main.py | 303 ++++++++++++++++++++++++++++++++++++++- ams/system.py | 63 +++++++- tests/test_1st_system.py | 15 ++ 4 files changed, 405 insertions(+), 4 deletions(-) create mode 100644 tests/test_1st_system.py diff --git a/ams/cli.py b/ams/cli.py index cf283fc1..25ce504d 100644 --- a/ams/cli.py +++ b/ams/cli.py @@ -44,6 +44,33 @@ def create_parser(): '[prepare] prepare the numerical code; ' '[selftest] run self test; ' ) + + doc = sub_parsers.add_parser('doc') + # TODO: fit to AMS + # doc.add_argument('attribute', help='System attribute name to get documentation', nargs='?') + # doc.add_argument('--config', '-c', help='Config help') + doc.add_argument('--list', '-l', help='List supported models and groups', action='store_true', + dest='list_supported') + + misc = sub_parsers.add_parser('misc') + config_exclusive = misc.add_mutually_exclusive_group() + config_exclusive.add_argument('--edit-config', help='Quick edit of the config file', + default='', nargs='?', type=str) + config_exclusive.add_argument('--save-config', help='save configuration to file name', + nargs='?', type=str, default='') + misc.add_argument('--license', action='store_true', help='Display software license', dest='show_license') + misc.add_argument('-C', '--clean', help='Clean output files', action='store_true') + misc.add_argument('-r', '--recursive', help='Recursively clean outputs (combined useage with --clean)', + action='store_true') + # TODO: fit to AMS + misc.add_argument('-O', '--config-option', + help='Set configuration option specificied by ' + 'NAME.FIELD=VALUE with no space. For example, "TDS.tf=2"', + type=str, default='', nargs='*') + misc.add_argument('--version', action='store_true', help='Display version information') + + selftest = sub_parsers.add_parser('selftest', aliases=command_aliases['selftest']) + return parser @@ -100,4 +127,3 @@ def main(): func = getattr(module, cmd) return func(cli=True, **vars(args)) - return func(cli=True, **vars(args)) diff --git a/ams/main.py b/ams/main.py index 7426fefd..c0fe087d 100644 --- a/ams/main.py +++ b/ams/main.py @@ -4,8 +4,15 @@ import logging # NOQA import os # NOQA -from andes.main import set_logger_level, _find_cases # NOQA -from andes.shared import coloredlogs # NOQA +import platform # NOQA +import sys +from subprocess import call # NOQA +from typing import Optional, Union # NOQA + +from ._version import get_versions + +from andes.main import _find_cases # NOQA +from andes.shared import coloredlogs, unittest # NOQA from andes.utils.misc import elapsed, is_interactive # NOQA import ams # NOQA @@ -138,6 +145,16 @@ def load(case, setup=True, return system +def set_logger_level(lg, type_to_set, level): + """ + Set logging level for the given type of handler. + """ + + for h in lg.handlers: + if isinstance(h, type_to_set): + h.setLevel(level) + + def find_log_path(lg): """ Find the file paths of the FileHandlers. @@ -147,3 +164,285 @@ def find_log_path(lg): if isinstance(h, logging.FileHandler): out.append(h.baseFilename) return out + + +def misc(edit_config='', save_config='', show_license=False, clean=True, recursive=False, + overwrite=None, version=False, **kwargs): + """ + Miscellaneous commands. + """ + + if edit_conf(edit_config): + return + if show_license: + print_license() + return + if save_config != '': + save_conf(save_config, overwrite=overwrite, **kwargs) + return + if clean is True: + remove_output(recursive) + return + + if demo is True: + demo(**kwargs) + return + + if version is True: + versioninfo() + return + + logger.info("info: no option specified. Use 'ams misc -h' for help.") + + +def doc(attribute=None, list_supported=False, config=False, **kwargs): + """ + Quick documentation from command-line. + """ + system = System() + if attribute is not None: + if attribute in system.__dict__ and hasattr(system.__dict__[attribute], 'doc'): + logger.info(system.__dict__[attribute].doc()) + else: + logger.error('Model <%s> does not exist.', attribute) + + elif list_supported is True: + logger.info(system.supported_routines()) + + else: + logger.info('info: no option specified. Use \'ams doc -h\' for help.') + + +def demo(**kwargs): + """ + TODO: show some demonstrations from CLI. + """ + raise NotImplementedError("Demos have not been implemented") + + +def versioninfo(): + """ + Print version info for ANDES and dependencies. + """ + + import numpy as np + import cvxpy + import andes + + versions = {'Python': platform.python_version(), + 'ams': get_versions()['version'], + 'andes': andes.__version__, + 'numpy': np.__version__, + 'cvxpy': cvxpy.__version__, + } + maxwidth = max([len(k) for k in versions.keys()]) + + try: + import numba + except ImportError: + numba = None + + if numba is not None: + versions["numba"] = numba.__version__ + + for key, val in versions.items(): + print(f"{key: <{maxwidth}} {val}") + + +def print_license(): + """ + Print out AMS license to stdout. + """ + + print(f""" + AMS version {ams.__version__} + + Copyright (c) 2023 Jinning Wang + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + A copy of the GNU General Public License is included below. + For further information, see . + """) + return True + + +def edit_conf(edit_config: Optional[Union[str, bool]] = ''): + """ + Edit the Andes config file which occurs first in the search path. + + Parameters + ---------- + edit_config : bool + If ``True``, try to open up an editor and edit the config file. Otherwise returns. + + Returns + ------- + bool + ``True`` is a config file is found and an editor is opened. ``False`` if ``edit_config`` is False. + """ + ret = False + + # no `edit-config` supplied + if edit_config == '': + return ret + + conf_path = get_config_path() + + if conf_path is None: + logger.info('Config file does not exist. Automatically saving.') + system = System() + conf_path = system.save_config() + + logger.info('Editing config file "%s"', conf_path) + + editor = '' + if edit_config is not None: + # use `edit_config` as default editor + editor = edit_config + else: + # use the following default editors + if platform.system() == 'Linux': + editor = os.environ.get('EDITOR', 'vim') + elif platform.system() == 'Darwin': + editor = os.environ.get('EDITOR', 'vim') + elif platform.system() == 'Windows': + editor = 'notepad.exe' + + editor_cmd = editor.split() + editor_cmd.append(conf_path) + call(editor_cmd) + ret = True + return ret + + +def save_conf(config_path=None, overwrite=None, **kwargs): + """ + Save the AMS config to a file at the path specified by ``save_config``. + The save action will not run if ``save_config = ''``. + + Parameters + ---------- + config_path : None or str, optional, ('' by default) + + Path to the file to save the config file. If the path is an emtpy + string, the save action will not run. Save to + `~/.ams/ams.conf` if ``None``. + + Returns + ------- + bool + ``True`` is the save action is run. ``False`` otherwise. + """ + ret = False + + # no ``--save-config `` + if config_path == '': + return ret + + if config_path is not None and os.path.isdir(config_path): + config_path = os.path.join(config_path, 'ams.rc') + + ps = System(**kwargs) + ps.save_config(config_path, overwrite=overwrite) + ret = True + + return ret + + +# TODO: list AMS output files here +def remove_output(recursive=False): + """ + Remove the outputs generated by Andes, including power flow reports + ``_out.txt``, time-domain list ``_out.lst`` and data ``_out.dat``, + eigenvalue analysis report ``_eig.txt``. + + Parameters + ---------- + recursive : bool + Recursively clean all subfolders + + Returns + ------- + bool + ``True`` is the function body executes with success. ``False`` + otherwise. + """ + found = False + cwd = os.getcwd() + + if recursive: + dirs = [x[0] for x in os.walk(cwd)] + else: + dirs = (cwd,) + + for d in dirs: + for file in os.listdir(d): + if file.endswith('_eig.txt') or \ + file.endswith('_out.txt'): + found = True + try: + os.remove(os.path.join(d, file)) + logger.info('"%s" removed.', os.path.join(d, file)) + except IOError: + logger.error('Error removing file "%s".', + os.path.join(d, file)) + if not found: + logger.info('No output file found in the working directory.') + + return True + + +def selftest(quick=False, extra=False, **kwargs): + """ + Run unit tests. + """ + + # map verbosity level from logging to unittest + vmap = {1: 3, 10: 3, 20: 2, 30: 1, 40: 1, 50: 1} + verbose = vmap[kwargs.get('verbose', 20)] + + # skip if quick + quick_skips = ('test_1_docs', 'test_codegen_inc') + + # extra test naming convention + extra_test = 'extra_test' + + try: + logger.handlers[0].setLevel(logging.WARNING) + sys.stdout = open(os.devnull, 'w') # suppress print statements + except IndexError: # logger not set up + pass + + # discover test cases + test_directory = tests_root() + suite = unittest.TestLoader().discover(test_directory) + + # remove codegen for quick mode + for test_group in suite._tests: + for test_class in test_group._tests: + tests_keep = list() + + for t in test_class._tests: + # skip the extra tests if `extra` is not True + if (extra is not True) and (extra_test in t._testMethodName): + continue + + # skip the ones for `quick` + if quick is True and (t._testMethodName in quick_skips): + continue + + tests_keep.append(t) + + test_class._tests = tests_keep + + unittest.TextTestRunner(verbosity=verbose).run(suite) + sys.stdout = sys.__stdout__ diff --git a/ams/system.py b/ams/system.py index 718dca30..ec07b6d3 100644 --- a/ams/system.py +++ b/ams/system.py @@ -1,6 +1,7 @@ """ Module for system. """ +import configparser import importlib # NOQA import inspect # NOQA import logging # NOQA @@ -18,6 +19,7 @@ from andes.utils.tab import Tab # NOQA from andes.shared import pd # NOQA +import ams.io # NOQA from ams.models.group import GroupBase # NOQA from ams.routines.type import TypeBase # NOQA from ams.models import file_classes # NOQA @@ -392,7 +394,7 @@ def setup(self): ret = False if self.Line.rate_a.v.max() == 0: - logger.warning("Line rate_a is corrected to large value automatically.") + logger.info("Line rate_a is adjusted to large value automatically.") self.Line.rate_a.v = 99 # === no device addition or removal after this point === # TODO: double check calc_pu_coeff @@ -519,3 +521,62 @@ def to_andes(self, setup=True, addfile=None, overwite=None, no_keep=True, return to_andes(self, setup=setup, addfile=addfile, overwite=overwite, no_keep=no_keep, **kwargs) + + +# --------------- Helper Functions --------------- + +def _config_numpy(seed='None', divide='warn', invalid='warn'): + """ + Configure NumPy based on Config. + """ + + # set up numpy random seed + if isinstance(seed, int): + np.random.seed(seed) + logger.debug("Random seed set to <%d>.", seed) + + # set levels + np.seterr(divide=divide, + invalid=invalid, + ) + + +def load_config_rc(conf_path=None): + """ + Load config from an rc-formatted file. + + Parameters + ---------- + conf_path : None or str + Path to the config file. If is `None`, the function body will not + run. + + Returns + ------- + configparse.ConfigParser + """ + if conf_path is None: + return + + conf = configparser.ConfigParser() + conf.read(conf_path) + logger.info('> Loaded config from file "%s"', conf_path) + return conf + + +def example(setup=True, no_output=True, **kwargs): + """ + Return an :py:class:`ams.system.System` object for the + ``ieee14_uced.xlsx`` as an example. + + This function is useful when a user wants to quickly get a + System object for testing. + + Returns + ------- + System + An example :py:class:`ams.system.System` object. + """ + + return ams.load(ams.get_case('matpower/case14.m'), + setup=setup, no_output=no_output, **kwargs) diff --git a/tests/test_1st_system.py b/tests/test_1st_system.py new file mode 100644 index 00000000..c816fc20 --- /dev/null +++ b/tests/test_1st_system.py @@ -0,0 +1,15 @@ +import unittest + +import ams + + +class TestCodegen(unittest.TestCase): + """ + Test code generation. + """ + + def test_1_docs(self) -> None: + sp = ams.system.System() + out = '' + for tp in sp.types.values(): + out += tp.doc_all() From 48f5a1a3ef06105dbec51e1a9a4dec3c5403f33f Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 28 Oct 2023 07:39:07 -0400 Subject: [PATCH 04/24] Add test addressing --- tests/test_addressing.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 tests/test_addressing.py diff --git a/tests/test_addressing.py b/tests/test_addressing.py new file mode 100644 index 00000000..25f716f0 --- /dev/null +++ b/tests/test_addressing.py @@ -0,0 +1,40 @@ +import unittest + +import ams +import numpy as np + +ams.config_logger(stream_level=40) + + +class TestAddressing(unittest.TestCase): + """ + Tests for addressing. + """ + + def test_ieee14_address(self): + """ + Test IEEE14 address. + """ + + # FIXME: why there will be case parsing information using ams.system.example()? + ss = ams.system.example() + + # bus variable indices (internal) + np.testing.assert_array_equal(ss.Bus.a.a, + np.arange(0, ss.Bus.n, 1)) + np.testing.assert_array_equal(ss.Bus.v.a, + np.arange(ss.Bus.n, 2*ss.Bus.n, 1)) + + # external variable indices + np.testing.assert_array_equal(ss.PV.ud.a, + np.array([28, 29, 30, 31])) + np.testing.assert_array_equal(ss.PV.p.a, + np.array([32, 33, 34, 35])) + np.testing.assert_array_equal(ss.PV.q.a, + np.array([36, 37, 38, 39])) + np.testing.assert_array_equal(ss.Slack.ud.a, + np.array([40])) + np.testing.assert_array_equal(ss.Slack.p.a, + np.array([41])) + np.testing.assert_array_equal(ss.Slack.q.a, + np.array([42])) From 26c77dcc9e266ad519c430a889b087cd6c1f3ee6 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 28 Oct 2023 08:43:01 -0400 Subject: [PATCH 05/24] Fix routine config definition --- ams/core/symprocessor.py | 2 +- ams/routines/ed.py | 13 +++++++++++-- ams/routines/routine.py | 1 - ams/routines/uc.py | 18 ++++++++++++------ 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/ams/core/symprocessor.py b/ams/core/symprocessor.py index d7397fb1..eb5ea42e 100644 --- a/ams/core/symprocessor.py +++ b/ams/core/symprocessor.py @@ -145,7 +145,7 @@ def generate_symbols(self, force_generate=False): # NOTE: hard-coded config 't' tex name as 'T_{cfg}' for clarity in doc self.tex_map['\\bt\\b'] = 'T_{cfg}' - self.tex_map['\\bcp\\b'] = 'c_{p, cfg}' + self.tex_map['\\bcul\\b'] = 'c_{ul, cfg}' # store tex names for pretty printing replacement later for var in self.inputs_dict: diff --git a/ams/routines/ed.py b/ams/routines/ed.py index 71d51460..588ac447 100644 --- a/ams/routines/ed.py +++ b/ams/routines/ed.py @@ -2,6 +2,7 @@ Real-time economic dispatch. """ import logging # NOQA +from collections import OrderedDict # NOQA import numpy as np # NOQA from ams.core.param import RParam # NOQA @@ -61,7 +62,12 @@ class EDModel(DCOPFModel): def __init__(self, system, config): DCOPFModel.__init__(self, system, config) - self.config.t = 1 # dispatch interval in hour + + self.config.add(OrderedDict((('t', 1), + ))) + self.config.add_extra("_help", + t="time interval in hours", + ) self.info = 'Economic dispatch' self.type = 'DCED' @@ -167,6 +173,9 @@ def unpack(self, **kwargs): class ED(EDData, EDModel): """ DC-based multi-period economic dispatch (ED). + Dispath interval ``config.t`` (:math:`T_{cfg}`) is introduced, + 1 [Hour] by default. + ED extends DCOPF as follows: 1. Var ``pg`` is extended to 2D @@ -175,7 +184,7 @@ class ED(EDData, EDModel): Notes ----- - 1. Formulations has been adjusted with interval ``config.t``, 1 [Hour] by default. + 1. Formulations has been adjusted with interval ``config.t`` 2. The tie-line flow is not implemented in this model. """ diff --git a/ams/routines/routine.py b/ams/routines/routine.py index e80b44c6..a7fed1a6 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -70,7 +70,6 @@ def __init__(self, system=None, config=None): # TODO: these default configs might to be revised self.config.add(OrderedDict((('sparselib', 'klu'), ('linsolve', 0), - ('t', 1), # time interval in hours ))) self.config.add_extra("_help", sparselib="linear sparse solver name", diff --git a/ams/routines/uc.py b/ams/routines/uc.py index 8e9cd681..0741820d 100644 --- a/ams/routines/uc.py +++ b/ams/routines/uc.py @@ -66,9 +66,13 @@ class UCModel(EDModel): def __init__(self, system, config): EDModel.__init__(self, system, config) - self.config.t = 1 # dispatch interval in hour - self.config.add(OrderedDict((('cp', 1000), # penalty for unserved load, $/p.u. + + self.config.add(OrderedDict((('cul', 1000), ))) + self.config.add_extra("_help", + cul="penalty for unserved load, $/p.u.", + ) + self.info = 'unit commitment' self.type = 'DCUC' @@ -169,7 +173,7 @@ def __init__(self, system, config): acost = ' + sum(csu * vgd + csd * wgd)' srcost = ' + sum(csr @ (multiply(Rpmax, ugd) - zug))' nsrcost = ' + sum(cnsr @ multiply((1 - ugd), Rpmax))' - dcost = ' + sum(cp dot pos(gs @ pg - pds))' + dcost = ' + sum(cul dot pos(gs @ pg - pds))' self.obj.e_str = gcost + acost + srcost + nsrcost + dcost def _initial_guess(self): @@ -221,6 +225,9 @@ class UC(UCData, UCModel): DC-based unit commitment (UC). The bilinear term in the formulation is linearized with big-M method. + Penalty for unserved load is introduced as ``config.cul`` (:math:`c_{ul, cfg}`), + 1000 [$/p.u.] by default. + Constraints include power balance, ramping, spinning reserve, non-spinning reserve, minimum ON/OFF duration. The cost inludes generation cost, startup cost, shutdown cost, spinning reserve cost, @@ -231,9 +238,9 @@ class UC(UCData, UCModel): Notes ----- - 1. Formulations has been adjusted with interval ``config.t``, 1 [Hour] by default. + 1. Formulations has been adjusted with interval ``config.t`` - 2. The tie-line flow has not been implemented in formulations. + 3. The tie-line flow has not been implemented in formulations. References ---------- @@ -259,7 +266,6 @@ def __init__(self, system, config): UCData.__init__(self) UCModel.__init__(self, system, config) ESD1Base.__init__(self) - self.config.t = 1 # dispatch interval in hour self.info = 'unit commitment with energy storage' self.type = 'DCUC' From d8f0a722507c1dacd0d2fc57405a9f0204635be1 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 28 Oct 2023 08:43:41 -0400 Subject: [PATCH 06/24] Fix RTED config --- ams/routines/rted.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ams/routines/rted.py b/ams/routines/rted.py index 7737e3a1..c79aa0d9 100644 --- a/ams/routines/rted.py +++ b/ams/routines/rted.py @@ -61,7 +61,13 @@ class RTEDModel(DCOPFModel): def __init__(self, system, config): DCOPFModel.__init__(self, system, config) - self.config.t = 5/60 # time interval in hours + + self.config.add(OrderedDict((('t', 5/60), + ))) + self.config.add_extra("_help", + t="time interval in hours", + ) + self.map1 = OrderedDict([ ('StaticGen', { 'pg0': 'p', From 586889ca3794f59b78f5aaa510bce7a6c1031c40 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 28 Oct 2023 09:07:54 -0400 Subject: [PATCH 07/24] Fix matpower io --- ams/io/matpower.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ams/io/matpower.py b/ams/io/matpower.py index 731b5258..c359abdf 100644 --- a/ams/io/matpower.py +++ b/ams/io/matpower.py @@ -294,8 +294,9 @@ def system2mpc(system) -> dict: # --- zone --- ZONE_I = system.Region.idx.v - mapping = {busi0: i for i, busi0 in enumerate(ZONE_I)} - bus[:, 10] = np.array([mapping[busi0] for busi0 in system.Bus.zone.v]) + if len(ZONE_I) > 0: + mapping = {busi0: i for i, busi0 in enumerate(ZONE_I)} + bus[:, 10] = np.array([mapping[busi0] for busi0 in system.Bus.zone.v]) # --- PQ --- if system.PQ.n > 0: @@ -366,6 +367,8 @@ def system2mpc(system) -> dict: gencost[:, 4] = system.GCost.c2.v / base_mva / base_mva gencost[:, 5] = system.GCost.c1.v / base_mva gencost[:, 6] = system.GCost.c0.v / base_mva + else: + mpc.pop('gencost') mpc['bus_name'] = np.array(system.Bus.name.v) From 63b16c38e8c2d6d95b969499c2e776d8dcd0e425 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 2 Nov 2023 19:39:47 -0400 Subject: [PATCH 08/24] Fix kwargs --- ams/pypower/routines/pflow.py | 1 + ams/routines/dcpf.py | 7 ++- ams/routines/pflow.py | 106 ++++++++++++++++++---------------- 3 files changed, 62 insertions(+), 52 deletions(-) diff --git a/ams/pypower/routines/pflow.py b/ams/pypower/routines/pflow.py index 7ba10d26..4516106a 100644 --- a/ams/pypower/routines/pflow.py +++ b/ams/pypower/routines/pflow.py @@ -138,6 +138,7 @@ def runpf(casedata, ppopt): method_map = {1: 'Newton', 2: 'fast-decoupled, XB', 3: 'fast-decoupled, BX', 4: 'Gauss-Seidel'} alg = method_map.get(ppopt['PF_ALG']) + sstats['solver_name'] = f'PYPOWER-{alg}' logger.debug(f"Solution method: {alg}'s method.") if alg is None: logger.debug('Only Newton\'s method, fast-decoupled, and ' diff --git a/ams/routines/dcpf.py b/ams/routines/dcpf.py index d3be3c78..2088b6e6 100644 --- a/ams/routines/dcpf.py +++ b/ams/routines/dcpf.py @@ -99,13 +99,14 @@ def unpack(self, res): self.system.recent = self.system.routines[self.class_name] return True - def solve(self, method=None, **kwargs): + def solve(self, method=None): """ Solve DC power flow using PYPOWER. """ ppc = system2ppc(self.system) ppopt = ppoption(PF_DC=True) - res, success, sstats = runpf(casedata=ppc, ppopt=ppopt, **kwargs) + + res, success, sstats = runpf(casedata=ppc, ppopt=ppopt) return res, success, sstats def run(self, force_init=False, no_code=True, @@ -135,7 +136,7 @@ def run(self, force_init=False, no_code=True, if not self.initialized: self.init(force=force_init, no_code=no_code) t0, _ = elapsed() - res, success, sstats = self.solve(method=method, **kwargs) + res, success, sstats = self.solve(method=method) self.exit_code = 0 if success else 1 _, s = elapsed(t0) self.exec_time = float(s.split(' ')[0]) diff --git a/ams/routines/pflow.py b/ams/routines/pflow.py index d953b614..949a5231 100644 --- a/ams/routines/pflow.py +++ b/ams/routines/pflow.py @@ -22,13 +22,14 @@ class PFlowData(DCPFlowData): def __init__(self): DCPFlowData.__init__(self) - self.qd = RParam(info='reactive power load in system base', - name='qd', - src='q0', - tex_name=r'q_{d}', - unit='p.u.', - model='PQ', - ) + self.qd = RParam( + info="reactive power load in system base", + name="qd", + src="q0", + tex_name=r"q_{d}", + unit="p.u.", + model="PQ", + ) class PFlowModel(DCPFlowBase): @@ -38,48 +39,53 @@ class PFlowModel(DCPFlowBase): def __init__(self, system, config): DCPFlowBase.__init__(self, system, config) - self.info = 'AC Power Flow' - self.type = 'PF' + self.info = "AC Power Flow" + self.type = "PF" # --- bus --- - self.aBus = Var(info='bus voltage angle', - unit='rad', - name='aBus', - src='a', - tex_name=r'a_{Bus}', - model='Bus', - ) - self.vBus = Var(info='bus voltage magnitude', - unit='p.u.', - name='vBus', - src='v', - tex_name=r'v_{Bus}', - model='Bus', - ) + self.aBus = Var( + info="bus voltage angle", + unit="rad", + name="aBus", + src="a", + tex_name=r"a_{Bus}", + model="Bus", + ) + self.vBus = Var( + info="bus voltage magnitude", + unit="p.u.", + name="vBus", + src="v", + tex_name=r"v_{Bus}", + model="Bus", + ) # --- gen --- - self.pg = Var(info='active power generation', - unit='p.u.', - name='pg', - src='p', - tex_name=r'p_{g}', - model='StaticGen', - ) - self.qg = Var(info='reactive power generation', - unit='p.u.', - name='qg', - src='q', - tex_name=r'q_{g}', - model='StaticGen', - ) + self.pg = Var( + info="active power generation", + unit="p.u.", + name="pg", + src="p", + tex_name=r"p_{g}", + model="StaticGen", + ) + self.qg = Var( + info="reactive power generation", + unit="p.u.", + name="qg", + src="q", + tex_name=r"q_{g}", + model="StaticGen", + ) # --- constraints --- - self.pb = Constraint(name='pb', - info='power balance', - e_str='sum(pl) - sum(pg)', - type='eq', - ) + self.pb = Constraint( + name="pb", + info="power balance", + e_str="sum(pl) - sum(pg)", + type="eq", + ) # TODO: AC power flow formulation - def solve(self, method='newton', **kwargs): + def solve(self, method="newton"): """ Solve the AC power flow using PYPOWER. """ @@ -95,11 +101,10 @@ def solve(self, method='newton', **kwargs): raise ValueError(msg) ppopt = ppoption(PF_ALG=alg) - res, success, sstats = runpf(casedata=ppc, ppopt=ppopt, **kwargs) + res, success, sstats = runpf(casedata=ppc, ppopt=ppopt) return res, success, sstats - def run(self, force_init=False, no_code=True, - method='newton', **kwargs): + def run(self, force_init=False, no_code=True, method="newton", **kwargs): """ Run AC power flow using PYPOWER. @@ -129,9 +134,12 @@ def run(self, force_init=False, no_code=True, exit_code : int Exit code of the routine. """ - super().run(force_init=force_init, - no_code=no_code, method=method, - **kwargs, ) + return super().run( + force_init=force_init, + no_code=no_code, + method=method, + **kwargs, + ) class PFlow(PFlowData, PFlowModel): From 29078c5ed1f866fdacccc9c2a6be0fa9b4f08b07 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 2 Nov 2023 19:46:23 -0400 Subject: [PATCH 09/24] Import andes.io.dump to ams.io --- ams/io/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ams/io/__init__.py b/ams/io/__init__.py index af70dd74..c24061cb 100644 --- a/ams/io/__init__.py +++ b/ams/io/__init__.py @@ -8,6 +8,7 @@ import os from andes.utils.misc import elapsed +from andes.io import dump # NOQA from ams.io import xlsx, psse, matpower, pypower, json # NOQA From 00fe7d88f42c68df126ee7fdd0805d9ecc9bee8c Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 3 Nov 2023 13:17:07 -0400 Subject: [PATCH 10/24] Rename ESD1 related vars for clarity --- ams/routines/ed.py | 6 +++--- ams/routines/rted.py | 40 ++++++++++++++++++++-------------------- ams/routines/uc.py | 8 ++++---- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/ams/routines/ed.py b/ams/routines/ed.py index 588ac447..0d052bfa 100644 --- a/ams/routines/ed.py +++ b/ams/routines/ed.py @@ -216,6 +216,6 @@ def __init__(self, system, config): self.type = 'DCED' self.SOC.horizon = self.timeslot - self.pec.horizon = self.timeslot - self.uc.horizon = self.timeslot - self.zc.horizon = self.timeslot + self.pge.horizon = self.timeslot + self.ued.horizon = self.timeslot + self.zue.horizon = self.timeslot diff --git a/ams/routines/rted.py b/ams/routines/rted.py index c79aa0d9..9ad82601 100644 --- a/ams/routines/rted.py +++ b/ams/routines/rted.py @@ -261,7 +261,7 @@ def __init__(self): tex_name=r'\eta_d', unit='%', model='ESD1',) self.genE = RParam(info='gen of ESD1', - name='genE', tex_name=r'g_{ESD1}', + name='genE', tex_name=r'g_{ES}', model='ESD1', src='gen',) # --- service --- @@ -280,22 +280,22 @@ def __init__(self): name='SOC', tex_name=r'SOC', model='ESD1', pos=True,) self.ce = VarSelect(u=self.pg, indexer='genE', - name='ce', tex_name=r'C_{ESD1}', - info='Select ESD1 pg from StaticGen',) - self.pec = Var(info='ESD1 charging power (system base)', - unit='p.u.', name='pec', tex_name=r'p_{c,ESD1}', + name='ce', tex_name=r'C_{ES}', + info='Select pge from pg',) + self.pge = Var(info='ESD1 output power (system base)', + unit='p.u.', name='pge', tex_name=r'p_{g,ES}', model='ESD1',) - self.uc = Var(info='ESD1 charging decision', - name='uc', tex_name=r'u_{c}', - model='ESD1', boolean=True,) - self.zc = Var(info='Aux var for ESD1 charging', - name='zc', tex_name=r'z_{c}', - model='ESD1', pos=True,) + self.ued = Var(info='ESD1 commitment decision', + name='ued', tex_name=r'u_{ES,d}', + model='ESD1', boolean=True,) + self.zue = Var(info='Aux var, :math:`z_{ue} = u_{e,d} * p_{g,ES}`', + name='zue', tex_name=r'z_{ue}', + model='ESD1', pos=True,) # --- constraints --- self.cpge = Constraint(name='cpge', type='eq', info='Select ESD1 power from StaticGen', - e_str='multiply(ce, pg) - zc',) + e_str='multiply(ce, pg) - zue',) self.SOClb = Constraint(name='SOClb', type='uq', info='ESD1 SOC lower bound', @@ -304,15 +304,15 @@ def __init__(self): info='ESD1 SOC upper bound', e_str='SOC - SOCmax',) - self.zclb = Constraint(name='zclb', type='uq', info='zc lower bound', - e_str='- zc + pec',) - self.zcub = Constraint(name='zcub', type='uq', info='zc upper bound', - e_str='zc - pec - Mb dot (1-uc)',) - self.zcub2 = Constraint(name='zcub2', type='uq', info='zc upper bound', - e_str='zc - Mb dot uc',) + self.zclb = Constraint(name='zclb', type='uq', info='zue lower bound', + e_str='- zue + pge',) + self.zcub = Constraint(name='zcub', type='uq', info='zue upper bound', + e_str='zue - pge - Mb dot (1-ued)',) + self.zcub2 = Constraint(name='zcub2', type='uq', info='zue upper bound', + e_str='zue - Mb dot ued',) - SOCb = 'SOC - SOCinit - t dot REn * EtaC * zc' - SOCb += '- t dot REn * REtaD * (pec - zc)' + SOCb = 'SOC - SOCinit - t dot REn * EtaC * zue' + SOCb += '- t dot REn * REtaD * (pge - zue)' self.SOCb = Constraint(name='SOCb', type='eq', info='ESD1 SOC balance', e_str=SOCb,) diff --git a/ams/routines/uc.py b/ams/routines/uc.py index 0741820d..1188944a 100644 --- a/ams/routines/uc.py +++ b/ams/routines/uc.py @@ -93,7 +93,7 @@ def __init__(self, system, config): model='StaticGen', src='u', boolean=True,) - self.zug = Var(info='Aux var for ugd', + self.zug = Var(info='Aux var, :math:`z_{ug} = u_{g,d} * p_g`', horizon=self.timeslot, name='zug', tex_name=r'z_{ug}', model='StaticGen', pos=True,) @@ -271,6 +271,6 @@ def __init__(self, system, config): self.type = 'DCUC' self.SOC.horizon = self.timeslot - self.pec.horizon = self.timeslot - self.uc.horizon = self.timeslot - self.zc.horizon = self.timeslot + self.pge.horizon = self.timeslot + self.ued.horizon = self.timeslot + self.zue.horizon = self.timeslot From fb7e701593214a25f961e44dec55a2fbf907c26d Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 3 Nov 2023 13:26:16 -0400 Subject: [PATCH 11/24] Add as example system --- ams/system.py | 88 ++++++++++++++++----------------------------------- 1 file changed, 28 insertions(+), 60 deletions(-) diff --git a/ams/system.py b/ams/system.py index ec07b6d3..9201c711 100644 --- a/ams/system.py +++ b/ams/system.py @@ -1,32 +1,31 @@ """ Module for system. """ -import configparser -import importlib # NOQA -import inspect # NOQA -import logging # NOQA -from collections import OrderedDict # NOQA +import importlib +import inspect +import logging +from collections import OrderedDict from typing import Dict, Optional, Tuple, Union # NOQA -import numpy as np # NOQA +import numpy as np -from andes.core import Config # NOQA -from andes.system import System as andes_System # NOQA -from andes.system import (_config_numpy, load_config_rc) # NOQA -from andes.variables import FileMan # NOQA +from andes.core import Config +from andes.system import System as andes_System +from andes.system import (_config_numpy, load_config_rc) +from andes.variables import FileMan -from andes.utils.misc import elapsed # NOQA -from andes.utils.tab import Tab # NOQA +from andes.utils.misc import elapsed +from andes.utils.tab import Tab from andes.shared import pd # NOQA -import ams.io # NOQA -from ams.models.group import GroupBase # NOQA -from ams.routines.type import TypeBase # NOQA -from ams.models import file_classes # NOQA -from ams.routines import all_routines # NOQA -from ams.utils.paths import get_config_path # NOQA -from ams.core.matprocessor import MatProcessor # NOQA -from ams.interop.andes import to_andes # NOQA +import ams.io +from ams.models.group import GroupBase +from ams.routines.type import TypeBase +from ams.models import file_classes +from ams.routines import all_routines +from ams.utils.paths import get_config_path +from ams.core.matprocessor import MatProcessor +from ams.interop.andes import to_andes logger = logging.getLogger(__name__) @@ -128,7 +127,7 @@ def __init__(self, 'save_config', 'collect_config', 'e_clear', 'f_update', 'fg_to_dae', 'from_ipysheet', 'g_islands', 'g_update', 'get_z', 'init', 'j_islands', 'j_update', 'l_update_eq', 'connectivity', 'summary', - 'l_update_var', 'precompile', 'prepare', 'reload', 'remove_pycapsule', 'reset', + 'l_update_var', 'precompile', 'prepare', 'reload', 'remove_pycapsule', 's_update_post', 's_update_var', 'store_adder_setter', 'store_no_check_init', 'store_sparse_pattern', 'store_switch_times', 'switch_action', 'to_ipysheet', 'undill'] @@ -374,6 +373,13 @@ def collect_ref(self): from_idx=model_idx, to_idx=dest_idx) + def reset(self, force=False): + """ + Reset to the state after reading data and setup. + """ + self.is_setup = False + self.setup() + def setup(self): """ Set up system for studies. @@ -524,45 +530,7 @@ def to_andes(self, setup=True, addfile=None, overwite=None, no_keep=True, # --------------- Helper Functions --------------- - -def _config_numpy(seed='None', divide='warn', invalid='warn'): - """ - Configure NumPy based on Config. - """ - - # set up numpy random seed - if isinstance(seed, int): - np.random.seed(seed) - logger.debug("Random seed set to <%d>.", seed) - - # set levels - np.seterr(divide=divide, - invalid=invalid, - ) - - -def load_config_rc(conf_path=None): - """ - Load config from an rc-formatted file. - - Parameters - ---------- - conf_path : None or str - Path to the config file. If is `None`, the function body will not - run. - - Returns - ------- - configparse.ConfigParser - """ - if conf_path is None: - return - - conf = configparser.ConfigParser() - conf.read(conf_path) - logger.info('> Loaded config from file "%s"', conf_path) - return conf - +# NOTE: _config_numpy, load_config_rc are imported from andes.system def example(setup=True, no_output=True, **kwargs): """ From ce92805b9b88d9076de47429528cb496a4ab4fdd Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 3 Nov 2023 13:50:11 -0400 Subject: [PATCH 12/24] Fit to selftest --- ams/__init__.py | 5 +- ams/cli.py | 45 ++++++-- ams/main.py | 289 +++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 329 insertions(+), 10 deletions(-) diff --git a/ams/__init__.py b/ams/__init__.py index 88a26831..240873a2 100644 --- a/ams/__init__.py +++ b/ams/__init__.py @@ -1,8 +1,6 @@ from . import _version __version__ = _version.get_versions()['version'] -from ams.main import config_logger, load # NOQA -from ams.utils.paths import get_case # NOQA from ams import io # NOQA from ams import utils # NOQA from ams import models # NOQA @@ -11,6 +9,9 @@ from ams import opt # NOQA from ams import pypower # NOQA +from ams.main import config_logger, load, run # NOQA +from ams.utils.paths import get_case # NOQA + __author__ = 'Jining Wang' __all__ = ['io', 'utils', 'models', 'system'] diff --git a/ams/cli.py b/ams/cli.py index 25ce504d..1f03322c 100644 --- a/ams/cli.py +++ b/ams/cli.py @@ -1,15 +1,18 @@ """ AMS command-line interface and argument parsers. """ -import logging # NOQA -import argparse # NOQA -import importlib # NOQA -import platform # NOQA -import sys # NOQA -from time import strftime # NOQA +import logging +import argparse +import importlib +import platform +import sys +from time import strftime + +from andes.shared import NCPUS_PHYSICAL from ams.main import config_logger, find_log_path # NOQA -from ams.utils.paths import get_log_dir # NOQA +from ams.utils.paths import get_log_dir +from ams.routines import routine_cli logger = logging.getLogger(__name__) @@ -45,6 +48,33 @@ def create_parser(): '[selftest] run self test; ' ) + run = sub_parsers.add_parser('run') + run.add_argument('filename', help='Case file name. Power flow is calculated by default.', nargs='*') + run.add_argument('-r', '--routine', nargs='*', default=('pflow', ), + action='store', help='Simulation routine(s). Single routine or multiple separated with ' + 'space. Run PFlow by default.', + choices=list(routine_cli.keys())) + run.add_argument('-p', '--input-path', help='Path to case files', type=str, default='') + run.add_argument('-a', '--addfile', help='Additional files used by some formats.') + run.add_argument('-o', '--output-path', help='Output path prefix', type=str, default='') + run.add_argument('-n', '--no-output', help='Force no output of any kind', action='store_true') + run.add_argument('--ncpu', help='Number of parallel processes', type=int, default=NCPUS_PHYSICAL) + run.add_argument('-c', '--convert', help='Convert to format.', type=str, default='', nargs='?') + run.add_argument('-b', '--add-book', help='Add a template workbook for the specified model.', type=str) + run.add_argument('--convert-all', help='Convert to format with all templates.', type=str, default='', + nargs='?') + run.add_argument('--profile', action='store_true', help='Enable Python cProfiler') + run.add_argument('-s', '--shell', action='store_true', help='Start in IPython shell') + run.add_argument('--no-preamble', action='store_true', help='Hide preamble') + run.add_argument('--no-pbar', action='store_true', help='Hide progress bar for time-domain') + run.add_argument('--pool', action='store_true', help='Start multiprocess with Pool ' + 'and return a list of Systems') + run.add_argument('--from-csv', help='Use data from a CSV file instead of from simulation') + run.add_argument('-O', '--config-option', + help='Set configuration option specificied by ' + 'NAME.FIELD=VALUE with no space. For example, "TDS.tf=2"', + type=str, default='', nargs='*') + doc = sub_parsers.add_parser('doc') # TODO: fit to AMS # doc.add_argument('attribute', help='System attribute name to get documentation', nargs='?') @@ -69,6 +99,7 @@ def create_parser(): type=str, default='', nargs='*') misc.add_argument('--version', action='store_true', help='Display version information') + # TODO: add quick or extra options for selftest selftest = sub_parsers.add_parser('selftest', aliases=command_aliases['selftest']) return parser diff --git a/ams/main.py b/ams/main.py index c0fe087d..e5a4c21b 100644 --- a/ams/main.py +++ b/ams/main.py @@ -2,20 +2,26 @@ Main entry point for the AMS CLI and scripting interfaces. """ +import cProfile +import io import logging # NOQA import os # NOQA import platform # NOQA +import pstats import sys +from functools import partial from subprocess import call # NOQA +from time import sleep from typing import Optional, Union # NOQA from ._version import get_versions from andes.main import _find_cases # NOQA -from andes.shared import coloredlogs, unittest # NOQA +from andes.shared import Pool, Process, coloredlogs, unittest, NCPUS_PHYSICAL # NOQA from andes.utils.misc import elapsed, is_interactive # NOQA import ams # NOQA +from ams.routines import routine_cli from ams.system import System # NOQA from ams.utils.paths import get_config_path, get_log_dir, tests_root # NOQA @@ -145,6 +151,287 @@ def load(case, setup=True, return system +def run_case(case, *, routine='pflow', profile=False, + convert='', convert_all='', add_book=None, + **kwargs): + """ + Run single simulation case for the given full path. + Use ``run`` instead of ``run_case`` whenever possible. + + Argument ``input_path`` will not be prepended to ``case``. + + Arguments recognizable by ``load`` can be passed to ``run_case``. + + Parameters + ---------- + case : str + Full path to the test case + routine : str, ('pflow', 'tds', 'eig') + Computation routine to run + profile : bool, optional + True to enable profiler + convert : str, optional + Format name for case file conversion. + convert_all : str, optional + Format name for case file conversion, output + sheets for all available devices. + add_book : str, optional + Name of the device to be added to an excel case + as a new sheet. + """ + + pr = cProfile.Profile() + # enable profiler if requested + if profile is True: + pr.enable() + + system = load(case, + use_input_path=False, + **kwargs) + + if system is None: + return None + + skip_empty = True + overwrite = None + # convert to xlsx and process `add-book` option + if add_book is not None: + convert = 'xlsx' + overwrite = True + if convert_all != '': + convert = 'xlsx' + skip_empty = False + + # convert to the requested format + if convert != '': + ams.io.dump(system, convert, overwrite=overwrite, skip_empty=skip_empty, + add_book=add_book) + return system + + # run the requested routine + if routine is not None: + if isinstance(routine, str): + routine = [routine] + + if system.is_setup: + for r in routine: + system.__dict__[routine_cli[r.lower()]].run(**kwargs) + else: + logger.error("System is not set up. Routines cannot continue.") + + # Disable profiler and output results + if profile: + pr.disable() + + if system.files.no_output: + nlines = 40 + s = io.StringIO() + ps = pstats.Stats(pr, stream=sys.stdout).sort_stats('cumtime') + ps.print_stats(nlines) + logger.info(s.getvalue()) + s.close() + else: + nlines = 999 + with open(system.files.prof, 'w') as s: + ps = pstats.Stats(pr, stream=s).sort_stats('cumtime') + ps.print_stats(nlines) + ps.dump_stats(system.files.prof_raw) + logger.info('cProfile text data written to "%s".', system.files.prof) + logger.info('cProfile raw data written to "%s". View with tool `snakeviz`.', system.files.prof_raw) + + return system + + +def _run_mp_proc(cases, ncpu=NCPUS_PHYSICAL, **kwargs): + """ + Run multiprocessing with `Process`. + + Return values from `run_case` are not preserved. Always return `True` when done. + """ + + # start processes + jobs = [] + for idx, file in enumerate(cases): + job = Process(name=f'Process {idx:d}', target=run_case, args=(file,), kwargs=kwargs) + jobs.append(job) + job.start() + start_msg = f'Process {idx:d} for "{file:s}" started.' + print(start_msg) + logger.debug(start_msg) + if (idx % ncpu == ncpu - 1) or (idx == len(cases) - 1): + sleep(0.1) + for job in jobs: + job.join() + jobs = [] + + return True + + +def _run_mp_pool(cases, ncpu=NCPUS_PHYSICAL, verbose=logging.INFO, **kwargs): + """ + Run multiprocessing jobs using Pool. + + This function returns all System instances in a list, but requires longer computation time. + + Parameters + ---------- + ncpu : int, optional = os.cpu_cout() + Number of cpu cores to use in parallel + mp_verbose : 10 - 50 + Verbosity level during multiprocessing + verbose : 10, 20, 30, 40, 50 + Verbosity level outside multiprocessing + """ + + pool = Pool(ncpu) + print("Cases are processed in the following order:") + print('\n'.join([f'"{name}"' for name in cases])) + + ret = pool.map(partial(run_case, + verbose=verbose, + remove_pycapsule=True, + autogen_stale=False, + **kwargs), + cases) + + # FIXME: does following code work in AMS? + # # fix address for in-place arrays + # for ss in ret: + # fix_view_arrays(ss) + + return ret + + +def run(filename, input_path='', verbose=20, mp_verbose=30, + ncpu=NCPUS_PHYSICAL, pool=False, + cli=False, shell=False, **kwargs): + """ + Entry point to run ANDES routines. + + Parameters + ---------- + filename : str + file name (or pattern) + input_path : str, optional + input search path + verbose : int, 10 (DEBUG), 20 (INFO), 30 (WARNING), 40 (ERROR), 50 (CRITICAL) + Verbosity level. If ``config_logger`` is called prior to ``run``, + this option will be ignored. + mp_verbose : int + Verbosity level for multiprocessing tasks + ncpu : int, optional + Number of cpu cores to use in parallel + pool: bool, optional + Use Pool for multiprocessing to return a list of created Systems. + kwargs + Other supported keyword arguments + cli : bool, optional + If is running from command-line. If True, returns exit code instead of System + shell : bool, optional + If True, enter IPython shell after routine. + + Returns + ------- + System or exit_code + An instance of system (if `cli == False`) or an exit code otherwise.. + + """ + + if is_interactive() and len(logger.handlers) == 0: + config_logger(verbose, file=False) + + # put some args back to `kwargs` + kwargs['input_path'] = input_path + kwargs['verbose'] = verbose + + cases = _find_cases(filename, input_path) + + system = None + ex_code = 0 + + if len(filename) > 0 and len(cases) == 0: + ex_code = 1 # file specified but not found + + t0, _ = elapsed() + if len(cases) == 1: + system = run_case(cases[0], **kwargs) + elif len(cases) > 1: + # FIXME: after standardize code generation, enable following code + # # import `pycode` to local namespace to avoid a picking issue + # import_pycode() + + # suppress logging output during multiprocessing + logger.info('-> Processing %s jobs on %s CPUs.', len(cases), ncpu) + set_logger_level(logger, logging.StreamHandler, mp_verbose) + set_logger_level(logger, logging.FileHandler, logging.DEBUG) + + if pool is True: + system = _run_mp_pool(cases, + ncpu=ncpu, + mp_verbose=mp_verbose, + **kwargs) + else: + system = _run_mp_proc(cases, + ncpu=ncpu, + mp_verbose=mp_verbose, + **kwargs) + + # restore command line output when all jobs are done + set_logger_level(logger, logging.StreamHandler, verbose) + + log_files = find_log_path(logger) + if len(log_files) > 0: + log_paths = '\n'.join(log_files) + print(f'Log saved to "{log_paths}".') + + t0, s0 = elapsed(t0) + + if len(cases) == 1: + if system is not None: + ex_code += system.exit_code + else: + ex_code += 1 + elif len(cases) > 1: + if isinstance(system, list): + for s in system: + ex_code += s.exit_code + + if len(cases) == 1: + if ex_code == 0: + print(f'-> Single process finished in {s0}.') + else: + print(f'-> Single process exit with an error in {s0}.') + elif len(cases) > 1: + if ex_code == 0: + print(f'-> Multiprocessing finished in {s0}.') + else: + print(f'-> Multiprocessing exit with an error in {s0}.') + + # IPython interactive shell + if shell is True: + try: + from IPython import embed + + # load plotter before entering IPython + if system is None: + logger.warning("IPython: The System object has not been created.") + elif isinstance(system, System): + logger.info("IPython: Access System object in variable `system`.") + system.TDS.load_plotter() + elif isinstance(system, list): + logger.warning("IPython: System objects stored in list `system`.\n" + "Call `TDS.load_plotter()` on each for plotter.") + + embed() + except ImportError: + logger.warning("IPython import error. Installed?") + + if cli is True: + return ex_code + + return system + + def set_logger_level(lg, type_to_set, level): """ Set logging level for the given type of handler. From 321cf59ff99f130b3d16565ebd07e6581069f7cb Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 3 Nov 2023 13:50:29 -0400 Subject: [PATCH 13/24] Add cases test --- ams/cases/ieee14/ieee14.json | 1166 ++++++++++++++++++++++++++++++++++ tests/test_case.py | 223 +++++++ 2 files changed, 1389 insertions(+) create mode 100644 ams/cases/ieee14/ieee14.json create mode 100644 tests/test_case.py diff --git a/ams/cases/ieee14/ieee14.json b/ams/cases/ieee14/ieee14.json new file mode 100644 index 00000000..3a1527af --- /dev/null +++ b/ams/cases/ieee14/ieee14.json @@ -0,0 +1,1166 @@ +{ + "Bus": [ + { + "idx": 1, + "u": 1.0, + "name": "BUS1", + "Vn": 69.0, + "vmax": 1.1, + "vmin": 0.9, + "v0": 1.03, + "a0": 0.0, + "xcoord": 0, + "ycoord": 0, + "area": 1, + "zone": 1, + "owner": 1 + }, + { + "idx": 2, + "u": 1.0, + "name": "BUS2", + "Vn": 69.0, + "vmax": 1.1, + "vmin": 0.9, + "v0": 1.0197, + "a0": -0.02798111856797309, + "xcoord": 0, + "ycoord": 0, + "area": 1, + "zone": 1, + "owner": 1 + }, + { + "idx": 3, + "u": 1.0, + "name": "BUS3", + "Vn": 69.0, + "vmax": 1.1, + "vmin": 0.9, + "v0": 1.00042, + "a0": -0.06009692213392075, + "xcoord": 0, + "ycoord": 0, + "area": 1, + "zone": 1, + "owner": 1 + }, + { + "idx": 4, + "u": 1.0, + "name": "BUS4", + "Vn": 69.0, + "vmax": 1.1, + "vmin": 0.9, + "v0": 0.99858, + "a0": -0.07472103593638124, + "xcoord": 0, + "ycoord": 0, + "area": 1, + "zone": 1, + "owner": 1 + }, + { + "idx": 5, + "u": 1.0, + "name": "BUS5", + "Vn": 69.0, + "vmax": 1.1, + "vmin": 0.9, + "v0": 1.00443, + "a0": -0.06431538293599104, + "xcoord": 0, + "ycoord": 0, + "area": 1, + "zone": 1, + "owner": 1 + }, + { + "idx": 6, + "u": 1.0, + "name": "BUS6", + "Vn": 138.0, + "vmax": 1.1, + "vmin": 0.9, + "v0": 0.99871, + "a0": -0.10999763077769062, + "xcoord": 0, + "ycoord": 0, + "area": 2, + "zone": 2, + "owner": 2 + }, + { + "idx": 7, + "u": 1.0, + "name": "BUS7", + "Vn": 138.0, + "vmax": 1.1, + "vmin": 0.9, + "v0": 1.00682, + "a0": -0.08428544023731016, + "xcoord": 0, + "ycoord": 0, + "area": 2, + "zone": 2, + "owner": 2 + }, + { + "idx": 8, + "u": 1.0, + "name": "BUS8", + "Vn": 69.0, + "vmax": 1.1, + "vmin": 0.9, + "v0": 1.01895, + "a0": -0.024338616419060925, + "xcoord": 0, + "ycoord": 0, + "area": 2, + "zone": 2, + "owner": 2 + }, + { + "idx": 9, + "u": 1.0, + "name": "BUS9", + "Vn": 138.0, + "vmax": 1.1, + "vmin": 0.9, + "v0": 1.00193, + "a0": -0.12750153784594176, + "xcoord": 0, + "ycoord": 0, + "area": 2, + "zone": 2, + "owner": 2 + }, + { + "idx": 10, + "u": 1.0, + "name": "BUS10", + "Vn": 138.0, + "vmax": 1.1, + "vmin": 0.9, + "v0": 0.99351, + "a0": -0.130201562198777, + "xcoord": 0, + "ycoord": 0, + "area": 2, + "zone": 2, + "owner": 2 + }, + { + "idx": 11, + "u": 1.0, + "name": "BUS11", + "Vn": 138.0, + "vmax": 1.1, + "vmin": 0.9, + "v0": 0.99245, + "a0": -0.12294797382748855, + "xcoord": 0, + "ycoord": 0, + "area": 2, + "zone": 2, + "owner": 2 + }, + { + "idx": 12, + "u": 1.0, + "name": "BUS12", + "Vn": 138.0, + "vmax": 1.1, + "vmin": 0.9, + "v0": 0.98639, + "a0": -0.1289344531618291, + "xcoord": 0, + "ycoord": 0, + "area": 2, + "zone": 2, + "owner": 2 + }, + { + "idx": 13, + "u": 1.0, + "name": "BUS13", + "Vn": 138.0, + "vmax": 1.1, + "vmin": 0.9, + "v0": 0.98403, + "a0": -0.13378646848237333, + "xcoord": 0, + "ycoord": 0, + "area": 2, + "zone": 2, + "owner": 2 + }, + { + "idx": 14, + "u": 1.0, + "name": "BUS14", + "Vn": 138.0, + "vmax": 1.1, + "vmin": 0.9, + "v0": 0.99063, + "a0": -0.1669163083437297, + "xcoord": 0, + "ycoord": 0, + "area": 2, + "zone": 2, + "owner": 2 + } + ], + "PQ": [ + { + "idx": "PQ_1", + "u": 1.0, + "name": "PQ_1", + "bus": 2, + "Vn": 69.0, + "p0": 0.217, + "q0": 0.127, + "vmax": 1.2, + "vmin": 0.8, + "owner": 1 + }, + { + "idx": "PQ_2", + "u": 1.0, + "name": "PQ_2", + "bus": 3, + "Vn": 69.0, + "p0": 0.5, + "q0": 0.25, + "vmax": 1.2, + "vmin": 0.8, + "owner": 1 + }, + { + "idx": "PQ_3", + "u": 1.0, + "name": "PQ_3", + "bus": 4, + "Vn": 69.0, + "p0": 0.478, + "q0": 0.1, + "vmax": 1.2, + "vmin": 0.8, + "owner": 1 + }, + { + "idx": "PQ_4", + "u": 1.0, + "name": "PQ_4", + "bus": 5, + "Vn": 69.0, + "p0": 0.076, + "q0": 0.016, + "vmax": 1.2, + "vmin": 0.8, + "owner": 1 + }, + { + "idx": "PQ_5", + "u": 1.0, + "name": "PQ_5", + "bus": 6, + "Vn": 138.0, + "p0": 0.15, + "q0": 0.075, + "vmax": 1.2, + "vmin": 0.8, + "owner": 2 + }, + { + "idx": "PQ_6", + "u": 1.0, + "name": "PQ_6", + "bus": 9, + "Vn": 138.0, + "p0": 0.295, + "q0": 0.166, + "vmax": 1.2, + "vmin": 0.8, + "owner": 2 + }, + { + "idx": "PQ_7", + "u": 1.0, + "name": "PQ_7", + "bus": 10, + "Vn": 138.0, + "p0": 0.09, + "q0": 0.057999999999999996, + "vmax": 1.2, + "vmin": 0.8, + "owner": 2 + }, + { + "idx": "PQ_8", + "u": 1.0, + "name": "PQ_8", + "bus": 11, + "Vn": 138.0, + "p0": 0.035, + "q0": 0.018000000000000002, + "vmax": 1.2, + "vmin": 0.8, + "owner": 2 + }, + { + "idx": "PQ_9", + "u": 1.0, + "name": "PQ_9", + "bus": 12, + "Vn": 138.0, + "p0": 0.061, + "q0": 0.016, + "vmax": 1.2, + "vmin": 0.8, + "owner": 2 + }, + { + "idx": "PQ_10", + "u": 1.0, + "name": "PQ_10", + "bus": 13, + "Vn": 138.0, + "p0": 0.135, + "q0": 0.057999999999999996, + "vmax": 1.2, + "vmin": 0.8, + "owner": 2 + }, + { + "idx": "PQ_11", + "u": 1.0, + "name": "PQ_11", + "bus": 14, + "Vn": 138.0, + "p0": 0.2, + "q0": 0.07, + "vmax": 1.2, + "vmin": 0.8, + "owner": 2 + } + ], + "PV": [ + { + "idx": 2, + "u": 1.0, + "name": 2, + "Sn": 100.0, + "Vn": 69.0, + "bus": 2, + "busr": null, + "p0": 0.4, + "q0": 0.15, + "pmax": 0.5, + "pmin": 0.1, + "qmax": 0.15, + "qmin": -0.4, + "v0": 1.03, + "vmax": 1.4, + "vmin": 0.6, + "ra": 0.0, + "xs": 0.13, + "ctrl": 1.0, + "Pc1": 0.0, + "Pc2": 0.0, + "Qc1min": 0.0, + "Qc1max": 0.0, + "Qc2min": 0.0, + "Qc2max": 0.0, + "Ragc": 999.0, + "R10": 999.0, + "R30": 999.0, + "Rq": 999.0, + "apf": 0.0, + "pg0": 0.0, + "td1": 0.0, + "td2": 0.0 + }, + { + "idx": 3, + "u": 1.0, + "name": 3, + "Sn": 100.0, + "Vn": 69.0, + "bus": 3, + "busr": null, + "p0": 0.4, + "q0": 0.15, + "pmax": 0.5, + "pmin": 0.1, + "qmax": 0.15, + "qmin": -0.1, + "v0": 1.01, + "vmax": 1.4, + "vmin": 0.6, + "ra": 0.0, + "xs": 0.13, + "ctrl": 1.0, + "Pc1": 0.0, + "Pc2": 0.0, + "Qc1min": 0.0, + "Qc1max": 0.0, + "Qc2min": 0.0, + "Qc2max": 0.0, + "Ragc": 999.0, + "R10": 999.0, + "R30": 999.0, + "Rq": 999.0, + "apf": 0.0, + "pg0": 0.0, + "td1": 0.0, + "td2": 0.0 + }, + { + "idx": 4, + "u": 1.0, + "name": 4, + "Sn": 100.0, + "Vn": 138.0, + "bus": 6, + "busr": null, + "p0": 0.3, + "q0": 0.1, + "pmax": 0.5, + "pmin": 0.1, + "qmax": 0.1, + "qmin": -0.06, + "v0": 1.03, + "vmax": 1.4, + "vmin": 0.6, + "ra": 0.0, + "xs": 0.12, + "ctrl": 1.0, + "Pc1": 0.0, + "Pc2": 0.0, + "Qc1min": 0.0, + "Qc1max": 0.0, + "Qc2min": 0.0, + "Qc2max": 0.0, + "Ragc": 999.0, + "R10": 999.0, + "R30": 999.0, + "Rq": 999.0, + "apf": 0.0, + "pg0": 0.0, + "td1": 0.0, + "td2": 0.0 + }, + { + "idx": 5, + "u": 1.0, + "name": 5, + "Sn": 100.0, + "Vn": 69.0, + "bus": 8, + "busr": null, + "p0": 0.35, + "q0": 0.1, + "pmax": 0.5, + "pmin": 0.1, + "qmax": 0.1, + "qmin": -0.06, + "v0": 1.03, + "vmax": 1.4, + "vmin": 0.6, + "ra": 0.0, + "xs": 0.12, + "ctrl": 1.0, + "Pc1": 0.0, + "Pc2": 0.0, + "Qc1min": 0.0, + "Qc1max": 0.0, + "Qc2min": 0.0, + "Qc2max": 0.0, + "Ragc": 999.0, + "R10": 999.0, + "R30": 999.0, + "Rq": 999.0, + "apf": 0.0, + "pg0": 0.0, + "td1": 0.0, + "td2": 0.0 + } + ], + "Slack": [ + { + "idx": 1, + "u": 1.0, + "name": 1, + "Sn": 100.0, + "Vn": 69.0, + "bus": 1, + "busr": null, + "p0": 0.8144199999999999, + "q0": 0.01962, + "pmax": 2.0, + "pmin": 0.5, + "qmax": 1.0, + "qmin": -0.5, + "v0": 1.03, + "vmax": 1.4, + "vmin": 0.6, + "ra": 0.0, + "xs": 0.23, + "a0": 0.0, + "ctrl": 1.0, + "Pc1": 0.0, + "Pc2": 0.0, + "Qc1min": 0.0, + "Qc1max": 0.0, + "Qc2min": 0.0, + "Qc2max": 0.0, + "Ragc": 999.0, + "R10": 999.0, + "R30": 999.0, + "Rq": 999.0, + "apf": 0.0, + "pg0": 0.0, + "td1": 0.0, + "td2": 0.0 + } + ], + "Shunt": [ + { + "idx": "Shunt_1", + "u": 1.0, + "name": "Shunt_1", + "bus": 9, + "Sn": 100.0, + "Vn": 138.0, + "g": 0.0, + "b": 0.19, + "fn": 60.0 + }, + { + "idx": "Shunt_2", + "u": 1.0, + "name": "Shunt_2", + "bus": 14, + "Sn": 100.0, + "Vn": 138.0, + "g": 0.0, + "b": 0.15, + "fn": 60.0 + } + ], + "Line": [ + { + "idx": "Line_1", + "u": 1.0, + "name": "Line_1", + "bus1": 1, + "bus2": 2, + "Sn": 100.0, + "fn": 60.0, + "Vn1": 69.0, + "Vn2": 69.0, + "r": 0.01938, + "x": 0.05917, + "b": 0.0528, + "g": 0.0, + "b1": 0.0, + "g1": 0.0, + "b2": 0.0, + "g2": 0.0, + "trans": 0.0, + "tap": 1.0, + "phi": 0.0, + "rate_a": 100.0, + "rate_b": 100.0, + "rate_c": 0.0, + "owner": null, + "xcoord": null, + "ycoord": null, + "amin": -6.283185307179586, + "amax": 6.283185307179586 + }, + { + "idx": "Line_2", + "u": 1.0, + "name": "Line_2", + "bus1": 1, + "bus2": 5, + "Sn": 100.0, + "fn": 60.0, + "Vn1": 69.0, + "Vn2": 69.0, + "r": 0.05403, + "x": 0.22304, + "b": 0.0492, + "g": 0.0, + "b1": 0.0, + "g1": 0.0, + "b2": 0.0, + "g2": 0.0, + "trans": 0.0, + "tap": 1.0, + "phi": 0.0, + "rate_a": 100.0, + "rate_b": 100.0, + "rate_c": 0.0, + "owner": null, + "xcoord": null, + "ycoord": null, + "amin": -6.283185307179586, + "amax": 6.283185307179586 + }, + { + "idx": "Line_3", + "u": 1.0, + "name": "Line_3", + "bus1": 2, + "bus2": 3, + "Sn": 100.0, + "fn": 60.0, + "Vn1": 69.0, + "Vn2": 69.0, + "r": 0.04699, + "x": 0.19797, + "b": 0.0438, + "g": 0.0, + "b1": 0.0, + "g1": 0.0, + "b2": 0.0, + "g2": 0.0, + "trans": 0.0, + "tap": 1.0, + "phi": 0.0, + "rate_a": 100.0, + "rate_b": 100.0, + "rate_c": 0.0, + "owner": null, + "xcoord": null, + "ycoord": null, + "amin": -6.283185307179586, + "amax": 6.283185307179586 + }, + { + "idx": "Line_4", + "u": 1.0, + "name": "Line_4", + "bus1": 2, + "bus2": 4, + "Sn": 100.0, + "fn": 60.0, + "Vn1": 69.0, + "Vn2": 69.0, + "r": 0.05811, + "x": 0.17632, + "b": 0.034, + "g": 0.0, + "b1": 0.0, + "g1": 0.0, + "b2": 0.0, + "g2": 0.0, + "trans": 0.0, + "tap": 1.0, + "phi": 0.0, + "rate_a": 100.0, + "rate_b": 100.0, + "rate_c": 0.0, + "owner": null, + "xcoord": null, + "ycoord": null, + "amin": -6.283185307179586, + "amax": 6.283185307179586 + }, + { + "idx": "Line_5", + "u": 1.0, + "name": "Line_5", + "bus1": 2, + "bus2": 5, + "Sn": 100.0, + "fn": 60.0, + "Vn1": 69.0, + "Vn2": 69.0, + "r": 0.05695, + "x": 0.17388, + "b": 0.0346, + "g": 0.0, + "b1": 0.0, + "g1": 0.0, + "b2": 0.0, + "g2": 0.0, + "trans": 0.0, + "tap": 1.0, + "phi": 0.0, + "rate_a": 100.0, + "rate_b": 100.0, + "rate_c": 0.0, + "owner": null, + "xcoord": null, + "ycoord": null, + "amin": -6.283185307179586, + "amax": 6.283185307179586 + }, + { + "idx": "Line_6", + "u": 1.0, + "name": "Line_6", + "bus1": 3, + "bus2": 4, + "Sn": 100.0, + "fn": 60.0, + "Vn1": 69.0, + "Vn2": 69.0, + "r": 0.06701, + "x": 0.17103, + "b": 0.0128, + "g": 0.0, + "b1": 0.0, + "g1": 0.0, + "b2": 0.0, + "g2": 0.0, + "trans": 0.0, + "tap": 1.0, + "phi": 0.0, + "rate_a": 100.0, + "rate_b": 100.0, + "rate_c": 0.0, + "owner": null, + "xcoord": null, + "ycoord": null, + "amin": -6.283185307179586, + "amax": 6.283185307179586 + }, + { + "idx": "Line_7", + "u": 1.0, + "name": "Line_7", + "bus1": 4, + "bus2": 5, + "Sn": 100.0, + "fn": 60.0, + "Vn1": 69.0, + "Vn2": 69.0, + "r": 0.01335, + "x": 0.04211, + "b": 0.0, + "g": 0.0, + "b1": 0.0, + "g1": 0.0, + "b2": 0.0, + "g2": 0.0, + "trans": 0.0, + "tap": 1.0, + "phi": 0.0, + "rate_a": 100.0, + "rate_b": 100.0, + "rate_c": 0.0, + "owner": null, + "xcoord": null, + "ycoord": null, + "amin": -6.283185307179586, + "amax": 6.283185307179586 + }, + { + "idx": "Line_8", + "u": 1.0, + "name": "Line_8", + "bus1": 6, + "bus2": 11, + "Sn": 100.0, + "fn": 60.0, + "Vn1": 138.0, + "Vn2": 138.0, + "r": 0.09498, + "x": 0.1989, + "b": 0.0, + "g": 0.0, + "b1": 0.0, + "g1": 0.0, + "b2": 0.0, + "g2": 0.0, + "trans": 0.0, + "tap": 1.0, + "phi": 0.0, + "rate_a": 100.0, + "rate_b": 100.0, + "rate_c": 0.0, + "owner": null, + "xcoord": null, + "ycoord": null, + "amin": -6.283185307179586, + "amax": 6.283185307179586 + }, + { + "idx": "Line_9", + "u": 1.0, + "name": "Line_9", + "bus1": 6, + "bus2": 12, + "Sn": 100.0, + "fn": 60.0, + "Vn1": 138.0, + "Vn2": 138.0, + "r": 0.12291, + "x": 0.25581, + "b": 0.0, + "g": 0.0, + "b1": 0.0, + "g1": 0.0, + "b2": 0.0, + "g2": 0.0, + "trans": 0.0, + "tap": 1.0, + "phi": 0.0, + "rate_a": 100.0, + "rate_b": 100.0, + "rate_c": 0.0, + "owner": null, + "xcoord": null, + "ycoord": null, + "amin": -6.283185307179586, + "amax": 6.283185307179586 + }, + { + "idx": "Line_10", + "u": 1.0, + "name": "Line_10", + "bus1": 6, + "bus2": 13, + "Sn": 100.0, + "fn": 60.0, + "Vn1": 138.0, + "Vn2": 138.0, + "r": 0.06615, + "x": 0.13027, + "b": 0.0, + "g": 0.0, + "b1": 0.0, + "g1": 0.0, + "b2": 0.0, + "g2": 0.0, + "trans": 0.0, + "tap": 1.0, + "phi": 0.0, + "rate_a": 100.0, + "rate_b": 100.0, + "rate_c": 0.0, + "owner": null, + "xcoord": null, + "ycoord": null, + "amin": -6.283185307179586, + "amax": 6.283185307179586 + }, + { + "idx": "Line_11", + "u": 1.0, + "name": "Line_11", + "bus1": 7, + "bus2": 9, + "Sn": 100.0, + "fn": 60.0, + "Vn1": 138.0, + "Vn2": 138.0, + "r": -0.0, + "x": 0.11001, + "b": 0.0, + "g": 0.0, + "b1": 0.0, + "g1": 0.0, + "b2": 0.0, + "g2": 0.0, + "trans": 0.0, + "tap": 1.0, + "phi": 0.0, + "rate_a": 100.0, + "rate_b": 100.0, + "rate_c": 0.0, + "owner": null, + "xcoord": null, + "ycoord": null, + "amin": -6.283185307179586, + "amax": 6.283185307179586 + }, + { + "idx": "Line_12", + "u": 1.0, + "name": "Line_12", + "bus1": 9, + "bus2": 10, + "Sn": 100.0, + "fn": 60.0, + "Vn1": 138.0, + "Vn2": 138.0, + "r": 0.03181, + "x": 0.0845, + "b": 0.0, + "g": 0.0, + "b1": 0.0, + "g1": 0.0, + "b2": 0.0, + "g2": 0.0, + "trans": 0.0, + "tap": 1.0, + "phi": 0.0, + "rate_a": 100.0, + "rate_b": 100.0, + "rate_c": 0.0, + "owner": null, + "xcoord": null, + "ycoord": null, + "amin": -6.283185307179586, + "amax": 6.283185307179586 + }, + { + "idx": "Line_13", + "u": 1.0, + "name": "Line_13", + "bus1": 9, + "bus2": 14, + "Sn": 100.0, + "fn": 60.0, + "Vn1": 138.0, + "Vn2": 138.0, + "r": 0.12711, + "x": 0.27038, + "b": 0.0, + "g": 0.0, + "b1": 0.0, + "g1": 0.0, + "b2": 0.0, + "g2": 0.0, + "trans": 0.0, + "tap": 1.0, + "phi": 0.0, + "rate_a": 100.0, + "rate_b": 100.0, + "rate_c": 0.0, + "owner": null, + "xcoord": null, + "ycoord": null, + "amin": -6.283185307179586, + "amax": 6.283185307179586 + }, + { + "idx": "Line_14", + "u": 1.0, + "name": "Line_14", + "bus1": 10, + "bus2": 11, + "Sn": 100.0, + "fn": 60.0, + "Vn1": 138.0, + "Vn2": 138.0, + "r": 0.08205, + "x": 0.19207, + "b": 0.0, + "g": 0.0, + "b1": 0.0, + "g1": 0.0, + "b2": 0.0, + "g2": 0.0, + "trans": 0.0, + "tap": 1.0, + "phi": 0.0, + "rate_a": 100.0, + "rate_b": 100.0, + "rate_c": 0.0, + "owner": null, + "xcoord": null, + "ycoord": null, + "amin": -6.283185307179586, + "amax": 6.283185307179586 + }, + { + "idx": "Line_15", + "u": 1.0, + "name": "Line_15", + "bus1": 12, + "bus2": 13, + "Sn": 100.0, + "fn": 60.0, + "Vn1": 138.0, + "Vn2": 138.0, + "r": 0.22092, + "x": 0.19988, + "b": 0.0, + "g": 0.0, + "b1": 0.0, + "g1": 0.0, + "b2": 0.0, + "g2": 0.0, + "trans": 0.0, + "tap": 1.0, + "phi": 0.0, + "rate_a": 100.0, + "rate_b": 100.0, + "rate_c": 0.0, + "owner": null, + "xcoord": null, + "ycoord": null, + "amin": -6.283185307179586, + "amax": 6.283185307179586 + }, + { + "idx": "Line_16", + "u": 1.0, + "name": "Line_16", + "bus1": 13, + "bus2": 14, + "Sn": 100.0, + "fn": 60.0, + "Vn1": 138.0, + "Vn2": 138.0, + "r": 0.17093, + "x": 0.34802, + "b": 0.0, + "g": 0.0, + "b1": 0.0, + "g1": 0.0, + "b2": 0.0, + "g2": 0.0, + "trans": 0.0, + "tap": 1.0, + "phi": 0.0, + "rate_a": 100.0, + "rate_b": 100.0, + "rate_c": 0.0, + "owner": null, + "xcoord": null, + "ycoord": null, + "amin": -6.283185307179586, + "amax": 6.283185307179586 + }, + { + "idx": "Line_17", + "u": 1.0, + "name": "Line_17", + "bus1": 4, + "bus2": 7, + "Sn": 100.0, + "fn": 60.0, + "Vn1": 69.0, + "Vn2": 138.0, + "r": 0.0, + "x": 0.20912, + "b": 0.0, + "g": 0.0, + "b1": 0.0, + "g1": 0.0, + "b2": 0.0, + "g2": 0.0, + "trans": 1.0, + "tap": 0.99677, + "phi": 0.0, + "rate_a": 20.0, + "rate_b": 20.0, + "rate_c": 0.0, + "owner": null, + "xcoord": null, + "ycoord": null, + "amin": -6.283185307179586, + "amax": 6.283185307179586 + }, + { + "idx": "Line_18", + "u": 1.0, + "name": "Line_18", + "bus1": 4, + "bus2": 9, + "Sn": 100.0, + "fn": 60.0, + "Vn1": 69.0, + "Vn2": 138.0, + "r": 0.0, + "x": 0.55618, + "b": 0.0, + "g": 0.0, + "b1": 0.0, + "g1": 0.0, + "b2": 0.0, + "g2": 0.0, + "trans": 1.0, + "tap": 0.99677, + "phi": 0.0, + "rate_a": 20.0, + "rate_b": 20.0, + "rate_c": 0.0, + "owner": null, + "xcoord": null, + "ycoord": null, + "amin": -6.283185307179586, + "amax": 6.283185307179586 + }, + { + "idx": "Line_19", + "u": 1.0, + "name": "Line_19", + "bus1": 6, + "bus2": 5, + "Sn": 100.0, + "fn": 60.0, + "Vn1": 138.0, + "Vn2": 69.0, + "r": 0.0, + "x": 0.25202, + "b": 0.0, + "g": 0.0, + "b1": 0.0, + "g1": 0.0, + "b2": 0.0, + "g2": 0.0, + "trans": 1.0, + "tap": 0.99677, + "phi": 0.0, + "rate_a": 50.0, + "rate_b": 50.0, + "rate_c": 0.0, + "owner": null, + "xcoord": null, + "ycoord": null, + "amin": -6.283185307179586, + "amax": 6.283185307179586 + }, + { + "idx": "Line_20", + "u": 1.0, + "name": "Line_20", + "bus1": 8, + "bus2": 7, + "Sn": 100.0, + "fn": 60.0, + "Vn1": 69.0, + "Vn2": 138.0, + "r": 0.0, + "x": 0.17615, + "b": 0.0, + "g": 0.0, + "b1": 0.0, + "g1": 0.0, + "b2": 0.0, + "g2": 0.0, + "trans": 1.0, + "tap": 0.99677, + "phi": 0.0, + "rate_a": 50.0, + "rate_b": 50.0, + "rate_c": 0.0, + "owner": null, + "xcoord": null, + "ycoord": null, + "amin": -6.283185307179586, + "amax": 6.283185307179586 + } + ], + "Area": [ + { + "idx": 1, + "u": 1.0, + "name": "AREA1" + }, + { + "idx": 2, + "u": 1.0, + "name": "AREA2" + } + ] +} \ No newline at end of file diff --git a/tests/test_case.py b/tests/test_case.py new file mode 100644 index 00000000..4a53a192 --- /dev/null +++ b/tests/test_case.py @@ -0,0 +1,223 @@ +import os +import unittest + +import numpy as np + +import ams +from ams.utils.paths import get_case + + +class Test5Bus(unittest.TestCase): + """ + Tests for the 5-bus system. + """ + + def setUp(self) -> None: + self.ss = ams.main.load( + get_case("5bus/pjm5bus_uced.xlsx"), + default_config=True, + no_output=True, + ) + + def test_essential(self): + """ + Test essential functionalities of Model and System. + """ + + # --- test model names + self.assertTrue("Bus" in self.ss.models) + self.assertTrue("PQ" in self.ss.models) + + # --- test device counts + self.assertEqual(self.ss.Bus.n, 5) + self.assertEqual(self.ss.PQ.n, 3) + self.assertEqual(self.ss.PV.n, 3) + self.assertEqual(self.ss.Slack.n, 1) + self.assertEqual(self.ss.Line.n, 7) + self.assertEqual(self.ss.Region.n, 2) + self.assertEqual(self.ss.SFR.n, 2) + self.assertEqual(self.ss.SR.n, 2) + self.assertEqual(self.ss.NSR.n, 2) + self.assertEqual(self.ss.GCost.n, 4) + self.assertEqual(self.ss.SFRCost.n, 4) + self.assertEqual(self.ss.SRCost.n, 4) + self.assertEqual(self.ss.NSRCost.n, 4) + self.assertEqual(self.ss.EDTSlot.n, 24) + self.assertEqual(self.ss.UCTSlot.n, 24) + + # test idx values + self.assertSequenceEqual(self.ss.Bus.idx.v, [0, 1, 2, 3, 4]) + self.assertSequenceEqual(self.ss.Area.idx.v, [1, 2, 3]) + + # test cache refreshing + self.ss.Bus.cache.refresh() # used in ANDES but not in AMS + + # test conversion to dataframe + self.ss.Bus.as_df() + self.ss.Bus.as_df(vin=True) + + # test conversion to dataframe of ``Horizon`` model + self.ss.EDTSlot.as_df() + self.ss.EDTSlot.as_df(vin=True) + + self.ss.UCTSlot.as_df() + self.ss.UCTSlot.as_df(vin=True) + + def test_pflow_reset(self): + """ + Test resetting power flow. + """ + + self.ss.PFlow.run() + self.ss.reset() + self.ss.PFlow.run() + + def test_alter_param(self): + """ + Test altering parameter for power flow. + """ + + self.ss.PV.alter("v0", "PV_3", 0.98) + self.assertEqual(self.ss.PV.v0.v[1], 0.98) + self.ss.PFlow.run() + + def test_multiple_disconnected_line(self): + """ + Test connectivity check for systems with disconnected lines. + + These disconnected lines (zeros) was not excluded when counting + connected buses, causing an out-of-bound error. + """ + + self.ss.Line.u.v[[0, 6]] = 0 + self.ss.PFlow.run() + # TODO: need to add `connectivity` in `system` + # self.assertEqual(len(self.ss.Bus.islands), 1) + # self.assertEqual(self.ss.Bus.n_islanded_buses, 0) + + +class TestIEEE14RAW(unittest.TestCase): + """ + Test IEEE14 system in the RAW format. + """ + + # TODO: after add `run` in `system`, improve this part + def test_ieee14_raw(self): + ss = ams.load( + get_case("ieee14/ieee14.raw"), + default_config=True, + no_output=True, + ) + ss.PFlow.run() + self.assertEqual(ss.PFlow.exit_code, 0, "Exit code is not 0.") + + def test_ieee14_raw_convert(self): + ss = ams.run( + get_case("ieee14/ieee14.raw"), + convert=True, + default_config=True, + ) + os.remove(ss.files.dump) + self.assertEqual(ss.exit_code, 0, "Exit code is not 0.") + + def test_ieee14_raw2json_convert(self): + ss = ams.run( + get_case("ieee14/ieee14.raw"), + convert="json", + default_config=True, + ) + + ss2 = ams.run( + "ieee14.json", + default_config=True, + no_output=True, + ) + + os.remove(ss.files.dump) + self.assertEqual(ss2.exit_code, 0, "Exit code is not 0.") + + def test_read_json_from_memory(self): + fd = open(get_case("ieee14/ieee14.json"), "r") + + ss = ams.main.System( + default_config=True, + no_output=True, + ) + ams.io.json.read(ss, fd) + ss.setup() + ss.PFlow.run() + + fd.close() + self.assertEqual(ss.exit_code, 0, "Exit code is not 0.") + + def test_read_mpc_from_memory(self): + fd = open(get_case("matpower/case14.m"), "r") + + ss = ams.main.System( + default_config=True, + no_output=True, + ) + ams.io.matpower.read(ss, fd) + ss.setup() + ss.PFlow.run() + + fd.close() + self.assertEqual(ss.exit_code, 0, "Exit code is not 0.") + + def test_read_psse_from_memory(self): + fd_raw = open(get_case("ieee14/ieee14.raw"), "r") + + ss = ams.main.System( + default_config=True, + no_output=True, + ) + # suppress out-of-normal info + ss.config.warn_limits = 0 + ss.config.warn_abnormal = 0 + + ams.io.psse.read(ss, fd_raw) + ss.setup() + ss.PFlow.run() + + fd_raw.close() + self.assertEqual(ss.exit_code, 0, "Exit code is not 0.") + + +class TestCaseInit(unittest.TestCase): + """ + Test if initializations pass. + """ + + def test_ieee39_init(self): + """ + Test if ieee39 initialization works. + """ + ss = ams.load( + get_case("ieee39/ieee39_uced.xlsx"), + default_config=True, + no_output=True, + ) + ss.DCOPF.init() + ss.RTED.init() + ss.ED.init() + ss.UC.init() + + self.assertEqual(ss.DCOPF.exit_code, 0, "Exit code is not 0.") + self.assertEqual(ss.RTED.exit_code, 0, "Exit code is not 0.") + self.assertEqual(ss.ED.exit_code, 0, "Exit code is not 0.") + self.assertEqual(ss.UC.exit_code, 0, "Exit code is not 0.") + + def test_ieee39_esd1_init(self): + """ + Test if ieee39 with ESD1 initialization works. + """ + ss = ams.load( + get_case("ieee39/ieee39_uced_esd1.xlsx"), + default_config=True, + no_output=True, + ) + ss.ED2.init() + ss.UC2.init() + + self.assertEqual(ss.ED2.exit_code, 0, "Exit code is not 0.") + self.assertEqual(ss.UC2.exit_code, 0, "Exit code is not 0.") From 89147da96e1afcd96512e872bb495266075b56d4 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 3 Nov 2023 14:19:15 -0400 Subject: [PATCH 14/24] Add testing param altering --- ams/system.py | 16 ++++++++++++++-- tests/test_case.py | 27 ++++++++++++++++++++++++--- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/ams/system.py b/ams/system.py index 9201c711..24ffbad9 100644 --- a/ams/system.py +++ b/ams/system.py @@ -16,7 +16,7 @@ from andes.utils.misc import elapsed from andes.utils.tab import Tab -from andes.shared import pd # NOQA +from andes.shared import (matrix, np, sparse, spmatrix) # NOQA import ams.io from ams.models.group import GroupBase @@ -126,7 +126,7 @@ def __init__(self, '_p_restore', '_store_calls', '_store_tf', '_to_orddct', '_v_to_dae', 'save_config', 'collect_config', 'e_clear', 'f_update', 'fg_to_dae', 'from_ipysheet', 'g_islands', 'g_update', 'get_z', - 'init', 'j_islands', 'j_update', 'l_update_eq', 'connectivity', 'summary', + 'init', 'j_islands', 'j_update', 'l_update_eq', 'summary', 'l_update_var', 'precompile', 'prepare', 'reload', 'remove_pycapsule', 's_update_post', 's_update_var', 'store_adder_setter', 'store_no_check_init', 'store_sparse_pattern', 'store_switch_times', 'switch_action', 'to_ipysheet', @@ -485,6 +485,18 @@ def rst_ref(name, export): return tab.draw() + def connectivity(self, info=True): + """ + Perform connectivity check for system. + + Parameters + ---------- + info : bool + True to log connectivity summary. + """ + + raise NotImplementedError + def to_andes(self, setup=True, addfile=None, overwite=None, no_keep=True, **kwargs): """ diff --git a/tests/test_case.py b/tests/test_case.py index 4a53a192..625fdfd4 100644 --- a/tests/test_case.py +++ b/tests/test_case.py @@ -81,6 +81,27 @@ def test_alter_param(self): self.assertEqual(self.ss.PV.v0.v[1], 0.98) self.ss.PFlow.run() + def test_alter_param_before_routine(self): + """ + Test altering parameter before running routine. + """ + + self.ss.GCost.alter("c1", ['GCost_1', 'GCost_2'], [1500., 3100.]) + np.testing.assert_array_equal(self.ss.GCost.c1.v, [1500., 3100., 4000., 3000.]) + self.ss.ACOPF.run() + np.testing.assert_array_equal(self.ss.GCost.c1.v, [1500., 3100., 4000., 3000.]) + + def test_alter_param_after_routine(self): + """ + Test altering parameter after running routine. + """ + + self.ss.ACOPF.run() + self.ss.GCost.alter("c1", ['GCost_1', 'GCost_2'], [1500., 3100.]) + np.testing.assert_array_equal(self.ss.GCost.c1.v, [1500., 3100., 4000., 3000.]) + self.ss.ACOPF.run() + np.testing.assert_array_equal(self.ss.GCost.c1.v, [1500., 3100., 4000., 3000.]) + def test_multiple_disconnected_line(self): """ Test connectivity check for systems with disconnected lines. @@ -88,10 +109,10 @@ def test_multiple_disconnected_line(self): These disconnected lines (zeros) was not excluded when counting connected buses, causing an out-of-bound error. """ - - self.ss.Line.u.v[[0, 6]] = 0 - self.ss.PFlow.run() # TODO: need to add `connectivity` in `system` + pass + # self.ss.Line.u.v[[0, 6]] = 0 + # self.ss.PFlow.run() # self.assertEqual(len(self.ss.Bus.islands), 1) # self.assertEqual(self.ss.Bus.n_islanded_buses, 0) From 5bf591aa430559a1eac9f9070d101d14546e49e1 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 3 Nov 2023 14:20:37 -0400 Subject: [PATCH 15/24] Add cli test --- tests/test_cli.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 tests/test_cli.py diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 00000000..3be40b4c --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,13 @@ +import unittest + +import ams + + +class TestCLI(unittest.TestCase): + def test_main_doc(self): + ams.main.doc('Bus') + ams.main.doc(list_supported=True) + + def test_misc(self): + ams.main.misc(show_license=True) + ams.main.misc(save_config=None, overwrite=True) From d30a86d49b24b4cdd1a5f6985fface893e5aa985 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 3 Nov 2023 14:28:48 -0400 Subject: [PATCH 16/24] Omit PFlow formulation --- ams/routines/pflow.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/ams/routines/pflow.py b/ams/routines/pflow.py index 949a5231..1cb66170 100644 --- a/ams/routines/pflow.py +++ b/ams/routines/pflow.py @@ -76,14 +76,7 @@ def __init__(self, system, config): tex_name=r"q_{g}", model="StaticGen", ) - # --- constraints --- - self.pb = Constraint( - name="pb", - info="power balance", - e_str="sum(pl) - sum(pg)", - type="eq", - ) - # TODO: AC power flow formulation + # NOTE: omit AC power flow formulation here def solve(self, method="newton"): """ From 83d9f317657fa8bd21d905d0184cd51c7936eef0 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 3 Nov 2023 14:47:52 -0400 Subject: [PATCH 17/24] Add group test --- tests/test_group.py | 76 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 tests/test_group.py diff --git a/tests/test_group.py b/tests/test_group.py new file mode 100644 index 00000000..e9deee7a --- /dev/null +++ b/tests/test_group.py @@ -0,0 +1,76 @@ +""" +Tests for group functions. +""" +import unittest + +import numpy as np + +import ams + + +class TestGroup(unittest.TestCase): + """ + Test the group class functions. + """ + + def setUp(self): + self.ss = ams.run(ams.get_case("ieee39/ieee39_uced_esd1.xlsx"), + default_config=True, + no_output=True, + ) + + def test_group_access(self): + """ + Test methods such as `idx2model` + """ + ss = self.ss + + # --- idx2uid --- + self.assertIsNone(ss.DG.idx2uid(None)) + self.assertListEqual(ss.DG.idx2uid([None]), [None]) + + # --- idx2model --- + # what works + self.assertIs(ss.DG.idx2model('ESD1_1'), ss.ESD1) + self.assertListEqual(ss.DG.idx2model(['ESD1_1']), [ss.ESD1]) + + # what does not work + self.assertRaises(KeyError, ss.DG.idx2model, idx='1') + self.assertRaises(KeyError, ss.DG.idx2model, idx=88) + self.assertRaises(KeyError, ss.DG.idx2model, idx=[1, 88]) + + # --- get --- + self.assertRaises(KeyError, ss.DG.get, 'EtaC', 999) + + np.testing.assert_equal(ss.DG.get('EtaC', 'ESD1_1',), 1.0) + + np.testing.assert_equal(ss.DG.get('EtaC', ['ESD1_1'], allow_none=True,), + [1.0]) + np.testing.assert_equal(ss.DG.get('EtaC', ['ESD1_1', None], + allow_none=True, default=0.95), + [1.0, 0.95]) + + # --- set --- + ss.DG.set('EtaC', 'ESD1_1', 'v', 0.95) + np.testing.assert_equal(ss.DG.get('EtaC', 'ESD1_1',), 0.95) + + ss.DG.set('EtaC', ['ESD1_1'], 'v', 0.97) + np.testing.assert_equal(ss.DG.get('EtaC', ['ESD1_1'],), [0.97]) + + ss.DG.set('EtaC', ['ESD1_1'], 'v', [0.99]) + np.testing.assert_equal(ss.DG.get('EtaC', ['ESD1_1'],), [0.99]) + + # --- find_idx --- + self.assertListEqual(ss.DG.find_idx('name', ['ESD1_1']), + ss.ESD1.find_idx('name', ['ESD1_1']), + ) + + self.assertListEqual(ss.DG.find_idx(['name', 'Sn'], + [('ESD1_1',), + (100.0,)]), + ss.ESD1.find_idx(['name', 'Sn'], + [('ESD1_1',), + (100.0,)]),) + + # --- get group idx --- + self.assertListEqual(ss.DG.get_idx(), ss.ESD1.idx.v) From b817334b785407a0ccbf2ebbc4b4a5779190853b Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 3 Nov 2023 15:05:18 -0400 Subject: [PATCH 18/24] Add model tests --- tests/test_model.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 tests/test_model.py diff --git a/tests/test_model.py b/tests/test_model.py new file mode 100644 index 00000000..577908e7 --- /dev/null +++ b/tests/test_model.py @@ -0,0 +1,45 @@ +import unittest +import numpy as np + +import ams + + +class TestModelMethods(unittest.TestCase): + """ + Test methods of Model. + """ + + def test_model_set(self): + """ + Test `Model.set()` method. + """ + + ss = ams.run( + ams.get_case("ieee14/ieee14.json"), + default_config=True, + no_output=True, + ) + + # set a single value + ss.PQ.set("p0", "PQ_1", "v", 0.25) + self.assertEqual(ss.PQ.p0.v[0], 0.25) + + # set a list of values + ss.PQ.set("p0", ["PQ_1", "PQ_2"], "v", [0.26, 0.51]) + np.testing.assert_equal(ss.PQ.p0.v[[0, 1]], [0.26, 0.51]) + + # set a list of values + ss.PQ.set("p0", ["PQ_3", "PQ_5"], "v", [0.52, 0.16]) + np.testing.assert_equal(ss.PQ.p0.v[[2, 4]], [0.52, 0.16]) + + # set a list of idxes with a single element to an array of values + ss.PQ.set("p0", ["PQ_4"], "v", np.array([0.086])) + np.testing.assert_equal(ss.PQ.p0.v[3], 0.086) + + # set an array of idxes with a single element to an array of values + ss.PQ.set("p0", np.array(["PQ_4"]), "v", np.array([0.096])) + np.testing.assert_equal(ss.PQ.p0.v[3], 0.096) + + # set an array of idxes with a list of single value + ss.PQ.set("p0", np.array(["PQ_4"]), "v", 0.097) + np.testing.assert_equal(ss.PQ.p0.v[3], 0.097) From 136fc78fe5ba24f67576bcf12a3ecea02dd4d885 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 3 Nov 2023 15:08:27 -0400 Subject: [PATCH 19/24] Add paths test --- tests/test_paths.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 tests/test_paths.py diff --git a/tests/test_paths.py b/tests/test_paths.py new file mode 100644 index 00000000..5177176e --- /dev/null +++ b/tests/test_paths.py @@ -0,0 +1,23 @@ +import os +import unittest + +import ams +from ams.utils.paths import list_cases + + +class TestPaths(unittest.TestCase): + def setUp(self) -> None: + self.npcc = 'npcc/' + self.matpower = 'matpower/' + self.ieee14 = ams.get_case("ieee14/ieee14.raw") + + def test_tree(self): + list_cases(self.npcc, no_print=True) + list_cases(self.matpower, no_print=True) + + def test_relative_path(self): + ss = ams.run('ieee14.raw', + input_path=ams.get_case('ieee14/', check=False), + no_output=True, default_config=True, + ) + self.assertNotEqual(ss, None) From 6e971f381256a9bee171f0a0236c54db1ad01f3b Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 3 Nov 2023 15:17:47 -0400 Subject: [PATCH 20/24] Add repr test --- tests/test_repr.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 tests/test_repr.py diff --git a/tests/test_repr.py b/tests/test_repr.py new file mode 100644 index 00000000..09e6928a --- /dev/null +++ b/tests/test_repr.py @@ -0,0 +1,21 @@ +import contextlib +import unittest + +import ams + + +class TestRepr(unittest.TestCase): + """Test __repr__""" + def setUp(self): + self.ss = ams.run(ams.get_case("ieee39/ieee39_uced.xlsx"), + no_output=True, + default_config=True, + ) + + def test_print_model_repr(self): + """ + Print out Model ``cache``'s fields and values. + """ + with contextlib.redirect_stdout(None): + for model in self.ss.models.values(): + print(model.cache.__dict__) From 504443245178f81e5b7af07f1bd4e30f0d05ff1f Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 3 Nov 2023 16:01:34 -0400 Subject: [PATCH 21/24] Fix routine set --- ams/routines/routine.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/ams/routines/routine.py b/ams/routines/routine.py index a7fed1a6..6137358c 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -211,14 +211,9 @@ def set(self, src: str, idx, attr: str = 'v', value=0.0): Set the value of an attribute of a routine parameter. """ if self.__dict__[src].owner is not None: + # TODO: fit to `_v` type param in the future owner = self.__dict__[src].owner - try: - owner.set(src=src, idx=idx, attr=attr, value=value) - return True - except KeyError: - # TODO: hold values to _v if necessary in the future - logger.info(f'Variable {self.name} has no mapping.') - return None + return owner.set(src=src, idx=idx, attr=attr, value=value) else: logger.info(f'Variable {self.name} has no owner.') # FIXME: add idx for non-grouped variables From 7991e78e6bfe7825bff71d3ace054c0213be2f56 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 3 Nov 2023 16:02:50 -0400 Subject: [PATCH 22/24] Add routine test --- tests/test_routine.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 tests/test_routine.py diff --git a/tests/test_routine.py b/tests/test_routine.py new file mode 100644 index 00000000..241d0922 --- /dev/null +++ b/tests/test_routine.py @@ -0,0 +1,39 @@ +import unittest +import numpy as np + +import ams + + +class TestRoutineMethods(unittest.TestCase): + """ + Test methods of Routine. + """ + def setUp(self) -> None: + self.ss = ams.load(ams.get_case("ieee39/ieee39_uced.xlsx"), + default_config=True, + no_output=True, + ) + + def test_routine_set(self): + """ + Test `Routine.set()` method. + """ + + self.ss.DCOPF.set('c2', 'GCost_1', 'v', 10) + np.testing.assert_equal(self.ss.GCost.get('c2', 'GCost_1', 'v'), 10) + + def test_routine_get(self): + """ + Test `Routine.get()` method. + """ + + # get a rparam value + np.testing.assert_equal(self.ss.DCOPF.get('ug', 'PV_30'), 1) + + # before solving, vars values are not available + self.assertRaises(KeyError, self.ss.DCOPF.get, 'pg', 'PV_30') + + self.ss.DCOPF.run(solver='OSQP') + self.assertEqual(self.ss.DCOPF.exit_code, 0, "Exit code is not 0.") + np.testing.assert_equal(self.ss.DCOPF.get('pg', 'PV_30', 'v'), + self.ss.StaticGen.get('p', 'PV_30', 'v')) From 24b06f21c102e24af16f94c73db977be1f127ee1 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 3 Nov 2023 16:25:45 -0400 Subject: [PATCH 23/24] Add routine tests --- tests/test_routine.py | 53 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/tests/test_routine.py b/tests/test_routine.py index 241d0922..6da09bfe 100644 --- a/tests/test_routine.py +++ b/tests/test_routine.py @@ -2,6 +2,23 @@ import numpy as np import ams +import cvxpy as cp + + +def require_MIP_solver(f): + """ + Decorator for skipping tests that require MIP solver. + """ + def wrapper(*args, **kwargs): + all_solvers = cp.installed_solvers() + mip_solvers = ['CBC', 'COPT', 'GLPK_MI', 'CPLEX', 'GUROBI', + 'MOSEK', 'SCIP', 'XPRESS', 'SCIPY'] + if any(s in mip_solvers for s in all_solvers): + pass + else: + raise unittest.SkipTest("MIP solver is not available.") + return f(*args, **kwargs) + return wrapper class TestRoutineMethods(unittest.TestCase): @@ -9,7 +26,7 @@ class TestRoutineMethods(unittest.TestCase): Test methods of Routine. """ def setUp(self) -> None: - self.ss = ams.load(ams.get_case("ieee39/ieee39_uced.xlsx"), + self.ss = ams.load(ams.get_case("ieee39/ieee39_uced_esd1.xlsx"), default_config=True, no_output=True, ) @@ -37,3 +54,37 @@ def test_routine_get(self): self.assertEqual(self.ss.DCOPF.exit_code, 0, "Exit code is not 0.") np.testing.assert_equal(self.ss.DCOPF.get('pg', 'PV_30', 'v'), self.ss.StaticGen.get('p', 'PV_30', 'v')) + + def test_RTED(self): + """ + Test `RTED.run()`. + """ + + self.ss.RTED.run(solver='OSQP') + self.assertEqual(self.ss.RTED.exit_code, 0, "Exit code is not 0.") + + @require_MIP_solver + def test_RTED2(self): + """ + Test `RTED2.run()`. + """ + + self.ss.RTED2.run() + self.assertEqual(self.ss.RTED2.exit_code, 0, "Exit code is not 0.") + + def test_ED(self): + """ + Test `ED.run()`. + """ + + self.ss.ED.run(solver='OSQP') + self.assertEqual(self.ss.ED.exit_code, 0, "Exit code is not 0.") + + @require_MIP_solver + def test_ED2(self): + """ + Test `ED2.run()`. + """ + + self.ss.ED2.run() + self.assertEqual(self.ss.ED2.exit_code, 0, "Exit code is not 0.") From 1739044eb74eed3240a5d29995fa6ac6c4218a83 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 3 Nov 2023 16:30:31 -0400 Subject: [PATCH 24/24] Update release notes --- docs/source/release-notes.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 394619c0..c6456808 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -9,6 +9,11 @@ The APIs before v3.0.0 are in beta and may change without prior notice. Pre-v1.0.0 ========== +v0.7.3 (2023-11-3) +------------------- + +- Add tests + v0.7.2 (2023-10-26) -------------------