From f4da1f74553e13281bd38096d77701b6eb6ac357 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 4 Nov 2023 03:47:01 -0400 Subject: [PATCH 01/77] Add github actions --- .github/ISSUE_TEMPLATE/bug_report.md | 39 +++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 +++++++++ .../pull_request_template.md | 8 ++++ .github/workflows/pythonapp.yml | 43 +++++++++++++++++++ 4 files changed, 110 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE/pull_request_template.md create mode 100644 .github/workflows/pythonapp.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..2c7a7bd1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,39 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: cuihantao + +--- + +**Describe the bug** + +A clear and concise description of what the bug is. + +**To Reproduce** + +Update data files and code for reproducing the error. + +If you need to insert code inline, use a pair of triple backticks to format: + + ```python + Code goes here + ``` + +**Expected behavior** + +A clear and concise description of what you expected to happen. + +**Desktop (please complete the following information):** + + - OS: [e.g. Windows] + - AMS version (please paste the output from `ams misc --version`) + + +**pip packages (please paste the output from `pip list`)** + + +**Additional context** + +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..f12c71ce --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md new file mode 100644 index 00000000..7a792a6b --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -0,0 +1,8 @@ +Fixes # + +## Proposed Changes + + - + - + - + \ No newline at end of file diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml new file mode 100644 index 00000000..a01fa9e3 --- /dev/null +++ b/.github/workflows/pythonapp.yml @@ -0,0 +1,43 @@ +name: Python application + +on: [push, pull_request] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Set up Python 3.9 + uses: actions/setup-python@v1 + with: + python-version: 3.9 + - name: Install dependencies + run: | + # work around a pip issue with extras_require: https://github.com/pypa/pip/issues/8323 + python -m pip install -U "pip @ git+https://github.com/pypa/pip.git" + python -m pip install --upgrade pip + python -m pip install .[all] + python -m pip install nbmake==0.10 pytest-xdist line_profiler # add'l packages for notebook tests. + # - name: Lint with flake8 for pull requests + # if: github.event_name == 'pull_request' + # run: | + # # stop the build if there are Python syntax errors or undefined names + # flake8 . + - name: Test with pytest + run: | + pytest --log-cli-level=10 + # - name: Test notebooks. + # run: | + # pytest --log-cli-level=10 --nbmake examples --ignore=examples/verification + # - name: Build a distribution if tagged + # if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + # run: | + # python setup.py sdist + # - name: Publish a Python distribution to PyPI if tagged + # if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + # uses: pypa/gh-action-pypi-publish@master + # with: + # user: __token__ + # password: ${{ secrets.pypi_password }} From aa84e98e5e1fc0715cfdd9a62d0f421c2e129698 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 4 Nov 2023 14:55:02 -0400 Subject: [PATCH 02/77] Fix rouinte var values before solve --- ams/opt/omodel.py | 5 ++++- tests/test_routine.py | 19 ++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index 335a3b6b..6c6baad1 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -248,7 +248,10 @@ def v(self): """ Return the CVXPY variable value. """ - out = self.om.vars[self.name].value if self._v is None else self._v + if self.name in self.om.vars: + out = self.om.vars[self.name].value if self._v is None else self._v + else: + out = None return out @v.setter diff --git a/tests/test_routine.py b/tests/test_routine.py index 6da09bfe..e36b0884 100644 --- a/tests/test_routine.py +++ b/tests/test_routine.py @@ -31,6 +31,22 @@ def setUp(self) -> None: no_output=True, ) + def test_var_access_brfore_solve(self): + """ + Test var access before solve. + """ + self.ss.DCOPF.init() + np.testing.assert_equal(self.ss.DCOPF.pg.v, None) + + def test_var_access_after_solve(self): + """ + Test var access after solve. + """ + self.ss.DCOPF.run() + np.testing.assert_equal(self.ss.DCOPF.pg.v, + self.ss.StaticGen.get(src='p', attr='v', + idx=self.ss.DCOPF.pg.get_idx())) + def test_routine_set(self): """ Test `Routine.set()` method. @@ -47,9 +63,6 @@ def test_routine_get(self): # 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'), From f219328188e84bacf2947700e185c74f26376c54 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 4 Nov 2023 15:00:48 -0400 Subject: [PATCH 03/77] Improve rparm and var __repr__ --- ams/core/param.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ams/core/param.py b/ams/core/param.py index 2a88ffc3..550be904 100644 --- a/ams/core/param.py +++ b/ams/core/param.py @@ -182,7 +182,7 @@ def __repr__(self): if len(self.v) <= 20: span = f', v={self.v}' else: - span = f', v in shape({self.v.shape})' + span = f', v in shape {self.v.shape}' elif isinstance(self.v, list): if len(self.v) <= 20: span = f', v={self.v}' From 9c28eeab1d337835264e1358f0b7e24cb0ccdac6 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 4 Nov 2023 15:01:21 -0400 Subject: [PATCH 04/77] Improve rparm and var __repr__ --- ams/opt/omodel.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index 6c6baad1..93365d65 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -335,6 +335,13 @@ def __repr__(self): if self.owner.n == 0: span = [] + elif isinstance(self.v, np.ndarray): + if self.v.shape[0] == 1 or self.v.ndim == 1: + if len(self.v) <= 20: + span = f', v={self.v}' + else: + span = f', v in shape {self.v.shape}' + elif 1 <= self.owner.n <= 20: span = f'a={self.a}, v={self.v}' From 1193424747775112dec2b9cb465138609cfcb59b Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 4 Nov 2023 15:03:39 -0400 Subject: [PATCH 05/77] Typo --- ams/opt/omodel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index 93365d65..d0facec2 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -340,7 +340,7 @@ def __repr__(self): if len(self.v) <= 20: span = f', v={self.v}' else: - span = f', v in shape {self.v.shape}' + span = f'v in shape {self.v.shape}' elif 1 <= self.owner.n <= 20: span = f'a={self.a}, v={self.v}' From d8322729b2ab4962ffc472f9d4591738ce666ab0 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 4 Nov 2023 15:06:04 -0400 Subject: [PATCH 06/77] Typo --- ams/core/param.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ams/core/param.py b/ams/core/param.py index 550be904..77488b59 100644 --- a/ams/core/param.py +++ b/ams/core/param.py @@ -190,7 +190,7 @@ def __repr__(self): span = f', v in length of {len(self.v)}' else: span = f', v={self.v}' - return f'{self.__class__.__name__}: {self.name} <{self.owner.__class__.__name__}>{span}' + return f'{self.__class__.__name__}: {self.owner.__class__.__name__}.{self.name}{span}' def get_idx(self): """ From 535db816b373700fcfe399c6a310fff8eff9ac48 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 4 Nov 2023 15:28:25 -0400 Subject: [PATCH 07/77] Add property method to access constr value --- ams/opt/omodel.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index d0facec2..95b8c078 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -340,7 +340,7 @@ def __repr__(self): if len(self.v) <= 20: span = f', v={self.v}' else: - span = f'v in shape {self.v.shape}' + span = f', v in shape {self.v.shape}' elif 1 <= self.owner.n <= 20: span = f'a={self.a}, v={self.v}' @@ -353,7 +353,7 @@ def __repr__(self): span = ':'.join([str(i) for i in span]) span = 'a=[' + span + ']' - return f'{self.__class__.__name__}: {self.owner.__class__.__name__}.{self.name}, {span}' + return f'{self.__class__.__name__}: {self.owner.__class__.__name__}.{self.name}{span}' class Constraint(OptzBase): @@ -447,6 +447,16 @@ def __repr__(self): enabled = 'ON' if self.name in self.om.constrs else 'OFF' return f"[{enabled}]: {self.e_str}" + @property + def v(self): + """ + Return the CVXPY constraint LHS value. + """ + if self.name in self.om.constrs: + return self.om.constrs[self.name]._expr.value + else: + return None + class Objective(OptzBase): """ From 6f71e9651fe23e9b5098b3483850d31fdbd8d4d5 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 4 Nov 2023 15:28:50 -0400 Subject: [PATCH 08/77] Add routine test of constr value --- tests/test_routine.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/tests/test_routine.py b/tests/test_routine.py index e36b0884..b3bb7a09 100644 --- a/tests/test_routine.py +++ b/tests/test_routine.py @@ -33,20 +33,34 @@ def setUp(self) -> None: def test_var_access_brfore_solve(self): """ - Test var access before solve. + Test Var access before solve. """ self.ss.DCOPF.init() - np.testing.assert_equal(self.ss.DCOPF.pg.v, None) + self.assertIsNone(self.ss.DCOPF.pg.v) def test_var_access_after_solve(self): """ - Test var access after solve. + Test Var access after solve. """ self.ss.DCOPF.run() np.testing.assert_equal(self.ss.DCOPF.pg.v, self.ss.StaticGen.get(src='p', attr='v', idx=self.ss.DCOPF.pg.get_idx())) + def test_constr_access_brfore_solve(self): + """ + Test Constr access before solve. + """ + self.ss.DCOPF.init(force=True) + np.testing.assert_equal(self.ss.DCOPF.lub.v, None) + + def test_constr_access_after_solve(self): + """ + Test Constr access after solve. + """ + self.ss.DCOPF.run() + self.assertIsInstance(self.ss.DCOPF.lub.v, np.ndarray) + def test_routine_set(self): """ Test `Routine.set()` method. From 6aa62f324186fd83d0cd0620e05675c68f87b38e Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 4 Nov 2023 15:40:37 -0400 Subject: [PATCH 09/77] Refactor routine tests --- tests/test_omodel.py | 64 +++++++++++++++++++++++++++++++++++ tests/test_routine.py | 77 +++++++++++++++++++++---------------------- 2 files changed, 102 insertions(+), 39 deletions(-) create mode 100644 tests/test_omodel.py diff --git a/tests/test_omodel.py b/tests/test_omodel.py new file mode 100644 index 00000000..6823dcec --- /dev/null +++ b/tests/test_omodel.py @@ -0,0 +1,64 @@ +import unittest +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 TestOMdel(unittest.TestCase): + """ + Test methods of `OModel`. + """ + def setUp(self) -> None: + self.ss = ams.load(ams.get_case("ieee39/ieee39_uced_esd1.xlsx"), + default_config=True, + no_output=True, + ) + + def test_var_access_brfore_solve(self): + """ + Test `Var` access before solve. + """ + self.ss.DCOPF.init() + self.assertIsNone(self.ss.DCOPF.pg.v) + + def test_var_access_after_solve(self): + """ + Test `Var` access after solve. + """ + self.ss.DCOPF.run() + np.testing.assert_equal(self.ss.DCOPF.pg.v, + self.ss.StaticGen.get(src='p', attr='v', + idx=self.ss.DCOPF.pg.get_idx())) + + def test_constr_access_brfore_solve(self): + """ + Test `Constr` access before solve. + """ + self.ss.DCOPF.init(force=True) + np.testing.assert_equal(self.ss.DCOPF.lub.v, None) + + def test_constr_access_after_solve(self): + """ + Test `Constr` access after solve. + """ + self.ss.DCOPF.run() + self.assertIsInstance(self.ss.DCOPF.lub.v, np.ndarray) + + # NOTE: add Var, Constr add functions diff --git a/tests/test_routine.py b/tests/test_routine.py index b3bb7a09..f60bc909 100644 --- a/tests/test_routine.py +++ b/tests/test_routine.py @@ -23,7 +23,7 @@ def wrapper(*args, **kwargs): class TestRoutineMethods(unittest.TestCase): """ - Test methods of Routine. + Test methods of `Routine`. """ def setUp(self) -> None: self.ss = ams.load(ams.get_case("ieee39/ieee39_uced_esd1.xlsx"), @@ -31,36 +31,6 @@ def setUp(self) -> None: no_output=True, ) - def test_var_access_brfore_solve(self): - """ - Test Var access before solve. - """ - self.ss.DCOPF.init() - self.assertIsNone(self.ss.DCOPF.pg.v) - - def test_var_access_after_solve(self): - """ - Test Var access after solve. - """ - self.ss.DCOPF.run() - np.testing.assert_equal(self.ss.DCOPF.pg.v, - self.ss.StaticGen.get(src='p', attr='v', - idx=self.ss.DCOPF.pg.get_idx())) - - def test_constr_access_brfore_solve(self): - """ - Test Constr access before solve. - """ - self.ss.DCOPF.init(force=True) - np.testing.assert_equal(self.ss.DCOPF.lub.v, None) - - def test_constr_access_after_solve(self): - """ - Test Constr access after solve. - """ - self.ss.DCOPF.run() - self.assertIsInstance(self.ss.DCOPF.lub.v, np.ndarray) - def test_routine_set(self): """ Test `Routine.set()` method. @@ -82,6 +52,17 @@ def test_routine_get(self): np.testing.assert_equal(self.ss.DCOPF.get('pg', 'PV_30', 'v'), self.ss.StaticGen.get('p', 'PV_30', 'v')) + +class TestRoutineSolve(unittest.TestCase): + """ + Test solving routines. + """ + def setUp(self) -> None: + self.ss = ams.load(ams.get_case("ieee39/ieee39_uced_esd1.xlsx"), + default_config=True, + no_output=True, + ) + def test_RTED(self): """ Test `RTED.run()`. @@ -90,22 +71,31 @@ def test_RTED(self): self.ss.RTED.run(solver='OSQP') self.assertEqual(self.ss.RTED.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_RTED2(self): + def test_UC(self): """ - Test `RTED2.run()`. + Test `UC.run()`. """ - self.ss.RTED2.run() - self.assertEqual(self.ss.RTED2.exit_code, 0, "Exit code is not 0.") + self.ss.UC.run() + self.assertEqual(self.ss.UC.exit_code, 0, "Exit code is not 0.") - def test_ED(self): + @require_MIP_solver + def test_RTED2(self): """ - Test `ED.run()`. + Test `RTED2.run()`. """ - self.ss.ED.run(solver='OSQP') - self.assertEqual(self.ss.ED.exit_code, 0, "Exit code is not 0.") + self.ss.RTED2.run() + self.assertEqual(self.ss.RTED2.exit_code, 0, "Exit code is not 0.") @require_MIP_solver def test_ED2(self): @@ -115,3 +105,12 @@ def test_ED2(self): self.ss.ED2.run() self.assertEqual(self.ss.ED2.exit_code, 0, "Exit code is not 0.") + + @require_MIP_solver + def test_UC2(self): + """ + Test `UC2.run()`. + """ + + self.ss.UC2.run() + self.assertEqual(self.ss.UC2.exit_code, 0, "Exit code is not 0.") From c79506e0e8136299e6916cef697e491bdf85776c Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 4 Nov 2023 23:18:04 -0400 Subject: [PATCH 10/77] Add mehotd graph to routine --- ams/routines/routine.py | 314 ++++++++++++++++++++++++++++++++-------- 1 file changed, 253 insertions(+), 61 deletions(-) diff --git a/ams/routines/routine.py b/ams/routines/routine.py index 6137358c..b2bedd71 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -2,25 +2,22 @@ Module for routine data. """ -import logging # NOQA -from typing import Optional, Union, Type, Iterable # NOQA -from collections import OrderedDict # NOQA +import logging +from typing import Optional, Union, Type, Iterable +from collections import OrderedDict -import numpy as np # NOQA +import numpy as np +import matplotlib.pyplot as plt -from andes.core import Config # NOQA +from andes.core import Config from andes.shared import deg2rad # NOQA -from andes.utils.misc import elapsed # NOQA -from ams.utils import timer # NOQA -from ams.core.param import RParam # NOQA -from ams.opt.omodel import OModel, Var, Constraint, Objective # NOQA +from andes.utils.misc import elapsed +from ams.core.param import RParam +from ams.opt.omodel import OModel, Var, Constraint, Objective -from ams.core.symprocessor import SymProcessor # NOQA -from ams.core.documenter import RDocumenter # NOQA -from ams.core.service import RBaseService, ValueService # NOQA - -from ams.models.group import GroupBase # NOQA -from ams.core.model import Model # NOQA +from ams.core.symprocessor import SymProcessor +from ams.core.documenter import RDocumenter +from ams.core.service import RBaseService, ValueService logger = logging.getLogger(__name__) @@ -43,9 +40,12 @@ def __init__(self, system=None, config=None): self.system = system self.config = Config(self.class_name) self.info = None - self.tex_names = OrderedDict((('sys_f', 'f_{sys}'), - ('sys_mva', 'S_{b,sys}'), - )) + self.tex_names = OrderedDict( + ( + ("sys_f", "f_{sys}"), + ("sys_mva", "S_{b,sys}"), + ) + ) self.syms = SymProcessor(self) # symbolic processor self._syms = False # flag if symbols has been generated @@ -55,7 +55,7 @@ def __init__(self, system=None, config=None): self.constrs = OrderedDict() self.obj = None self.initialized = False - self.type = 'UndefinedType' + self.type = "UndefinedType" self.docum = RDocumenter(self) # --- sync mapping --- @@ -68,17 +68,24 @@ def __init__(self, system=None, config=None): if config is not None: self.config.load(config) # TODO: these default configs might to be revised - self.config.add(OrderedDict((('sparselib', 'klu'), - ('linsolve', 0), - ))) - self.config.add_extra("_help", - sparselib="linear sparse solver name", - linsolve="solve symbolic factorization each step (enable when KLU segfaults)", - ) - self.config.add_extra("_alt", - sparselib=("klu", "umfpack", "spsolve", "cupy"), - linsolve=(0, 1), - ) + self.config.add( + OrderedDict( + ( + ("sparselib", "klu"), + ("linsolve", 0), + ) + ) + ) + self.config.add_extra( + "_help", + sparselib="linear sparse solver name", + linsolve="solve symbolic factorization each step (enable when KLU segfaults)", + ) + self.config.add_extra( + "_alt", + sparselib=("klu", "umfpack", "spsolve", "cupy"), + linsolve=(0, 1), + ) self.exec_time = 0.0 # recorded time to execute the routine in seconds # TODO: check exit_code of gurobipy or any other similiar solvers @@ -100,7 +107,8 @@ def _loc(self, src: str, idx, allow_none=False): return loc else: idx_none = [idxe for idxe in idx if idxe not in src_idx] - raise ValueError(f'Var <{self.class_name}.{src}> does not contain value with idx={idx_none[0]}') + msg = f"Var <{self.class_name}.{src}> does not contain value with idx={idx_none}" + raise ValueError(msg) def get_load(self, horizon: Union[int, str], src: str, attr: str = 'v', @@ -126,22 +134,22 @@ def get_load(self, horizon: Union[int, str], pq_zone = self.system.PQ.zone.v pq0 = self.system.PQ.get(src=src, attr=attr, idx=idx) else: - pq_zone = self.system.PQ.get(src='zone', attr='v', idx=idx) + pq_zone = self.system.PQ.get(src="zone", attr="v", idx=idx) pq0 = self.system.PQ.get(src=src, attr=attr, idx=idx) col = [all_zone.index(pq_z) for pq_z in pq_zone] mdl = self.system.__dict__[model] if mdl.n == 0: - raise ValueError(f'<{model}> does not have data, check input file.') + raise ValueError(f"<{model}> does not have data, check input file.") if factor not in mdl.__dict__.keys(): - raise ValueError(f'<{model}> does not have <{factor}>.') + raise ValueError(f"<{model}> does not have <{factor}>.") sdv = mdl.__dict__[factor].v horizon_all = mdl.idx.v try: row = horizon_all.index(horizon) except ValueError: - raise ValueError(f'<{model}> does not have horizon with idx=<{horizon}>.') + raise ValueError(f"<{model}> does not have horizon with idx=<{horizon}>.") pq_factor = np.array(sdv[:, col][row, :]) pqv = np.multiply(pq0, pq_factor) return pqv @@ -163,16 +171,16 @@ def get(self, src: str, idx, attr: str = 'v', Horizon index. """ if src not in self.__dict__.keys(): - raise ValueError(f'<{src}> does not exist in <<{self.class_name}>.') + raise ValueError(f"<{src}> does not exist in <<{self.class_name}>.") item = self.__dict__[src] if not hasattr(item, attr): - raise ValueError(f'{attr} does not exist in {self.class_name}.{src}.') + raise ValueError(f"{attr} does not exist in {self.class_name}.{src}.") idx_all = item.get_idx() if idx_all is None: - raise ValueError(f'<{self.class_name}> item <{src}> has no idx.') + raise ValueError(f"<{self.class_name}> item <{src}> has no idx.") if isinstance(idx, (str, int)): idx = [idx] @@ -183,12 +191,13 @@ def get(self, src: str, idx, attr: str = 'v', loc = [idx_all.index(idxe) if idxe in idx_all else None for idxe in idx] if None in loc: idx_none = [idxe for idxe in idx if idxe not in idx_all] - raise ValueError(f'Var <{self.class_name}.{src}> does not contain value with idx={idx_none}') + msg = f"Var <{self.class_name}.{src}> does not contain value with idx={idx_none}" + raise ValueError(msg) out = getattr(item, attr)[loc] if horizon is not None: if item.horizon is None: - raise ValueError(f'horizon is not defined for {self.class_name}.{src}.') + raise ValueError(f"horizon is not defined for {self.class_name}.{src}.") horizon_all = item.horizon.get_idx() if isinstance(horizon, int): horizon = [horizon] @@ -197,16 +206,20 @@ def get(self, src: str, idx, attr: str = 'v', if isinstance(horizon, np.ndarray): horizon = horizon.tolist() if isinstance(horizon, list): - loc_h = [horizon_all.index(idxe) if idxe in horizon_all else None for idxe in horizon] + loc_h = [ + horizon_all.index(idxe) if idxe in horizon_all else None + for idxe in horizon + ] if None in loc_h: idx_none = [idxe for idxe in horizon if idxe not in horizon_all] - raise ValueError(f'Var <{self.class_name}.{src}> does not contain horizon with idx={idx_none}') + msg = f"Var <{self.class_name}.{src}> does not contain horizon with idx={idx_none}" + raise ValueError(msg) out = out[:, loc_h] if out.shape[1] == 1: out = out[:, 0] return out - def set(self, src: str, idx, attr: str = 'v', value=0.0): + def set(self, src: str, idx, attr: str = "v", value=0.0): """ Set the value of an attribute of a routine parameter. """ @@ -215,11 +228,11 @@ def set(self, src: str, idx, attr: str = 'v', value=0.0): owner = self.__dict__[src].owner return owner.set(src=src, idx=idx, attr=attr, value=value) else: - logger.info(f'Variable {self.name} has no owner.') + logger.info(f"Variable {self.name} has no owner.") # FIXME: add idx for non-grouped variables return None - def doc(self, max_width=78, export='plain'): + def doc(self, max_width=78, export="plain"): """ Retrieve routine documentation as a string. """ @@ -249,7 +262,8 @@ def _data_check(self): no_input.append(rname) owner_list.append(rparam.owner.class_name) if len(no_input) > 0: - logger.error(f"Following models are missing from input file: {set(owner_list)}") + msg = f"Following models have no input: {set(owner_list)}" + logger.error(msg) return False # TODO: add data validation for RParam, typical range, etc. return True @@ -267,12 +281,13 @@ def init(self, force=False, no_code=True, **kwargs): """ # TODO: add input check, e.g., if GCost exists if not force and self.initialized: - logger.debug(f'{self.class_name} has already been initialized.') + logger.debug(f"{self.class_name} has already been initialized.") return True if self._data_check(): - logger.debug(f'{self.class_name} data check passed.') + logger.debug(f"{self.class_name} data check passed.") else: - logger.warning(f'{self.class_name} data check failed, setup may run into error!') + msg = f"{self.class_name} data check failed, setup may run into error!" + logger.warning(msg) self._constr_check() # FIXME: build the system matrices every init might slow down the process self.system.mats.make() @@ -332,7 +347,7 @@ def run(self, force_init=False, no_code=True, **kwargs): self.exit_code = self.syms.status[status] self.system.exit_code = self.exit_code _, s = elapsed(t0) - self.exec_time = float(s.split(' ')[0]) + self.exec_time = float(s.split(" ")[0]) sstats = self.om.mdl.solver_stats # solver stats n_iter = int(sstats.num_iters) n_iter_str = f"{n_iter} iterations " if n_iter > 1 else f"{n_iter} iteration " @@ -361,7 +376,7 @@ def report(self, **kwargs): raise NotImplementedError def __repr__(self): - return f'{self.class_name} at {hex(id(self))}' + return f"{self.class_name} at {hex(id(self))}" def _ppc2ams(self): """ @@ -383,11 +398,12 @@ def _check_attribute(self, key, value): """ if key in self.__dict__: existing_keys = [] - for type in ['constrs', 'vars', 'rparams']: + for type in ["constrs", "vars", "rparams"]: if type in self.__dict__: existing_keys += list(self.__dict__[type].keys()) if key in existing_keys: - logger.warning(f"{self.class_name}: redefinition of member <{key}>. Likely a modeling error.") + msg = f"Attribute <{key}> already exists in <{self.class_name}>." + logger.warning(msg) # register owner routine instance of following attributes if isinstance(value, (RBaseService)): @@ -443,7 +459,7 @@ def __delattr__(self, name): name of the attribute """ self._unregister_attribute(name) - if name == 'obj': + if name == "obj": self.obj = None else: super().__delattr__(name) # Call the superclass implementation @@ -749,11 +765,31 @@ def addVars(self, """ if model is None and shape is None: raise ValueError("Either model or shape must be specified.") - item = Var(name=name, tex_name=tex_name, info=info, src=src, unit=unit, - model=model, shape=shape, lb=lb, ub=ub, horizon=horizon, nonneg=nonneg, - nonpos=nonpos, complex=complex, imag=imag, symmetric=symmetric, - diag=diag, psd=psd, nsd=nsd, hermitian=hermitian, bool=bool, - integer=integer, pos=pos, neg=neg, ) + item = Var( + name=name, + tex_name=tex_name, + info=info, + src=src, + unit=unit, + model=model, + shape=shape, + lb=lb, + ub=ub, + horizon=horizon, + nonneg=nonneg, + nonpos=nonpos, + complex=complex, + imag=imag, + symmetric=symmetric, + diag=diag, + psd=psd, + nsd=nsd, + hermitian=hermitian, + bool=bool, + integer=integer, + pos=pos, + neg=neg, + ) # add the variable as an routine attribute setattr(self, name, item) @@ -766,8 +802,10 @@ def addVars(self, elif item.model in self.system.models.keys(): item.owner = self.system.models[item.model] else: - msg = f'Model indicator \'{item.model}\' of <{item.rtn.class_name}.{name}>' - msg += ' is not a model or group. Likely a modeling error.' + msg = ( + f"Model indicator '{item.model}' of <{item.rtn.class_name}.{name}>" + ) + msg += " is not a model or group. Likely a modeling error." logger.warning(msg) self._post_add_check() @@ -779,3 +817,157 @@ def _initial_guess(self): Generate initial guess for the optimization model. """ raise NotImplementedError + + def graph( + self, + input: Optional[Union[RParam, Var]] = None, + ytimes: Optional[float] = None, + directed: Optional[bool] = True, + dpi: Optional[int] = 100, + figsize: Optional[tuple] = None, + adjust_bus: Optional[bool] = False, + gen_color: Optional[str] = "red", + rest_color: Optional[str] = "black", + vertex_size: Optional[int] = 10.0, + vertex_label_size: Optional[int] = 8, + vertex_label_dist: Optional[int] = -1.5, + vertex_label_angle: Optional[int] = 10.2, + edge_arrow_size: Optional[int] = 8, + edge_width: Optional[int] = 1, + edge_align_label: Optional[bool] = True, + layout: Optional[str] = "rt", + autocurve: Optional[bool] = True, + ax: Optional[plt.Axes] = None, + **visual_style, + ): + """ + Plot a system graph, with optional input. + For now, only support plotting of Bus and Line elements as input. + + Examples + -------- + >>> import ams + >>> sp = ams.load(ams.get_case('5bus/pjm5bus_uced.xlsx')) + >>> sp.DCOPF.run() + >>> sp.DCOPF.plot(input=sp.DCOPF.pn, + >>> ytimes=10, + >>> adjust_bus=True, + >>> vertex_size=10, + >>> vertex_label_size=15, + >>> vertex_label_dist=2, + >>> vertex_label_angle=90, + >>> show=False, + >>> edge_align_label=True, + >>> autocurve=True,) + + Parameters + ---------- + input: RParam or Var, optional + The variable or parameter to be plotted. + ytimes: float, optional + The scaling factor of the values. + directed: bool, optional + Whether the graph is directed. + dpi: int, optional + Dots per inch. + figsize: tuple, optional + Figure size. + adjust_bus: bool, optional + Whether to adjust the bus size. + gen_color: str, optional + Color of the generator bus. + rest_color: str, optional + Color of the rest buses. + vertex_size: int, optional + Size of the vertices. + vertex_label_size: int, optional + Size of the vertex labels. + vertex_label_dist: int, optional + Distance of the vertex labels. + vertex_label_angle: int, optional + Angle of the vertex labels. + edge_arrow_size: int, optional + Size of the edge arrows. + edge_width: int, optional + Width of the edges. + edge_align_label: bool, optional + Whether to align the edge labels. + layout: str, optional + Layout of the graph. + autocurve: bool, optional + Whether to use autocurve. + ax: plt.Axes, optional + Matplotlib axes. + visual_style: dict, optional + Visual style, see ``igraph.plot`` for details. + """ + try: + import igraph as ig + except ImportError: + logger.error("Package `igraph` is not installed.") + return None + + system = self.system + edges = np.column_stack([system.Line.bus1.v, system.Line.bus2.v]) + g = ig.Graph(n=system.Bus.n, directed=directed, edges=edges) + + # --- visual style --- + vstyle = { + # layout style + "layout": layout, + # vertices + "vertex_size": vertex_size, + "vertex_label_size": vertex_label_size, + "vertex_label_dist": vertex_label_dist, + "vertex_label_angle": vertex_label_angle, + # edges + "edge_arrow_size": edge_arrow_size, + "edge_width": edge_width, + "edge_align_label": edge_align_label, + # others + **visual_style, + } + + # bus size + gidx = system.PV.idx.v + system.Slack.idx.v + gbus = system.StaticGen.get(src="bus", attr="v", idx=gidx) + # initialize all bus size as vertex_size + bus_size = [vertex_size] * system.Bus.n + if adjust_bus and isinstance(vertex_size, (int, float)): + # adjust gen bus size using Sn + gsn = system.StaticGen.get(src="Sn", attr="v", idx=gidx) + gbsize = vertex_size * gsn / gsn.max() + gbus_dict = {bus: size for bus, size in zip(gbus, gbsize)} + for key, val in gbus_dict.items(): + bus_size[system.Bus.idx2uid(key)] = val + if isinstance(vertex_size, Iterable): + bus_size = vertex_size + vstyle["vertex_size"] = bus_size + + # bus colors + gbus_uid = system.Bus.idx2uid(gbus) + bus_uid = system.Bus.idx2uid(system.Bus.idx.v) + g.vs["label"] = system.Bus.name.v + g.vs["bus_type"] = ["gen" if bus_i in gbus_uid else "rest" for bus_i in bus_uid] + color_dict = {"gen": gen_color, "rest": rest_color} + vstyle["vertex_color"] = [color_dict[btype] for btype in g.vs["bus_type"]] + + # --- variables --- + k = ytimes if ytimes is not None else 1 + if input.owner.class_name == "Bus": + logger.debug(f"Plotting <{input.name}> as vertex label.") + values = [f"${input.tex_name}$={round(k*v)}" for v in input.v] + vlabel = system.Bus.name.v + vout = [f"{vin}, {label}" for label, vin in zip(values, vlabel)] + vstyle["vertex_label"] = vout + elif input.owner.class_name == "Line": + logger.debug(f"Plotting <{input.name}> as edge label.") + values = [f"${input.tex_name}$={round(k*v)}" for v in input.v] + elabel = system.Line.name.v + eout = [f"{label}" for label, ein in zip(values, elabel)] + vstyle["edge_label"] = eout + + if ax is None: + _, ax = plt.subplots(figsize=figsize, dpi=dpi) + ig.plot(g, autocurve=autocurve, target=ax, **vstyle) + return ax, g From f469a02410990836e6d482ed40ebab64fe7958b2 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 4 Nov 2023 23:58:10 -0400 Subject: [PATCH 11/77] Fix sparse matrix value --- ams/core/matprocessor.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/ams/core/matprocessor.py b/ams/core/matprocessor.py index 43d75631..c0855530 100644 --- a/ams/core/matprocessor.py +++ b/ams/core/matprocessor.py @@ -9,6 +9,9 @@ from ams.pypower.make import makePTDF, makeBdc # NOQA from ams.io.pypower import system2ppc # NOQA +from scipy.sparse import csr_matrix as c_sparse # NOQA +from scipy.sparse import lil_matrix as l_sparse # NOQA + logger = logging.getLogger(__name__) @@ -56,6 +59,14 @@ def v(self): """ Return the value of the parameter. """ + # NOTE: scipy.sparse matrix will return 2D array + # so we squeeze it here if only one row + if isinstance(self._v, (c_sparse, l_sparse)): + out = self._v.toarray() + if out.shape[0] == 1: + return np.squeeze(out) + else: + return out return self._v @property @@ -127,13 +138,13 @@ def make(self): idx=system.StaticLoad.get_idx()) idx_PD = system.PQ.find_idx(keys="bus", values=all_bus, allow_none=True, default=None) - self.pl._v = np.array(system.PQ.get(src='p0', attr='v', idx=idx_PD)) + self.pl._v = c_sparse(system.PQ.get(src='p0', attr='v', idx=idx_PD)) self.ql._v = np.array(system.PQ.get(src='q0', attr='v', idx=idx_PD)) row, col = np.meshgrid(all_bus, gen_bus) - self.Cg._v = (row == col).astype(int) + self.Cg._v = c_sparse((row == col).astype(int)) row, col = np.meshgrid(all_bus, load_bus) - self.Cl._v = (row == col).astype(int) + self.Cl._v = c_sparse((row == col).astype(int)) return True From 1bb956fc8a1bf07cbae46f92d0324c7d3c3dc4c5 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 4 Nov 2023 23:58:28 -0400 Subject: [PATCH 12/77] Fix var __repr__ --- ams/opt/omodel.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index 95b8c078..196b52e9 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -332,26 +332,25 @@ def parse(self): return True def __repr__(self): + span = [] if self.owner.n == 0: span = [] - - elif isinstance(self.v, np.ndarray): - if self.v.shape[0] == 1 or self.v.ndim == 1: - if len(self.v) <= 20: - span = f', v={self.v}' - else: - span = f', v in shape {self.v.shape}' - elif 1 <= self.owner.n <= 20: - span = f'a={self.a}, v={self.v}' - + span = f'a={self.a}, v={self.v.round(3)}' else: span = [] span.append(self.a[0]) span.append(self.a[-1]) span.append(self.a[1] - self.a[0]) span = ':'.join([str(i) for i in span]) - span = 'a=[' + span + ']' + span = ', a=[' + span + ']' + + if isinstance(self.v, np.ndarray): + if self.v.shape[0] == 1 or self.v.ndim == 1: + if len(self.v) <= 20: + span = f', v={self.v.round(3)}' + else: + span = f', v in shape {self.v.shape}' return f'{self.__class__.__name__}: {self.owner.__class__.__name__}.{self.name}{span}' From 689d0a16d99744d73349c0c65e60bd2ae0bb3f89 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 5 Nov 2023 00:08:01 -0400 Subject: [PATCH 13/77] Fix routine method graph with None input --- ams/routines/routine.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/ams/routines/routine.py b/ams/routines/routine.py index b2bedd71..6410a19b 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -954,18 +954,19 @@ def graph( # --- variables --- k = ytimes if ytimes is not None else 1 - if input.owner.class_name == "Bus": - logger.debug(f"Plotting <{input.name}> as vertex label.") - values = [f"${input.tex_name}$={round(k*v)}" for v in input.v] - vlabel = system.Bus.name.v - vout = [f"{vin}, {label}" for label, vin in zip(values, vlabel)] - vstyle["vertex_label"] = vout - elif input.owner.class_name == "Line": - logger.debug(f"Plotting <{input.name}> as edge label.") - values = [f"${input.tex_name}$={round(k*v)}" for v in input.v] - elabel = system.Line.name.v - eout = [f"{label}" for label, ein in zip(values, elabel)] - vstyle["edge_label"] = eout + if input is not None: + if input.owner.class_name == "Bus": + logger.debug(f"Plotting <{input.name}> as vertex label.") + values = [f"${input.tex_name}$={round(k*v)}" for v in input.v] + vlabel = system.Bus.name.v + vout = [f"{vin}, {label}" for label, vin in zip(values, vlabel)] + vstyle["vertex_label"] = vout + elif input.owner.class_name == "Line": + logger.debug(f"Plotting <{input.name}> as edge label.") + values = [f"${input.tex_name}$={round(k*v)}" for v in input.v] + elabel = system.Line.name.v + eout = [f"{label}" for label, ein in zip(values, elabel)] + vstyle["edge_label"] = eout if ax is None: _, ax = plt.subplots(figsize=figsize, dpi=dpi) From e84dac62ac8e9a843fffb5217cd977c4a234daab Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 5 Nov 2023 01:17:38 -0400 Subject: [PATCH 14/77] Add routine tests of graph --- ams/routines/routine.py | 25 +++++++++---- tests/test_routine.py | 79 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 6 deletions(-) diff --git a/ams/routines/routine.py b/ams/routines/routine.py index 6410a19b..fb8b5f90 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -818,6 +818,19 @@ def _initial_guess(self): """ raise NotImplementedError + def gmake(self, directed=True): + try: + import igraph as ig + except ImportError: + logger.error("Package `igraph` is not installed.") + return None + + system = self.system + edges = np.column_stack([system.Bus.idx2uid(system.Line.bus1.v), + system.Bus.idx2uid(system.Line.bus2.v)]) + g = ig.Graph(n=system.Bus.n, directed=directed, edges=edges) + return g + def graph( self, input: Optional[Union[RParam, Var]] = None, @@ -907,9 +920,7 @@ def graph( logger.error("Package `igraph` is not installed.") return None - system = self.system - edges = np.column_stack([system.Line.bus1.v, system.Line.bus2.v]) - g = ig.Graph(n=system.Bus.n, directed=directed, edges=edges) + g = self.gmake(directed=directed) # --- visual style --- vstyle = { @@ -927,7 +938,7 @@ def graph( # others **visual_style, } - + system = self.system # bus size gidx = system.PV.idx.v + system.Slack.idx.v gbus = system.StaticGen.get(src="bus", attr="v", idx=gidx) @@ -957,16 +968,18 @@ def graph( if input is not None: if input.owner.class_name == "Bus": logger.debug(f"Plotting <{input.name}> as vertex label.") - values = [f"${input.tex_name}$={round(k*v)}" for v in input.v] + values = [f"${input.tex_name}$={round(k*v, 3)}" for v in input.v] vlabel = system.Bus.name.v vout = [f"{vin}, {label}" for label, vin in zip(values, vlabel)] vstyle["vertex_label"] = vout elif input.owner.class_name == "Line": logger.debug(f"Plotting <{input.name}> as edge label.") - values = [f"${input.tex_name}$={round(k*v)}" for v in input.v] + values = [f"${input.tex_name}$={round(k*v, 3)}" for v in input.v] elabel = system.Line.name.v eout = [f"{label}" for label, ein in zip(values, elabel)] vstyle["edge_label"] = eout + else: + logger.error(f"Unsupported input type <{input.owner.class_name}>.") if ax is None: _, ax = plt.subplots(figsize=figsize, dpi=dpi) diff --git a/tests/test_routine.py b/tests/test_routine.py index f60bc909..0bb75f79 100644 --- a/tests/test_routine.py +++ b/tests/test_routine.py @@ -21,6 +21,19 @@ def wrapper(*args, **kwargs): return wrapper +def require_igraph(f): + """ + Decorator for skipping tests that require igraph. + """ + def wrapper(*args, **kwargs): + try: + import igraph + except ImportError: + raise unittest.SkipTest("igraph is not available.") + return f(*args, **kwargs) + return wrapper + + class TestRoutineMethods(unittest.TestCase): """ Test methods of `Routine`. @@ -114,3 +127,69 @@ def test_UC2(self): self.ss.UC2.run() self.assertEqual(self.ss.UC2.exit_code, 0, "Exit code is not 0.") + + +class TestRoutineGraph(unittest.TestCase): + """ + Test routine graph. + """ + + @require_igraph + def test_5bus_graph(self): + """ + Test routine graph of PJM 5-bus system. + """ + ss = ams.load(ams.get_case("5bus/pjm5bus_uced.xlsx"), + default_config=True, + no_output=True, + ) + _, g = ss.DCOPF.graph() + self.assertGreaterEqual(np.min(g.degree()), 1) + + @require_igraph + def test_ieee14_graph(self): + """ + Test routine graph of IEEE 14-bus system. + """ + ss = ams.load(ams.get_case("ieee14/ieee14_uced.xlsx"), + default_config=True, + no_output=True, + ) + _, g = ss.DCOPF.graph() + self.assertGreaterEqual(np.min(g.degree()), 1) + + @require_igraph + def test_ieee39_graph(self): + """ + Test routine graph of IEEE 39-bus system. + """ + ss = ams.load(ams.get_case("ieee39/ieee39_uced_esd1.xlsx"), + default_config=True, + no_output=True, + ) + _, g = ss.DCOPF.graph() + self.assertGreaterEqual(np.min(g.degree()), 1) + + @require_igraph + def test_npcc_graph(self): + """ + Test routine graph of NPCC 140-bus system. + """ + ss = ams.load(ams.get_case("npcc/npcc_uced.xlsx"), + default_config=True, + no_output=True, + ) + _, g = ss.DCOPF.graph() + self.assertGreaterEqual(np.min(g.degree()), 1) + + @require_igraph + def test_wecc_graph(self): + """ + Test routine graph of WECC 179-bus system. + """ + ss = ams.load(ams.get_case("wecc/wecc_uced.xlsx"), + default_config=True, + no_output=True, + ) + _, g = ss.DCOPF.graph() + self.assertGreaterEqual(np.min(g.degree()), 1) From d41c2cc61e2fdeda0fd69d138f94acb8c95325aa Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 5 Nov 2023 01:18:22 -0400 Subject: [PATCH 15/77] Refactor DCOPF --- ams/routines/dcopf.py | 15 ++++++++++++++- ams/routines/dopf.py | 6 ------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/ams/routines/dcopf.py b/ams/routines/dcopf.py index 135520af..d055e4f3 100644 --- a/ams/routines/dcopf.py +++ b/ams/routines/dcopf.py @@ -3,7 +3,9 @@ """ import logging # NOQA +import numpy as np # NOQA from ams.core.param import RParam # NOQA +from ams.core.service import NumOp # NOQA from ams.routines.routine import RoutineData, RoutineModel # NOQA @@ -52,6 +54,9 @@ def __init__(self): self.Cg = RParam(info='connection matrix for Gen and Bus', name='Cg', tex_name=r'C_{g}', model='mats', src='Cg',) + self.Cft = RParam(info='connection matrix for Line and Bus', + name='Cft', tex_name=r'C_{ft}', + model='mats', src='Cft',) # --- load --- self.pl = RParam(info='nodal active load (system base)', name='pl', tex_name=r'p_{l}', @@ -172,13 +177,21 @@ def __init__(self, system, config): self.pn = Var(info='Bus active power injection (system base)', unit='p.u.', name='pn', tex_name=r'p_{n}', model='Bus',) + self.plf = Var(info='line active power', + name='plf', tex_name=r'p_{lf}', unit='p.u.', + model='Line',) # --- constraints --- + self.CftT = NumOp(u=self.Cft, + fun=np.transpose, + name='CftT', + tex_name=r'C_{ft}^{T}', + info='transpose of connection matrix',) self.pb = Constraint(name='pb', info='power balance', e_str='sum(pl) - sum(pg)', type='eq',) self.pinj = Constraint(name='pinj', info='nodal power injection', - e_str='Cg@(pn - pl) - pg', + e_str='CftT@plf - pl - pn', type='eq',) self.lub = Constraint(name='lub', info='Line limits upper bound', e_str='PTDF @ (pn - pl) - rate_a', diff --git a/ams/routines/dopf.py b/ams/routines/dopf.py index 812e9571..5e3844f7 100644 --- a/ams/routines/dopf.py +++ b/ams/routines/dopf.py @@ -40,9 +40,6 @@ def __init__(self): self.qmin = RParam(info='generator minimum reactive power (system base)', name='qmin', tex_name=r'q_{min}', unit='p.u.', model='StaticGen', src='qmin',) - self.Cft = RParam(info='connection matrix for Line and Bus', - name='Cft', tex_name=r'C_{ft}', - model='mats', src='Cft',) class LDOPFModel(DCOPFModel): @@ -75,9 +72,6 @@ def __init__(self, system, config): e_str='-vsq + vmin**2', type='uq',) - self.plf = Var(info='line active power', - name='plf', tex_name=r'p_{lf}', unit='p.u.', - model='Line',) self.qlf = Var(info='line reactive power', name='qlf', tex_name=r'q_{lf}', unit='p.u.', model='Line',) From d6b2c191568df67e376c81b4303e24b96bb5b800 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 5 Nov 2023 11:29:11 -0500 Subject: [PATCH 16/77] Refactor require_igraph --- ams/routines/routine.py | 32 ++++++++++++++++++++++++-------- ams/shared.py | 9 +++++++++ 2 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 ams/shared.py diff --git a/ams/routines/routine.py b/ams/routines/routine.py index fb8b5f90..5830280e 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -7,21 +7,41 @@ from collections import OrderedDict import numpy as np +from functools import wraps import matplotlib.pyplot as plt from andes.core import Config from andes.shared import deg2rad # NOQA from andes.utils.misc import elapsed -from ams.core.param import RParam -from ams.opt.omodel import OModel, Var, Constraint, Objective +from ams.core.param import RParam from ams.core.symprocessor import SymProcessor from ams.core.documenter import RDocumenter from ams.core.service import RBaseService, ValueService +from ams.opt.omodel import OModel, Var, Constraint, Objective + +from ams.shared import igraph as ig + logger = logging.getLogger(__name__) +def require_igraph(f): + """ + Decorator for functions that require igraph. + """ + + @wraps(f) + def wrapper(*args, **kwargs): + try: + getattr(ig, '__version__') + except AttributeError: + logger.error("Package `igraph` is not installed.") + return f(*args, **kwargs) + + return wrapper + + class RoutineData: """ Class to hold routine parameters. @@ -818,19 +838,15 @@ def _initial_guess(self): """ raise NotImplementedError + @require_igraph def gmake(self, directed=True): - try: - import igraph as ig - except ImportError: - logger.error("Package `igraph` is not installed.") - return None - system = self.system edges = np.column_stack([system.Bus.idx2uid(system.Line.bus1.v), system.Bus.idx2uid(system.Line.bus2.v)]) g = ig.Graph(n=system.Bus.n, directed=directed, edges=edges) return g + @require_igraph def graph( self, input: Optional[Union[RParam, Var]] = None, diff --git a/ams/shared.py b/ams/shared.py new file mode 100644 index 00000000..e062d5bb --- /dev/null +++ b/ams/shared.py @@ -0,0 +1,9 @@ +""" +Shared constants and delayed imports. + +This module is supplementary to the ``andes.shared`` module. +""" + +from andes.utils.lazyimport import LazyImport + +igraph = LazyImport('import igraph') From b0f0a78132b6677615a38317f02fd6c3ad889e07 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 5 Nov 2023 12:02:37 -0500 Subject: [PATCH 17/77] Minor refactor routine tests --- ams/routines/routine.py | 51 ++++++++++++++++++++--------------------- ams/shared.py | 47 ++++++++++++++++++++++++++++++++++++- tests/test_routine.py | 38 ++++++++++-------------------- 3 files changed, 83 insertions(+), 53 deletions(-) diff --git a/ams/routines/routine.py b/ams/routines/routine.py index 5830280e..a9af5ed1 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -7,7 +7,6 @@ from collections import OrderedDict import numpy as np -from functools import wraps import matplotlib.pyplot as plt from andes.core import Config @@ -21,27 +20,12 @@ from ams.opt.omodel import OModel, Var, Constraint, Objective from ams.shared import igraph as ig +from ams.shared import require_igraph logger = logging.getLogger(__name__) -def require_igraph(f): - """ - Decorator for functions that require igraph. - """ - - @wraps(f) - def wrapper(*args, **kwargs): - try: - getattr(ig, '__version__') - except AttributeError: - logger.error("Package `igraph` is not installed.") - return f(*args, **kwargs) - - return wrapper - - class RoutineData: """ Class to hold routine parameters. @@ -839,7 +823,20 @@ def _initial_guess(self): raise NotImplementedError @require_igraph - def gmake(self, directed=True): + def igmake(self, directed=True): + """ + Build an igraph object from the system. + + Parameters + ---------- + directed: bool + Whether the graph is directed. + + Returns + ------- + igraph.Graph + An igraph object. + """ system = self.system edges = np.column_stack([system.Bus.idx2uid(system.Line.bus1.v), system.Bus.idx2uid(system.Line.bus2.v)]) @@ -847,7 +844,7 @@ def gmake(self, directed=True): return g @require_igraph - def graph( + def igraph( self, input: Optional[Union[RParam, Var]] = None, ytimes: Optional[float] = None, @@ -870,7 +867,7 @@ def graph( **visual_style, ): """ - Plot a system graph, with optional input. + Plot a system uging `g.plot()` of `igraph`, with optional input. For now, only support plotting of Bus and Line elements as input. Examples @@ -929,14 +926,16 @@ def graph( Matplotlib axes. visual_style: dict, optional Visual style, see ``igraph.plot`` for details. + + Returns + ------- + plt.Axes + Matplotlib axes. + igraph.Graph + An igraph object. """ - try: - import igraph as ig - except ImportError: - logger.error("Package `igraph` is not installed.") - return None - g = self.gmake(directed=directed) + g = self.igmake(directed=directed) # --- visual style --- vstyle = { diff --git a/ams/shared.py b/ams/shared.py index e062d5bb..19739c06 100644 --- a/ams/shared.py +++ b/ams/shared.py @@ -4,6 +4,51 @@ This module is supplementary to the ``andes.shared`` module. """ +import logging +from functools import wraps + +import cvxpy as cp + from andes.utils.lazyimport import LazyImport -igraph = LazyImport('import igraph') + +logger = logging.getLogger(__name__) + + +igraph = LazyImport("import igraph") + +# NOTE: copied from CVXPY documentation +MIP_SOLVERS = ['CBC', 'COPT', 'GLPK_MI', 'CPLEX', 'GUROBI', + 'MOSEK', 'SCIP', 'XPRESS', 'SCIPY'] + +INSTALLED_SOLVERS = cp.installed_solvers() + + +def require_igraph(f): + """ + Decorator for functions that require igraph. + """ + + @wraps(f) + def wrapper(*args, **kwargs): + try: + getattr(igraph, "__version__") + except AttributeError: + logger.error("Package `igraph` is not installed.") + return f(*args, **kwargs) + + return wrapper + + +def require_MIP_solver(f): + """ + Decorator for functions that require MIP solver. + """ + + @wraps(f) + def wrapper(*args, **kwargs): + if not any(s in MIP_SOLVERS for s in INSTALLED_SOLVERS): + raise ImportError("No MIP solver is available.") + return f(*args, **kwargs) + + return wrapper diff --git a/tests/test_routine.py b/tests/test_routine.py index 0bb75f79..7dafa7b6 100644 --- a/tests/test_routine.py +++ b/tests/test_routine.py @@ -1,36 +1,22 @@ +from functools import wraps import unittest import numpy as np import ams -import cvxpy as cp +from ams.shared import require_igraph, MIP_SOLVERS, INSTALLED_SOLVERS def require_MIP_solver(f): """ - Decorator for skipping tests that require MIP solver. + Decorator for functions 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 - -def require_igraph(f): - """ - Decorator for skipping tests that require igraph. - """ + @wraps(f) def wrapper(*args, **kwargs): - try: - import igraph - except ImportError: - raise unittest.SkipTest("igraph is not available.") + if not any(s in MIP_SOLVERS for s in INSTALLED_SOLVERS): + raise ModuleNotFoundError("No MIP solver is available.") return f(*args, **kwargs) + return wrapper @@ -143,7 +129,7 @@ def test_5bus_graph(self): default_config=True, no_output=True, ) - _, g = ss.DCOPF.graph() + _, g = ss.DCOPF.igraph() self.assertGreaterEqual(np.min(g.degree()), 1) @require_igraph @@ -155,7 +141,7 @@ def test_ieee14_graph(self): default_config=True, no_output=True, ) - _, g = ss.DCOPF.graph() + _, g = ss.DCOPF.igraph() self.assertGreaterEqual(np.min(g.degree()), 1) @require_igraph @@ -167,7 +153,7 @@ def test_ieee39_graph(self): default_config=True, no_output=True, ) - _, g = ss.DCOPF.graph() + _, g = ss.DCOPF.igraph() self.assertGreaterEqual(np.min(g.degree()), 1) @require_igraph @@ -179,7 +165,7 @@ def test_npcc_graph(self): default_config=True, no_output=True, ) - _, g = ss.DCOPF.graph() + _, g = ss.DCOPF.igraph() self.assertGreaterEqual(np.min(g.degree()), 1) @require_igraph @@ -191,5 +177,5 @@ def test_wecc_graph(self): default_config=True, no_output=True, ) - _, g = ss.DCOPF.graph() + _, g = ss.DCOPF.igraph() self.assertGreaterEqual(np.min(g.degree()), 1) From 94d8e6d89b62dd4ccddb464f91bfe886ada7d1d5 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 5 Nov 2023 12:03:13 -0500 Subject: [PATCH 18/77] Typo --- ams/shared.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/ams/shared.py b/ams/shared.py index 19739c06..345eaf05 100644 --- a/ams/shared.py +++ b/ams/shared.py @@ -3,7 +3,6 @@ This module is supplementary to the ``andes.shared`` module. """ - import logging from functools import wraps @@ -40,15 +39,4 @@ def wrapper(*args, **kwargs): return wrapper -def require_MIP_solver(f): - """ - Decorator for functions that require MIP solver. - """ - - @wraps(f) - def wrapper(*args, **kwargs): - if not any(s in MIP_SOLVERS for s in INSTALLED_SOLVERS): - raise ImportError("No MIP solver is available.") - return f(*args, **kwargs) - return wrapper From cf0b1337946c6fc6af033e33c05adad20e89ac5f Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 5 Nov 2023 12:07:38 -0500 Subject: [PATCH 19/77] Revise versioninfo --- ams/main.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/ams/main.py b/ams/main.py index e5a4c21b..fcffe07d 100644 --- a/ams/main.py +++ b/ams/main.py @@ -515,23 +515,17 @@ def versioninfo(): import numpy as np import cvxpy import andes + from ams.shared import INSTALLED_SOLVERS versions = {'Python': platform.python_version(), 'ams': get_versions()['version'], 'andes': andes.__version__, 'numpy': np.__version__, 'cvxpy': cvxpy.__version__, + 'solvers': ', '.join(INSTALLED_SOLVERS), } 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}") From 7c3fba15e1053f897c763eddf79bfa9e0dd6b480 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 6 Nov 2023 06:46:40 -0500 Subject: [PATCH 20/77] Add more igraph parameters --- ams/routines/routine.py | 75 +++++++++++++++++++++++++++++++---------- 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/ams/routines/routine.py b/ams/routines/routine.py index a9af5ed1..941c0046 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -848,22 +848,35 @@ def igraph( self, input: Optional[Union[RParam, Var]] = None, ytimes: Optional[float] = None, + decimal: Optional[int] = 6, directed: Optional[bool] = True, dpi: Optional[int] = 100, figsize: Optional[tuple] = None, adjust_bus: Optional[bool] = False, gen_color: Optional[str] = "red", rest_color: Optional[str] = "black", - vertex_size: Optional[int] = 10.0, - vertex_label_size: Optional[int] = 8, - vertex_label_dist: Optional[int] = -1.5, - vertex_label_angle: Optional[int] = 10.2, - edge_arrow_size: Optional[int] = 8, - edge_width: Optional[int] = 1, + vertex_shape: Optional[str] = "circle", + vertex_font: Optional[str] = None, + no_vertex_label: Optional[bool] = False, + vertex_label: Optional[Union[str, list]] = None, + vertex_size: Optional[float] = None, + vertex_label_size: Optional[float] = None, + vertex_label_dist: Optional[float] = 1.5, + vertex_label_angle: Optional[float] = 10.2, + edge_arrow_size: Optional[float] = None, + edge_arrow_width: Optional[float] = None, + edge_width: Optional[float] = None, edge_align_label: Optional[bool] = True, + edge_background: Optional[str] = None, + edge_color: Optional[str] = None, + edge_curved: Optional[bool] = False, + edge_font: Optional[str] = None, + edge_label: Optional[Union[str, list]] = None, layout: Optional[str] = "rt", autocurve: Optional[bool] = True, ax: Optional[plt.Axes] = None, + title: Optional[str] = None, + title_loc: Optional[str] = None, **visual_style, ): """ @@ -904,22 +917,32 @@ def igraph( Color of the generator bus. rest_color: str, optional Color of the rest buses. - vertex_size: int, optional + no_vertex_label: bool, optional + Whether to show vertex labels. + vertex_shape: str, optional + Shape of the vertices. + vertex_font: str, optional + Font of the vertices. + vertex_size: float, optional Size of the vertices. - vertex_label_size: int, optional + vertex_label_size: float, optional Size of the vertex labels. - vertex_label_dist: int, optional + vertex_label_dist: float, optional Distance of the vertex labels. - vertex_label_angle: int, optional + vertex_label_angle: float, optional Angle of the vertex labels. - edge_arrow_size: int, optional + edge_arrow_size: float, optional Size of the edge arrows. - edge_width: int, optional + edge_arrow_width: float, optional + Width of the edge arrows. + edge_width: float, optional Width of the edges. edge_align_label: bool, optional Whether to align the edge labels. + edge_background: str, optional + RGB colored rectangle background of the edge labels. layout: str, optional - Layout of the graph. + Layout of the graph, ['rt', 'kk', 'fr', 'drl', 'lgl', 'circle', 'grid_fr']. autocurve: bool, optional Whether to use autocurve. ax: plt.Axes, optional @@ -942,18 +965,32 @@ def igraph( # layout style "layout": layout, # vertices + "vertex_shape": vertex_shape, + "vertex_font": vertex_font, "vertex_size": vertex_size, + "vertex_label": vertex_label, "vertex_label_size": vertex_label_size, "vertex_label_dist": vertex_label_dist, "vertex_label_angle": vertex_label_angle, # edges "edge_arrow_size": edge_arrow_size, + "edge_arrow_width": edge_arrow_width, "edge_width": edge_width, "edge_align_label": edge_align_label, + "edge_background": edge_background, + "edge_color": edge_color, + "edge_curved": edge_curved, + "edge_font": edge_font, + "edge_label": edge_label, # others **visual_style, } system = self.system + # bus name, will be overwritten if input is not None + vstyle["vertex_name"] = system.Bus.name.v + if vertex_label is None: + vstyle["vertex_label"] = None if no_vertex_label else system.Bus.name.v + # bus size gidx = system.PV.idx.v + system.Slack.idx.v gbus = system.StaticGen.get(src="bus", attr="v", idx=gidx) @@ -983,13 +1020,11 @@ def igraph( if input is not None: if input.owner.class_name == "Bus": logger.debug(f"Plotting <{input.name}> as vertex label.") - values = [f"${input.tex_name}$={round(k*v, 3)}" for v in input.v] - vlabel = system.Bus.name.v - vout = [f"{vin}, {label}" for label, vin in zip(values, vlabel)] - vstyle["vertex_label"] = vout + values = [f"${input.tex_name}$={round(k*v, decimal)}" for v in input.v] + vstyle["vertex_label"] = values elif input.owner.class_name == "Line": logger.debug(f"Plotting <{input.name}> as edge label.") - values = [f"${input.tex_name}$={round(k*v, 3)}" for v in input.v] + values = [f"${input.tex_name}$={round(k*v, decimal)}" for v in input.v] elabel = system.Line.name.v eout = [f"{label}" for label, ein in zip(values, elabel)] vstyle["edge_label"] = eout @@ -998,5 +1033,9 @@ def igraph( if ax is None: _, ax = plt.subplots(figsize=figsize, dpi=dpi) + default_name = self.class_name + if input is not None: + default_name += f"\n${input.tex_name}$" + f" [${input.unit}$]" + ax.set_title(title if title else default_name, loc=title_loc) ig.plot(g, autocurve=autocurve, target=ax, **vstyle) return ax, g From 1a8ec7410f26d1c680dfef5e49f98edc4664ffff Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 6 Nov 2023 06:46:59 -0500 Subject: [PATCH 21/77] Remove unnecessary extra dependencies --- requirements-extra.txt | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/requirements-extra.txt b/requirements-extra.txt index 9a60f48f..48a96c77 100644 --- a/requirements-extra.txt +++ b/requirements-extra.txt @@ -8,17 +8,13 @@ # Only one `#` is allowed per line. Lines starting with `#` are ignored. pytest==7.0.1 # dev -flake8 # dev -sphinx # dev, doc -pydata-sphinx-theme # dev, doc -numpydoc # dev, doc -sphinx-copybutton # dev, doc -sphinx-panels # dev, doc -pydata-sphinx-theme # dev, doc -myst-parser==0.15.2 # dev, doc -myst-nb # dev, doc -graphviz # doc -pydeps # doc -scip # opt -pyscipopt # opt -gurobipy # opt \ No newline at end of file +flake8 # dev +igraph # dev +sphinx # dev, doc +pydata-sphinx-theme # dev, doc +numpydoc # dev, doc +sphinx-copybutton # dev, doc +sphinx-panels # dev, doc +pydata-sphinx-theme # dev, doc +myst-parser==0.15.2 # dev, doc +myst-nb # dev, doc \ No newline at end of file From d1ee567b7a638ba89bf5fc72bb780620527e81d0 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 6 Nov 2023 09:43:36 -0500 Subject: [PATCH 22/77] Minor fix --- ams/routines/dopf.py | 2 +- ams/routines/uc.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ams/routines/dopf.py b/ams/routines/dopf.py index 5e3844f7..3fbd981f 100644 --- a/ams/routines/dopf.py +++ b/ams/routines/dopf.py @@ -94,7 +94,7 @@ def __init__(self, system, config): self.lvd = Constraint(name='lvd', info='line voltage drop', - e_str='Cft@vsq - (r * pl + x * qlf)', + e_str='Cft@vsq - (r * plf + x * qlf)', type='eq',) # --- objective --- diff --git a/ams/routines/uc.py b/ams/routines/uc.py index 1188944a..ce92a824 100644 --- a/ams/routines/uc.py +++ b/ams/routines/uc.py @@ -217,7 +217,7 @@ def _initial_guess(self): def init(self, **kwargs): self._initial_guess() - super().init(**kwargs) + return super().init(**kwargs) class UC(UCData, UCModel): From 3259ff32a180f5d72bfa644ac9cc5caa36dcee57 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 6 Nov 2023 09:51:53 -0500 Subject: [PATCH 23/77] Typo --- tests/test_service.py | 125 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 tests/test_service.py diff --git a/tests/test_service.py b/tests/test_service.py new file mode 100644 index 00000000..67b174ef --- /dev/null +++ b/tests/test_service.py @@ -0,0 +1,125 @@ +from functools import wraps +import unittest +import numpy as np + +from scipy.sparse import csr_matrix as c_sparse # NOQA +from scipy.sparse import lil_matrix as l_sparse # NOQA + +import ams +from ams.core.matprocessor import MatProcessor +from ams.core.service import NumOp, ZonalSum + + +class TestMatProcessor(unittest.TestCase): + """ + Test functionality of MatProcessor. + """ + + def setUp(self) -> None: + self.ss = ams.load( + ams.get_case("ieee39/ieee39_uced_esd1.xlsx"), + default_config=True, + no_output=True, + ) + self.nR = self.ss.Region.n + self.nB = self.ss.Bus.n + self.nl = self.ss.Line.n + + self.mats = MatProcessor(self.ss) + self.mats.make() + + def test_PTDF(self): + """ + Test `PTDF`. + """ + # self.assertIsInstance(self.mats.PTDF._v, (c_sparse, l_sparse)) + self.assertIsInstance(self.mats.PTDF.v, np.ndarray) + self.assertEqual(self.mats.PTDF.v.shape, (self.nl, self.nB)) + + def test_Cft(self): + """ + Test `Cft`. + """ + self.assertIsInstance(self.mats.Cft._v, (c_sparse, l_sparse)) + self.assertIsInstance(self.mats.Cft.v, np.ndarray) + self.assertEqual(self.mats.Cft.v.max(), 1) + + def test_pl(self): + """ + Test `pl`. + """ + self.assertEqual(self.mats.pl.v.ndim, 1) + + +class TestService(unittest.TestCase): + """ + Test functionality of Services. + """ + + def setUp(self) -> None: + self.ss = ams.load( + ams.get_case("ieee39/ieee39_uced_esd1.xlsx"), + default_config=True, + no_output=True, + ) + self.nR = self.ss.Region.n + self.nB = self.ss.Bus.n + self.nl = self.ss.Line.n + + def test_NumOp_norfun(self): + """ + Test `NumOp` without return function. + """ + CftT = NumOp(u=self.ss.mats.Cft, fun=np.transpose) + np.testing.assert_array_equal(CftT.v.transpose(), self.ss.mats.Cft.v) + + def test_NumOp_rfun(self): + """ + Test `NumOp` with return function. + """ + CftTT = NumOp(u=self.ss.mats.Cft, fun=np.transpose, rfun=np.transpose) + np.testing.assert_array_equal(CftTT.v, self.ss.mats.Cft.v) + + def test_NumOp_ArrayOut(self): + """ + Test `NumOp` non-array output. + """ + M = NumOp( + u=self.ss.PV.pmax, + fun=np.max, + rfun=np.dot, + rargs=dict(b=10), + array_out=True, + ) + self.assertIsInstance(M.v, np.ndarray) + M2 = NumOp( + u=self.ss.PV.pmax, + fun=np.max, + rfun=np.dot, + rargs=dict(b=10), + array_out=False, + ) + self.assertIsInstance(M2.v, (int, float)) + + def test_NumOpDual(self): + """ + Test `NumOpDual`. + """ + pass + + def test_ZonalSum(self): + """ + Test `ZonalSum`. + """ + ds = ZonalSum( + u=self.ss.RTED.zb, + zone="Region", + name="ds", + tex_name=r"S_{d}", + info="Sum pl vector in shape of zone", + ) + ds.rtn = self.ss.RTED + # check if the shape is correct + np.testing.assert_array_equal(ds.v.shape, (self.nR, self.nB)) + # check if the values are correct + self.assertTrue(np.all(ds.v.sum(axis=1) <= np.array([self.nB, self.nB]))) From 0d315a5337d216f9b7c775f596a451a905b10b29 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 6 Nov 2023 09:52:14 -0500 Subject: [PATCH 24/77] Typo --- ams/core/service.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ams/core/service.py b/ams/core/service.py index a0547dc3..efc76fae 100644 --- a/ams/core/service.py +++ b/ams/core/service.py @@ -245,6 +245,8 @@ class NumOp(ROperationService): Keyword arguments to pass to ``rfun``. expand_dims : int, optional Expand the dimensions of the output array along a specified axis. + array_out : bool, optional + Whether to force the output to be an array. """ def __init__(self, @@ -317,8 +319,8 @@ class NumExpandDim(NumOp): Description. vtype : Type, optional Variable type. - model : str, optional - Model name. + array_out : bool, optional + Whether to force the output to be an array. """ def __init__(self, @@ -375,6 +377,8 @@ class NumOpDual(NumOp): Keyword arguments to pass to ``rfun``. expand_dims : int, optional Expand the dimensions of the output array along a specified axis. + array_out : bool, optional + Whether to force the output to be an array. """ def __init__(self, From 94328ccee16f9ac19c9c1d429a09058b262c400d Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 6 Nov 2023 09:52:22 -0500 Subject: [PATCH 25/77] Typo --- ams/routines/routine.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ams/routines/routine.py b/ams/routines/routine.py index 941c0046..556c62e4 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -266,8 +266,8 @@ def _data_check(self): no_input.append(rname) owner_list.append(rparam.owner.class_name) if len(no_input) > 0: - msg = f"Following models have no input: {set(owner_list)}" - logger.error(msg) + msg = f"Following models are missing in input: {set(owner_list)}" + logger.warning(msg) return False # TODO: add data validation for RParam, typical range, etc. return True From b252a65be15769fb37844f51bb92da7bb79d2748 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 6 Nov 2023 09:52:34 -0500 Subject: [PATCH 26/77] Refactor routine tests --- ams/shared.py | 17 +++++++-- tests/test_routine.py | 85 +++++++++---------------------------------- 2 files changed, 31 insertions(+), 71 deletions(-) diff --git a/ams/shared.py b/ams/shared.py index 345eaf05..0c5a3090 100644 --- a/ams/shared.py +++ b/ams/shared.py @@ -23,6 +23,20 @@ INSTALLED_SOLVERS = cp.installed_solvers() +def require_MIP_solver(f): + """ + Decorator for functions that require MIP solver. + """ + + @wraps(f) + def wrapper(*args, **kwargs): + if not any(s in MIP_SOLVERS for s in INSTALLED_SOLVERS): + raise ModuleNotFoundError("No MIP solver is available.") + return f(*args, **kwargs) + + return wrapper + + def require_igraph(f): """ Decorator for functions that require igraph. @@ -37,6 +51,3 @@ def wrapper(*args, **kwargs): return f(*args, **kwargs) return wrapper - - - diff --git a/tests/test_routine.py b/tests/test_routine.py index 7dafa7b6..cffa367e 100644 --- a/tests/test_routine.py +++ b/tests/test_routine.py @@ -1,23 +1,8 @@ -from functools import wraps import unittest import numpy as np import ams -from ams.shared import require_igraph, MIP_SOLVERS, INSTALLED_SOLVERS - - -def require_MIP_solver(f): - """ - Decorator for functions that require MIP solver. - """ - - @wraps(f) - def wrapper(*args, **kwargs): - if not any(s in MIP_SOLVERS for s in INSTALLED_SOLVERS): - raise ModuleNotFoundError("No MIP solver is available.") - return f(*args, **kwargs) - - return wrapper +from ams.shared import require_igraph class TestRoutineMethods(unittest.TestCase): @@ -43,76 +28,40 @@ def test_routine_get(self): Test `Routine.get()` method. """ - # get a rparam value + # get an rparam value np.testing.assert_equal(self.ss.DCOPF.get('ug', 'PV_30'), 1) + # get an unpacked var value 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')) -class TestRoutineSolve(unittest.TestCase): +class TestRoutineInit(unittest.TestCase): """ Test solving routines. """ def setUp(self) -> None: - self.ss = ams.load(ams.get_case("ieee39/ieee39_uced_esd1.xlsx"), + self.s1 = ams.load(ams.get_case("ieee39/ieee39_uced_esd1.xlsx"), + default_config=True, + no_output=True, + ) + self.s2 = ams.load(ams.get_case("ieee123/ieee123_regcv1.xlsx"), default_config=True, no_output=True, ) - 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.") - - 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_UC(self): - """ - Test `UC.run()`. - """ - - self.ss.UC.run() - self.assertEqual(self.ss.UC.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.") - - @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.") - - @require_MIP_solver - def test_UC2(self): + def test_Init(self): """ - Test `UC2.run()`. + Test `routine.init()`. """ - - self.ss.UC2.run() - self.assertEqual(self.ss.UC2.exit_code, 0, "Exit code is not 0.") + # NOTE: for DED, using ieee123 as a test case + for rtn in self.s1.routines.values(): + if rtn.type == 'DED': + rtn = getattr(self.s2, rtn.class_name) + self.assertTrue(rtn.init(force=True), + f"{rtn.class_name} initialization failed!") class TestRoutineGraph(unittest.TestCase): From d953c0f0501a910d488a5fd9c8bbebe5013f3685 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 6 Nov 2023 15:57:47 -0500 Subject: [PATCH 27/77] Minor refactor on routine tests --- tests/test_routine.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_routine.py b/tests/test_routine.py index cffa367e..5886d756 100644 --- a/tests/test_routine.py +++ b/tests/test_routine.py @@ -58,8 +58,10 @@ def test_Init(self): """ # NOTE: for DED, using ieee123 as a test case for rtn in self.s1.routines.values(): - if rtn.type == 'DED': + if not rtn._data_check(): rtn = getattr(self.s2, rtn.class_name) + if not rtn._data_check(): + continue # TODO: here should be a warning? self.assertTrue(rtn.init(force=True), f"{rtn.class_name} initialization failed!") From 5b195edb52bebee313d981c1f7f8fad9f9729c5e Mon Sep 17 00:00:00 2001 From: jinningwang Date: Tue, 7 Nov 2023 17:37:05 -0500 Subject: [PATCH 28/77] Add more tests into service test --- tests/test_service.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/tests/test_service.py b/tests/test_service.py index 67b174ef..1e53fd75 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -1,4 +1,3 @@ -from functools import wraps import unittest import numpy as np @@ -6,8 +5,8 @@ from scipy.sparse import lil_matrix as l_sparse # NOQA import ams -from ams.core.matprocessor import MatProcessor -from ams.core.service import NumOp, ZonalSum +from ams.core.matprocessor import MatProcessor, MParam +from ams.core.service import NumOp, NumOpDual, ZonalSum class TestMatProcessor(unittest.TestCase): @@ -28,11 +27,20 @@ def setUp(self) -> None: self.mats = MatProcessor(self.ss) self.mats.make() + def test_MParam(self): + """ + Test `MParam`. + """ + one_vec = MParam(v=c_sparse(np.ones(self.ss.Bus.n))) + # check if `_v` is `c_sparse` instance + self.assertIsInstance(one_vec._v, c_sparse) + # check if `v` is 1D-array + self.assertEqual(one_vec.v.shape, (self.ss.Bus.n,)) + def test_PTDF(self): """ Test `PTDF`. """ - # self.assertIsInstance(self.mats.PTDF._v, (c_sparse, l_sparse)) self.assertIsInstance(self.mats.PTDF.v, np.ndarray) self.assertEqual(self.mats.PTDF.v.shape, (self.nl, self.nB)) @@ -91,7 +99,6 @@ def test_NumOp_ArrayOut(self): rargs=dict(b=10), array_out=True, ) - self.assertIsInstance(M.v, np.ndarray) M2 = NumOp( u=self.ss.PV.pmax, fun=np.max, @@ -99,13 +106,18 @@ def test_NumOp_ArrayOut(self): rargs=dict(b=10), array_out=False, ) + self.assertIsInstance(M.v, np.ndarray) self.assertIsInstance(M2.v, (int, float)) def test_NumOpDual(self): """ Test `NumOpDual`. """ - pass + p_vec = MParam(v=self.ss.mats.pl.v) + one_vec = MParam(v=np.ones(self.ss.Bus.n)) + p_sum = NumOpDual(u=p_vec, u2=one_vec, + fun=np.multiply, rfun=np.sum) + self.assertEqual(p_sum.v, self.ss.PQ.p0.v.sum()) def test_ZonalSum(self): """ From 23b6565470194604027af53ab598133257bf8482 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Tue, 7 Nov 2023 21:43:19 -0500 Subject: [PATCH 29/77] Refactor RParam, Var, Constr __repr__ --- ams/core/param.py | 34 +--------------------------------- ams/opt/omodel.py | 26 ++++---------------------- 2 files changed, 5 insertions(+), 55 deletions(-) diff --git a/ams/core/param.py b/ams/core/param.py index 77488b59..a4e5c3ca 100644 --- a/ams/core/param.py +++ b/ams/core/param.py @@ -158,39 +158,7 @@ def class_name(self): return self.__class__.__name__ def __repr__(self): - if self.is_ext: - span = '' - if isinstance(self.v, np.ndarray): - if 1 in self.v.shape: - if len(self.v) <= 20: - span = f', v={self.v}' - else: - if len(self.v) <= 20: - span = f', v={self.v}' - else: - span = f', v in length of {len(self.v)}' - return f'{self.__class__.__name__}: {self.name}{span}' - else: - span = '' - if 1 <= self.n <= 20: - span = f', v={self.v}' - if hasattr(self, 'vin') and (self.vin is not None): - span += f', v in length of {self.vin}' - - if isinstance(self.v, np.ndarray): - if self.v.shape[0] == 1 or self.v.ndim == 1: - if len(self.v) <= 20: - span = f', v={self.v}' - else: - span = f', v in shape {self.v.shape}' - elif isinstance(self.v, list): - if len(self.v) <= 20: - span = f', v={self.v}' - else: - span = f', v in length of {len(self.v)}' - else: - span = f', v={self.v}' - return f'{self.__class__.__name__}: {self.owner.__class__.__name__}.{self.name}{span}' + return f'{self.__class__.__name__}: {self.owner.__class__.__name__}.{self.name}' def get_idx(self): """ diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index 196b52e9..28cf47fe 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -332,27 +332,7 @@ def parse(self): return True def __repr__(self): - span = [] - if self.owner.n == 0: - span = [] - elif 1 <= self.owner.n <= 20: - span = f'a={self.a}, v={self.v.round(3)}' - else: - span = [] - span.append(self.a[0]) - span.append(self.a[-1]) - span.append(self.a[1] - self.a[0]) - span = ':'.join([str(i) for i in span]) - span = ', a=[' + span + ']' - - if isinstance(self.v, np.ndarray): - if self.v.shape[0] == 1 or self.v.ndim == 1: - if len(self.v) <= 20: - span = f', v={self.v.round(3)}' - else: - span = f', v in shape {self.v.shape}' - - return f'{self.__class__.__name__}: {self.owner.__class__.__name__}.{self.name}{span}' + return f'{self.__class__.__name__}: {self.owner.__class__.__name__}.{self.name}' class Constraint(OptzBase): @@ -444,7 +424,9 @@ def parse(self, no_code=True): def __repr__(self): enabled = 'ON' if self.name in self.om.constrs else 'OFF' - return f"[{enabled}]: {self.e_str}" + out = f"[{enabled}]: {self.e_str}" + out += " =0" if self.type == 'eq' else " <=0" + return out @property def v(self): From 94a485f5e8992f7ce91d3839ff6203b306ae77f9 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Tue, 7 Nov 2023 22:32:17 -0500 Subject: [PATCH 30/77] Change init default param --- ams/routines/routine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ams/routines/routine.py b/ams/routines/routine.py index 556c62e4..fffb2f84 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -272,7 +272,7 @@ def _data_check(self): # TODO: add data validation for RParam, typical range, etc. return True - def init(self, force=False, no_code=True, **kwargs): + def init(self, force=True, no_code=True, **kwargs): """ Setup optimization model. From 4b882ed07bc08479526d3125bdfba4ad29ea8eac Mon Sep 17 00:00:00 2001 From: jinningwang Date: Tue, 7 Nov 2023 22:32:30 -0500 Subject: [PATCH 31/77] Add two examples --- examples/ex1.ipynb | 940 ++++++++++++++++++--------------------------- examples/ex2.ipynb | 701 +++++++++++++++++++++++++++++++++ 2 files changed, 1066 insertions(+), 575 deletions(-) create mode 100644 examples/ex2.ipynb diff --git a/examples/ex1.ipynb b/examples/ex1.ipynb index d708d84f..66ebb923 100644 --- a/examples/ex1.ipynb +++ b/examples/ex1.ipynb @@ -9,12 +9,10 @@ ] }, { - "cell_type": "code", - "execution_count": 1, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [ - "import ams" + "This example gives a \"hello world\" example to use AMS." ] }, { @@ -35,31 +33,33 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ - "import ams" + "import ams\n", + "\n", + "import datetime" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "'0.6.5.post25.dev0+ga5fc571'" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "Last run time: 2023-11-07 22:30:50\n", + "ams:0.7.3.post29.dev0+g23b6565\n" + ] } ], "source": [ - "ams.__version__" + "print(\"Last run time:\", datetime.datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\"))\n", + "\n", + "print(f'ams:{ams.__version__}')" ] }, { @@ -74,7 +74,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -114,26 +114,28 @@ "source": [ "AMS support multiple input file formats, including AMS ``.xlsx`` file, MATPOWER ``.m`` file, PYPOWER ``.py`` file, and PSS/E ``.raw`` file.\n", "\n", - "Here we use the AMS ``.xlsx`` file as an example. The source file locates at ``$HOME/ams/ams/cases/ieee14/ieee14_opf.xlsx``." + "Here we use the AMS ``.xlsx`` file as an example. The source file locates at ``$HOME/ams/ams/cases/``." ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "Parsing input file \"/Users/jinningwang/Documents/work/ams/ams/cases/ieee14/ieee14_opf.xlsx\"...\n", - "Input file parsed in 0.4313 seconds.\n", - "System set up in 0.0021 seconds.\n" + "Parsing input file \"/Users/jinningwang/Documents/work/ams/ams/cases/ieee39/ieee39_uced.xlsx\"...\n", + "Input file parsed in 0.1640 seconds.\n", + "System set up in 0.0041 seconds.\n" ] } ], "source": [ - "sp = ams.load(ams.get_case('ieee14/ieee14_opf.xlsx'), default_config=True)" + "sp = ams.load(ams.get_case('ieee39/ieee39_uced.xlsx'),\n", + " default_config=True,\n", + " setup=True)" ] }, { @@ -154,28 +156,37 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "OrderedDict([('Summary', Summary (1 device) at 0x1590a8fd0),\n", - " ('Bus', Bus (14 devices) at 0x15996c040),\n", - " ('PQ', PQ (11 devices) at 0x15996cac0),\n", - " ('PV', PV (4 devices) at 0x15996ce80),\n", - " ('Slack', Slack (1 device) at 0x1599799d0),\n", - " ('Shunt', Shunt (2 devices) at 0x159984400),\n", - " ('Line', Line (20 devices) at 0x1599848b0),\n", - " ('ESD1', ESD1 (0 devices) at 0x15998afa0),\n", - " ('Area', Area (2 devices) at 0x15999d700),\n", - " ('Region', Region (0 devices) at 0x15999dfa0),\n", - " ('SFR', SFR (0 devices) at 0x159a54790),\n", - " ('GCost', GCost (5 devices) at 0x159a54e80),\n", - " ('SFRCost', SFRCost (0 devices) at 0x159a61520)])" + "OrderedDict([('Summary', Summary (3 devices) at 0x1069a7670),\n", + " ('Bus', Bus (39 devices) at 0x161e03850),\n", + " ('PQ', PQ (19 devices) at 0x161e16250),\n", + " ('PV', PV (9 devices) at 0x161e16790),\n", + " ('Slack', Slack (1 device) at 0x161e25220),\n", + " ('Shunt', Shunt (0 devices) at 0x161e25ca0),\n", + " ('Line', Line (46 devices) at 0x161e2e190),\n", + " ('ESD1', ESD1 (0 devices) at 0x161e3c880),\n", + " ('REGCV1', REGCV1 (0 devices) at 0x161e3cfa0),\n", + " ('Area', Area (2 devices) at 0x161e4f730),\n", + " ('Region', Region (2 devices) at 0x161e4feb0),\n", + " ('SFR', SFR (2 devices) at 0x161e5a6a0),\n", + " ('SR', SR (2 devices) at 0x161e5ad00),\n", + " ('NSR', NSR (2 devices) at 0x161e64160),\n", + " ('GCost', GCost (10 devices) at 0x161e64580),\n", + " ('SFRCost', SFRCost (10 devices) at 0x161e64c10),\n", + " ('SRCost', SRCost (10 devices) at 0x161e731f0),\n", + " ('NSRCost', NSRCost (10 devices) at 0x161e73610),\n", + " ('REGCV1Cost', REGCV1Cost (0 devices) at 0x161e73a30),\n", + " ('TimeSlot', TimeSlot (0 devices) at 0x161e73d30),\n", + " ('EDTSlot', EDTSlot (24 devices) at 0x161e7c9a0),\n", + " ('UCTSlot', UCTSlot (24 devices) at 0x161e7cd90)])" ] }, - "execution_count": 6, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -194,7 +205,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 6, "metadata": {}, "outputs": [ { @@ -221,15 +232,12 @@ " idx\n", " u\n", " name\n", + " bus\n", " Vn\n", + " p0\n", + " q0\n", " vmax\n", " vmin\n", - " v0\n", - " a0\n", - " xcoord\n", - " ycoord\n", - " area\n", - " zone\n", " owner\n", " \n", " \n", @@ -244,283 +252,291 @@ " \n", " \n", " \n", - " \n", - " \n", - " \n", " \n", " \n", " \n", " \n", " 0\n", - " 1\n", + " PQ_1\n", " 1.0\n", - " BUS1\n", - " 69.0\n", - " 1.1\n", - " 0.9\n", - " 1.03000\n", - " 0.000000\n", - " 0\n", - " 0\n", - " 1\n", - " 1\n", + " PQ_1\n", + " 3\n", + " 345.0\n", + " 6.000\n", + " 2.500\n", + " 1.2\n", + " 0.8\n", " 1\n", " \n", " \n", " 1\n", - " 2\n", + " PQ_2\n", " 1.0\n", - " BUS2\n", - " 69.0\n", - " 1.1\n", - " 0.9\n", - " 1.01970\n", - " -0.027981\n", - " 0\n", - " 0\n", - " 1\n", - " 1\n", + " PQ_2\n", + " 4\n", + " 345.0\n", + " 4.500\n", + " 1.840\n", + " 1.2\n", + " 0.8\n", " 1\n", " \n", " \n", " 2\n", - " 3\n", + " PQ_3\n", " 1.0\n", - " BUS3\n", - " 69.0\n", - " 1.1\n", - " 0.9\n", - " 1.00042\n", - " -0.060097\n", - " 0\n", - " 0\n", - " 1\n", - " 1\n", + " PQ_3\n", + " 7\n", + " 345.0\n", + " 2.338\n", + " 0.840\n", + " 1.2\n", + " 0.8\n", " 1\n", " \n", " \n", " 3\n", - " 4\n", + " PQ_4\n", " 1.0\n", - " BUS4\n", - " 69.0\n", - " 1.1\n", - " 0.9\n", - " 0.99858\n", - " -0.074721\n", - " 0\n", - " 0\n", - " 1\n", - " 1\n", + " PQ_4\n", + " 8\n", + " 345.0\n", + " 5.220\n", + " 1.766\n", + " 1.2\n", + " 0.8\n", " 1\n", " \n", " \n", " 4\n", - " 5\n", + " PQ_5\n", " 1.0\n", - " BUS5\n", - " 69.0\n", - " 1.1\n", - " 0.9\n", - " 1.00443\n", - " -0.064315\n", - " 0\n", - " 0\n", - " 1\n", - " 1\n", + " PQ_5\n", + " 12\n", + " 138.0\n", + " 1.200\n", + " 0.300\n", + " 1.2\n", + " 0.8\n", " 1\n", " \n", " \n", " 5\n", - " 6\n", + " PQ_6\n", " 1.0\n", - " BUS6\n", - " 138.0\n", - " 1.1\n", - " 0.9\n", - " 0.99871\n", - " -0.109998\n", - " 0\n", - " 0\n", - " 2\n", - " 2\n", - " 2\n", + " PQ_6\n", + " 15\n", + " 345.0\n", + " 3.200\n", + " 1.530\n", + " 1.2\n", + " 0.8\n", + " 1\n", " \n", " \n", " 6\n", - " 7\n", + " PQ_7\n", " 1.0\n", - " BUS7\n", - " 138.0\n", - " 1.1\n", - " 0.9\n", - " 1.00682\n", - " -0.084285\n", - " 0\n", - " 0\n", - " 2\n", - " 2\n", - " 2\n", + " PQ_7\n", + " 16\n", + " 345.0\n", + " 3.290\n", + " 0.323\n", + " 1.2\n", + " 0.8\n", + " 1\n", " \n", " \n", " 7\n", - " 8\n", + " PQ_8\n", " 1.0\n", - " BUS8\n", - " 69.0\n", - " 1.1\n", - " 0.9\n", - " 1.01895\n", - " -0.024339\n", - " 0\n", - " 0\n", - " 2\n", - " 2\n", - " 2\n", + " PQ_8\n", + " 18\n", + " 345.0\n", + " 1.580\n", + " 0.300\n", + " 1.2\n", + " 0.8\n", + " 1\n", " \n", " \n", " 8\n", - " 9\n", + " PQ_9\n", " 1.0\n", - " BUS9\n", + " PQ_9\n", + " 20\n", " 138.0\n", - " 1.1\n", - " 0.9\n", - " 1.00193\n", - " -0.127502\n", - " 0\n", - " 0\n", - " 2\n", - " 2\n", - " 2\n", + " 6.800\n", + " 1.030\n", + " 1.2\n", + " 0.8\n", + " 1\n", " \n", " \n", " 9\n", - " 10\n", + " PQ_10\n", " 1.0\n", - " BUS10\n", - " 138.0\n", - " 1.1\n", - " 0.9\n", - " 0.99351\n", - " -0.130202\n", - " 0\n", - " 0\n", - " 2\n", - " 2\n", - " 2\n", + " PQ_10\n", + " 21\n", + " 345.0\n", + " 2.740\n", + " 1.150\n", + " 1.2\n", + " 0.8\n", + " 1\n", " \n", " \n", " 10\n", - " 11\n", + " PQ_11\n", " 1.0\n", - " BUS11\n", - " 138.0\n", - " 1.1\n", - " 0.9\n", - " 0.99245\n", - " -0.122948\n", - " 0\n", - " 0\n", - " 2\n", - " 2\n", - " 2\n", + " PQ_11\n", + " 23\n", + " 345.0\n", + " 2.475\n", + " 0.846\n", + " 1.2\n", + " 0.8\n", + " 1\n", " \n", " \n", " 11\n", - " 12\n", + " PQ_12\n", " 1.0\n", - " BUS12\n", - " 138.0\n", - " 1.1\n", - " 0.9\n", - " 0.98639\n", - " -0.128934\n", - " 0\n", - " 0\n", - " 2\n", - " 2\n", - " 2\n", + " PQ_12\n", + " 24\n", + " 345.0\n", + " 3.086\n", + " -0.922\n", + " 1.2\n", + " 0.8\n", + " 1\n", " \n", " \n", " 12\n", - " 13\n", + " PQ_13\n", " 1.0\n", - " BUS13\n", - " 138.0\n", - " 1.1\n", - " 0.9\n", - " 0.98403\n", - " -0.133786\n", - " 0\n", - " 0\n", - " 2\n", - " 2\n", - " 2\n", + " PQ_13\n", + " 25\n", + " 345.0\n", + " 2.240\n", + " 0.472\n", + " 1.2\n", + " 0.8\n", + " 1\n", " \n", " \n", " 13\n", - " 14\n", + " PQ_14\n", " 1.0\n", - " BUS14\n", - " 138.0\n", - " 1.1\n", - " 0.9\n", - " 0.99063\n", - " -0.166916\n", - " 0\n", - " 0\n", - " 2\n", - " 2\n", - " 2\n", + " PQ_14\n", + " 26\n", + " 345.0\n", + " 1.390\n", + " 0.170\n", + " 1.2\n", + " 0.8\n", + " 1\n", + " \n", + " \n", + " 14\n", + " PQ_15\n", + " 1.0\n", + " PQ_15\n", + " 27\n", + " 345.0\n", + " 2.810\n", + " 0.755\n", + " 1.2\n", + " 0.8\n", + " 1\n", + " \n", + " \n", + " 15\n", + " PQ_16\n", + " 1.0\n", + " PQ_16\n", + " 28\n", + " 345.0\n", + " 2.060\n", + " 0.276\n", + " 1.2\n", + " 0.8\n", + " 1\n", + " \n", + " \n", + " 16\n", + " PQ_17\n", + " 1.0\n", + " PQ_17\n", + " 29\n", + " 345.0\n", + " 2.835\n", + " 1.269\n", + " 1.2\n", + " 0.8\n", + " 1\n", + " \n", + " \n", + " 17\n", + " PQ_18\n", + " 1.0\n", + " PQ_18\n", + " 31\n", + " 34.5\n", + " 0.800\n", + " 0.400\n", + " 1.2\n", + " 0.8\n", + " 1\n", + " \n", + " \n", + " 18\n", + " PQ_19\n", + " 1.0\n", + " PQ_19\n", + " 39\n", + " 345.0\n", + " 4.000\n", + " 2.500\n", + " 1.2\n", + " 0.8\n", + " 1\n", " \n", " \n", "\n", "" ], "text/plain": [ - " idx u name Vn vmax vmin v0 a0 xcoord ycoord \\\n", - "uid \n", - "0 1 1.0 BUS1 69.0 1.1 0.9 1.03000 0.000000 0 0 \n", - "1 2 1.0 BUS2 69.0 1.1 0.9 1.01970 -0.027981 0 0 \n", - "2 3 1.0 BUS3 69.0 1.1 0.9 1.00042 -0.060097 0 0 \n", - "3 4 1.0 BUS4 69.0 1.1 0.9 0.99858 -0.074721 0 0 \n", - "4 5 1.0 BUS5 69.0 1.1 0.9 1.00443 -0.064315 0 0 \n", - "5 6 1.0 BUS6 138.0 1.1 0.9 0.99871 -0.109998 0 0 \n", - "6 7 1.0 BUS7 138.0 1.1 0.9 1.00682 -0.084285 0 0 \n", - "7 8 1.0 BUS8 69.0 1.1 0.9 1.01895 -0.024339 0 0 \n", - "8 9 1.0 BUS9 138.0 1.1 0.9 1.00193 -0.127502 0 0 \n", - "9 10 1.0 BUS10 138.0 1.1 0.9 0.99351 -0.130202 0 0 \n", - "10 11 1.0 BUS11 138.0 1.1 0.9 0.99245 -0.122948 0 0 \n", - "11 12 1.0 BUS12 138.0 1.1 0.9 0.98639 -0.128934 0 0 \n", - "12 13 1.0 BUS13 138.0 1.1 0.9 0.98403 -0.133786 0 0 \n", - "13 14 1.0 BUS14 138.0 1.1 0.9 0.99063 -0.166916 0 0 \n", - "\n", - " area zone owner \n", - "uid \n", - "0 1 1 1 \n", - "1 1 1 1 \n", - "2 1 1 1 \n", - "3 1 1 1 \n", - "4 1 1 1 \n", - "5 2 2 2 \n", - "6 2 2 2 \n", - "7 2 2 2 \n", - "8 2 2 2 \n", - "9 2 2 2 \n", - "10 2 2 2 \n", - "11 2 2 2 \n", - "12 2 2 2 \n", - "13 2 2 2 " + " idx u name bus Vn p0 q0 vmax vmin owner\n", + "uid \n", + "0 PQ_1 1.0 PQ_1 3 345.0 6.000 2.500 1.2 0.8 1\n", + "1 PQ_2 1.0 PQ_2 4 345.0 4.500 1.840 1.2 0.8 1\n", + "2 PQ_3 1.0 PQ_3 7 345.0 2.338 0.840 1.2 0.8 1\n", + "3 PQ_4 1.0 PQ_4 8 345.0 5.220 1.766 1.2 0.8 1\n", + "4 PQ_5 1.0 PQ_5 12 138.0 1.200 0.300 1.2 0.8 1\n", + "5 PQ_6 1.0 PQ_6 15 345.0 3.200 1.530 1.2 0.8 1\n", + "6 PQ_7 1.0 PQ_7 16 345.0 3.290 0.323 1.2 0.8 1\n", + "7 PQ_8 1.0 PQ_8 18 345.0 1.580 0.300 1.2 0.8 1\n", + "8 PQ_9 1.0 PQ_9 20 138.0 6.800 1.030 1.2 0.8 1\n", + "9 PQ_10 1.0 PQ_10 21 345.0 2.740 1.150 1.2 0.8 1\n", + "10 PQ_11 1.0 PQ_11 23 345.0 2.475 0.846 1.2 0.8 1\n", + "11 PQ_12 1.0 PQ_12 24 345.0 3.086 -0.922 1.2 0.8 1\n", + "12 PQ_13 1.0 PQ_13 25 345.0 2.240 0.472 1.2 0.8 1\n", + "13 PQ_14 1.0 PQ_14 26 345.0 1.390 0.170 1.2 0.8 1\n", + "14 PQ_15 1.0 PQ_15 27 345.0 2.810 0.755 1.2 0.8 1\n", + "15 PQ_16 1.0 PQ_16 28 345.0 2.060 0.276 1.2 0.8 1\n", + "16 PQ_17 1.0 PQ_17 29 345.0 2.835 1.269 1.2 0.8 1\n", + "17 PQ_18 1.0 PQ_18 31 34.5 0.800 0.400 1.2 0.8 1\n", + "18 PQ_19 1.0 PQ_19 39 345.0 4.000 2.500 1.2 0.8 1" ] }, - "execution_count": 7, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "sp.Bus.as_df()" + "sp.PQ.as_df()" ] }, { @@ -528,27 +544,33 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Similarly, in AMS, all supported routines are registered to an OrderedDict ``routines``." + "In AMS, all supported routines are registered to an OrderedDict ``routines``." ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "OrderedDict([('DCPF', DCPF at 0x1590a8fa0),\n", - " ('PFlow', PFlow at 0x159a6c670),\n", - " ('ACOPF', ACOPF at 0x159b5f220),\n", - " ('DCOPF', DCOPF at 0x159b9b1c0),\n", - " ('ED', ED at 0x159b9b670),\n", - " ('RTED', RTED at 0x159b9be50),\n", - " ('UC', UC at 0x159bb1b50)])" + "OrderedDict([('DCPF', DCPF at 0x161e035e0),\n", + " ('PFlow', PFlow at 0x161e89a30),\n", + " ('CPF', CPF at 0x161e9a040),\n", + " ('ACOPF', ACOPF at 0x161e9a640),\n", + " ('DCOPF', DCOPF at 0x161eb0340),\n", + " ('ED', ED at 0x161eb08e0),\n", + " ('ED2', ED2 at 0x161ebfbb0),\n", + " ('RTED', RTED at 0x161ecec70),\n", + " ('RTED2', RTED2 at 0x162008670),\n", + " ('UC', UC at 0x165495a30),\n", + " ('UC2', UC2 at 0x1658735e0),\n", + " ('LDOPF', LDOPF at 0x16587ec40),\n", + " ('LDOPF2', LDOPF2 at 0x16588b730)])" ] }, - "execution_count": 8, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -562,522 +584,290 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Solve Power Flow" + "### Solve an Routine" ] }, { - "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "In AMS, the AC power flow and DC power flow are solved by PYPOWER ``runpf()`` and ``rundcpf()`` functions, respectively." + "Before solving an routine, we need to initialize it first.\n", + "Here Real-time Economic Dispatch (RTED) is used as an example." ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "Setup model for DCPF\n", - "DCPF has no objective function.\n", - "DCPF model set up in 0.0011 seconds.\n", - "PYPOWER Version 5.1.4, 27-June-2018\n", - " -- DC Power Flow\n", - "\n", - "DCPF completed in 0.0033 seconds with exit code 0.\n" + "Routine initialized in 0.0060 seconds.\n" ] }, { "data": { "text/plain": [ - "0" + "True" ] }, - "execution_count": 9, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "sp.DCPF.run()" + "sp.RTED.init()" ] }, { - "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "Inspect the generator power outputs and bus angles." + "Then, one can solve it by calling ``run()``.\n", + "Here, argument `solver` can be passed to specify the solver to use, such as `solver='GUROBI'`.\n", + "\n", + "Installed solvers can be listed by ``ams.shared.INSTALLED_SOLVERS``,\n", + "and more detailes of solver can be found at [CVXPY-Choosing a solver](https://www.cvxpy.org/tutorial/advanced/index.html#choosing-a-solver)." ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 9, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "array([0.4 , 0.4 , 0.3 , 0.35 , 0.787])" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "sp.DCPF.pg.v" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ + "name": "stderr", + "output_type": "stream", + "text": [ + "RTED solved as optimal in 0.0118 seconds, converged after 50 iterations using solver OSQP.\n" + ] + }, { "data": { "text/plain": [ - "array([ 0. , -0.02887178, -0.0641022 , -0.07743542, -0.06670094,\n", - " -0.11334928, -0.08549152, -0.02403816, -0.12824676, -0.13308983,\n", - " -0.12681192, -0.1339144 , -0.13779051, -0.16285214])" + "True" ] }, - "execution_count": 11, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "sp.DCPF.aBus.v" + "sp.RTED.run()" ] }, { - "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "Solve the AC power flow." + "The solved results are stored in each variable itself.\n", + "For example, the solved power generation of ten generators\n", + "are stored in ``pg.v``." ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 10, "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Setup model for PFlow\n", - "PFlow has no objective function.\n", - "PFlow model set up in 0.0012 seconds.\n", - "PYPOWER Version 5.1.4, 27-June-2018\n", - "\n", - "Newton's method power flow converged in 3 iterations.\n", - "\n", - "PFlow completed in 0.0091 seconds with exit code 0.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - " -- AC Power Flow (Newton)\n", - "\n" - ] - }, { "data": { "text/plain": [ - "0" + "array([6.01822025, 5.99427957, 5.97043409, 5.08 , 5.98234496,\n", + " 5.8 , 5.64 , 6.00623798, 6.03022646, 6.04225669])" ] }, - "execution_count": 12, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "sp.PFlow.run()" + "sp.RTED.pg.v" ] }, { - "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "Inspect the generator power outputs, bus angles, and bus voltages." + "Here, ``get_idx()`` can be used to get the index of a variable." ] }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "array([0.4 , 0.4 , 0.3 , 0.35 , 0.81427213])" + "['PV_30',\n", + " 'PV_31',\n", + " 'PV_32',\n", + " 'PV_33',\n", + " 'PV_34',\n", + " 'PV_35',\n", + " 'PV_36',\n", + " 'PV_37',\n", + " 'PV_38',\n", + " 'Slack_39']" ] }, - "execution_count": 13, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "sp.PFlow.pg.v" + "sp.RTED.pg.get_idx()" ] }, { - "cell_type": "code", - "execution_count": 14, + "cell_type": "markdown", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "array([ 0. , -0.03078883, -0.06173455, -0.07696511, -0.06707345,\n", - " -0.11262148, -0.08526268, -0.02687732, -0.12646404, -0.12942483,\n", - " -0.12356406, -0.13042896, -0.13475262, -0.16547667])" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ - "sp.PFlow.aBus.v" + "Part of the solved results can be accessed with given indices." ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "array([1.03 , 1.03 , 1.01 , 1.01140345, 1.01725551,\n", - " 1.03 , 1.0224715 , 1.03 , 1.02176879, 1.01554207,\n", - " 1.01911515, 1.01740699, 1.01445023, 1.0163402 ])" + "array([6.01822025, 5.99427957])" ] }, - "execution_count": 15, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "sp.PFlow.vBus.v" + "sp.RTED.get(src='pg', attr='v', idx=['PV_30', 'PV_31'])" ] }, { - "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "### Solve ACOPF" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In AMS, the ACOPF is solved by PYPOWER ``runopf()`` function." + "All variables are listed in an OrderedDict ``vars``." ] }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 13, "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Setup model for ACOPF\n", - "ACOPF model set up in 0.0015 seconds.\n", - "PYPOWER Version 5.1.4, 27-June-2018\n", - " -- AC Optimal Power Flow\n", - "\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Python Interior Point Solver - PIPS, Version 1.0, 07-Feb-2011\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "ACOPF completed in 0.2583 seconds with exit code 0.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Converged!\n" - ] - }, { "data": { "text/plain": [ - "0" + "OrderedDict([('pg', Var: StaticGen.pg),\n", + " ('pn', Var: Bus.pn),\n", + " ('plf', Var: Line.plf),\n", + " ('pru', Var: StaticGen.pru),\n", + " ('prd', Var: StaticGen.prd)])" ] }, - "execution_count": 16, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "sp.ACOPF.run()" + "sp.RTED.vars" ] }, { - "cell_type": "code", - "execution_count": 17, + "cell_type": "markdown", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "array([0.49996655, 0.10000065, 0.10000064, 0.10000065, 1.49809555])" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ - "sp.ACOPF.pg.v" + "The objective value can be accessed with ``obj.v``." ] }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "array([0.14990061, 0.14999993, 0.09997174, 0.09993129, 0.08059299])" + "592.8203580808987" ] }, - "execution_count": 18, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "sp.ACOPF.qg.v" + "sp.RTED.obj.v" ] }, { - "cell_type": "code", - "execution_count": 19, + "cell_type": "markdown", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "array([ 0. , -0.04662694, -0.11452421, -0.11585264, -0.09988879,\n", - " -0.18205907, -0.16148797, -0.14627229, -0.19500474, -0.19788944,\n", - " -0.19253337, -0.19878038, -0.20322846, -0.23228802])" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ - "sp.ACOPF.aBus.v" + "Similarly, the constraints are listed in an OrderedDict ``constrs``,\n", + "and the expression values can also be accessed." ] }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "array([1.09999682, 1.08133094, 1.05126009, 1.05401909, 1.06111871,\n", - " 1.05714675, 1.06795632, 1.08056893, 1.06468318, 1.05600708,\n", - " 1.05321342, 1.04626446, 1.04476777, 1.055634 ])" + "OrderedDict([('pb', [ON]: sum(pl) - sum(pg) =0),\n", + " ('pinj', [ON]: CftT@plf - pl - pn =0),\n", + " ('lub', [ON]: PTDF @ (pn - pl) - rate_a <=0),\n", + " ('llb', [ON]: - PTDF @ (pn - pl) - rate_a <=0),\n", + " ('rbu', [ON]: gs @ multiply(ug, pru) - dud =0),\n", + " ('rbd', [ON]: gs @ multiply(ug, prd) - ddd =0),\n", + " ('rru', [ON]: multiply(ug, pg + pru) - pmax <=0),\n", + " ('rrd', [ON]: multiply(ug, -pg + prd) - pmin <=0),\n", + " ('rgu', [ON]: multiply(ug, pg-pg0-R10) <=0),\n", + " ('rgd', [ON]: multiply(ug, -pg+pg0-R10) <=0)])" ] }, - "execution_count": 20, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "sp.ACOPF.vBus.v" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Solve DCOPF" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In AMS, DCOPF and other routines are modeled and solved with CVXPY." + "sp.RTED.constrs" ] }, { "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Setup model of DCOPF\n", - "DCOPF model set up in 0.0017 seconds.\n", - "DCOPF solved as optimal in 0.0055 seconds with exit code 0.\n" - ] - } - ], - "source": [ - "sp.DCOPF.run()" - ] - }, - { - "cell_type": "code", - "execution_count": 22, + "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "array([0.5 , 0.5 , 0.3685, 0.3685, 0.5 ])" + "array([-99.48177975, -99.50572043, -99.52956591, -98.92 ,\n", + " -99.61765504, -98.8 , -98.76 , -99.49376202,\n", + " -99.46977354, -99.73645331])" ] }, - "execution_count": 22, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "sp.DCOPF.pg.v" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "One can specify the solver by passing the solver name to the ``solver`` argument of ``run()``." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The default CVXPV solver ``ECOS`` might be slow or incapable of solving large-scale problems." - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Parsing input file \"/Users/jinningwang/Documents/work/ams/ams/cases/matpower/case_ACTIVSg2000.m\"...\n", - "Input file parsed in 0.4876 seconds.\n", - "The bus index is not continuous, adjusted automatically.\n", - "System set up in 0.8002 seconds.\n" - ] - } - ], - "source": [ - "sp = ams.load(ams.get_case('matpower/case_ACTIVSg2000.m'), default_config=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Setup model of DCOPF\n", - "DCOPF model set up in 0.0094 seconds.\n", - "DCOPF solved as optimal in 29.8392 seconds with exit code 0.\n" - ] - } - ], - "source": [ - "sp.DCOPF.run(solver='ECOS')" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "One can specify the solver by passing the solver name to the ``solver`` argument of ``run()``." - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "DCOPF model set up in 0.0129 seconds.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Set parameter Username\n", - "Academic license - for non-commercial use only - expires 2024-05-21\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "DCOPF solved as optimal in 0.7355 seconds with exit code 0.\n" - ] - } - ], - "source": [ - "sp.DCOPF.run(solver='GUROBI')" + "sp.RTED.rgu.v" ] } ], diff --git a/examples/ex2.ipynb b/examples/ex2.ipynb new file mode 100644 index 00000000..b1b40b99 --- /dev/null +++ b/examples/ex2.ipynb @@ -0,0 +1,701 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Manipulate the Model Data" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This example shows how to manipulate the model." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import ams\n", + "\n", + "import datetime" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Last run time: 2023-11-07 22:31:12\n", + "ams:0.7.3.post29.dev0+g23b6565\n" + ] + } + ], + "source": [ + "print(\"Last run time:\", datetime.datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\"))\n", + "\n", + "print(f'ams:{ams.__version__}')" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "ams.config_logger(stream_level=20)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Run Simulations" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Load Case" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Parsing input file \"/Users/jinningwang/Documents/work/ams/ams/cases/ieee39/ieee39_uced.xlsx\"...\n", + "Input file parsed in 0.1714 seconds.\n", + "System set up in 0.0043 seconds.\n" + ] + } + ], + "source": [ + "sp = ams.load(ams.get_case('ieee39/ieee39_uced.xlsx'),\n", + " default_config=True,\n", + " setup=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The system load are defined in model `PQ`." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idxunamebusVnp0q0vmaxvminowner
uid
0PQ_11.0PQ_13345.06.0002.5001.20.81
1PQ_21.0PQ_24345.04.5001.8401.20.81
2PQ_31.0PQ_37345.02.3380.8401.20.81
3PQ_41.0PQ_48345.05.2201.7661.20.81
4PQ_51.0PQ_512138.01.2000.3001.20.81
5PQ_61.0PQ_615345.03.2001.5301.20.81
6PQ_71.0PQ_716345.03.2900.3231.20.81
7PQ_81.0PQ_818345.01.5800.3001.20.81
8PQ_91.0PQ_920138.06.8001.0301.20.81
9PQ_101.0PQ_1021345.02.7401.1501.20.81
10PQ_111.0PQ_1123345.02.4750.8461.20.81
11PQ_121.0PQ_1224345.03.086-0.9221.20.81
12PQ_131.0PQ_1325345.02.2400.4721.20.81
13PQ_141.0PQ_1426345.01.3900.1701.20.81
14PQ_151.0PQ_1527345.02.8100.7551.20.81
15PQ_161.0PQ_1628345.02.0600.2761.20.81
16PQ_171.0PQ_1729345.02.8351.2691.20.81
17PQ_181.0PQ_183134.50.8000.4001.20.81
18PQ_191.0PQ_1939345.04.0002.5001.20.81
\n", + "
" + ], + "text/plain": [ + " idx u name bus Vn p0 q0 vmax vmin owner\n", + "uid \n", + "0 PQ_1 1.0 PQ_1 3 345.0 6.000 2.500 1.2 0.8 1\n", + "1 PQ_2 1.0 PQ_2 4 345.0 4.500 1.840 1.2 0.8 1\n", + "2 PQ_3 1.0 PQ_3 7 345.0 2.338 0.840 1.2 0.8 1\n", + "3 PQ_4 1.0 PQ_4 8 345.0 5.220 1.766 1.2 0.8 1\n", + "4 PQ_5 1.0 PQ_5 12 138.0 1.200 0.300 1.2 0.8 1\n", + "5 PQ_6 1.0 PQ_6 15 345.0 3.200 1.530 1.2 0.8 1\n", + "6 PQ_7 1.0 PQ_7 16 345.0 3.290 0.323 1.2 0.8 1\n", + "7 PQ_8 1.0 PQ_8 18 345.0 1.580 0.300 1.2 0.8 1\n", + "8 PQ_9 1.0 PQ_9 20 138.0 6.800 1.030 1.2 0.8 1\n", + "9 PQ_10 1.0 PQ_10 21 345.0 2.740 1.150 1.2 0.8 1\n", + "10 PQ_11 1.0 PQ_11 23 345.0 2.475 0.846 1.2 0.8 1\n", + "11 PQ_12 1.0 PQ_12 24 345.0 3.086 -0.922 1.2 0.8 1\n", + "12 PQ_13 1.0 PQ_13 25 345.0 2.240 0.472 1.2 0.8 1\n", + "13 PQ_14 1.0 PQ_14 26 345.0 1.390 0.170 1.2 0.8 1\n", + "14 PQ_15 1.0 PQ_15 27 345.0 2.810 0.755 1.2 0.8 1\n", + "15 PQ_16 1.0 PQ_16 28 345.0 2.060 0.276 1.2 0.8 1\n", + "16 PQ_17 1.0 PQ_17 29 345.0 2.835 1.269 1.2 0.8 1\n", + "17 PQ_18 1.0 PQ_18 31 34.5 0.800 0.400 1.2 0.8 1\n", + "18 PQ_19 1.0 PQ_19 39 345.0 4.000 2.500 1.2 0.8 1" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sp.PQ.as_df()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For simplicity, PQ is reorganized as nodal load `pl` in an routine." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([0. , 0. , 6. , 4.5 , 0. , 0. , 2.338, 5.22 , 0. ,\n", + " 0. , 0. , 1.2 , 0. , 0. , 3.2 , 3.29 , 0. , 1.58 ,\n", + " 0. , 6.8 , 2.74 , 0. , 2.475, 3.086, 2.24 , 1.39 , 2.81 ,\n", + " 2.06 , 2.835, 0. , 0.8 , 0. , 0. , 0. , 0. , 0. ,\n", + " 0. , 0. , 4. ])" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sp.RTED.pl.v" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "RTED can be solved and one can inspect the results as discussed in\n", + "previous example." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Routine initialized in 0.0083 seconds.\n", + "RTED solved as optimal in 0.0138 seconds, converged after 50 iterations using solver OSQP.\n" + ] + }, + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sp.RTED.run()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([6.01822025, 5.99427957, 5.97043409, 5.08 , 5.98234496,\n", + " 5.8 , 5.64 , 6.00623798, 6.03022646, 6.04225669])" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sp.RTED.pg.v" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The load values can be manipulated in the model `PQ`." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sp.PQ.set(src='p0', attr='v', idx=['PQ_1', 'PQ_3'], value=[6.5, 3])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The routine need to be re-initialized to make the changes effective." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Routine initialized in 0.0064 seconds.\n" + ] + }, + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sp.RTED.init()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can see the `pl` is changed as expected." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([0. , 0. , 6.5 , 4.5 , 0. , 0. , 3. , 5.22 , 0. ,\n", + " 0. , 0. , 1.2 , 0. , 0. , 3.2 , 3.29 , 0. , 1.58 ,\n", + " 0. , 6.8 , 2.74 , 0. , 2.475, 3.086, 2.24 , 1.39 , 2.81 ,\n", + " 2.06 , 2.835, 0. , 0.8 , 0. , 0. , 0. , 0. , 0. ,\n", + " 0. , 0. , 4. ])" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sp.RTED.pl.v" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After manipulation, the routined can be solved again." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "RTED solved as optimal in 0.0140 seconds, converged after 50 iterations using solver OSQP.\n" + ] + }, + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sp.RTED.run()" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([6.18438538, 6.16011369, 6.13593852, 5.07999972, 6.14801408,\n", + " 5.79999973, 5.63999972, 6.17223744, 6.19655759, 6.20875415])" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sp.RTED.pg.v" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "ams", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.16" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "d2b3bf80176349caa68dc4a3c77bd06eaade8abc678330f7d1c813c53380e5d2" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 364723a517d47b949c9a32e409bca39c985737d0 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Tue, 7 Nov 2023 22:35:58 -0500 Subject: [PATCH 32/77] Add examples into doc index --- docs/source/examples/index.rst | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/source/examples/index.rst b/docs/source/examples/index.rst index 0ebbce0a..36303b9d 100644 --- a/docs/source/examples/index.rst +++ b/docs/source/examples/index.rst @@ -5,4 +5,16 @@ Examples .. _`development demos`: https://github.com/jinningwang/ams/tree/master/dev/demo -Refer to the development `development demos`_ for examples prior to preparing this section. \ No newline at end of file +Refer to the development `development demos`_ for examples prior to preparing this section. + +A collection of examples are presented to supplement the tutorial. The +examples below are identical to the Jupyter Notebook in the ``examples`` +folder of the repository +`here `__. + +.. toctree:: + :maxdepth: 2 + :caption: Scripting + + ../_examples/ex1.ipynb + ../_examples/ex2.ipynb From 6e28676d98d5ade482204dc42a672293acf3d472 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 8 Nov 2023 12:14:58 -0500 Subject: [PATCH 33/77] Minor fix on RTED __repr__ --- ams/core/param.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ams/core/param.py b/ams/core/param.py index a4e5c3ca..aa989c4d 100644 --- a/ams/core/param.py +++ b/ams/core/param.py @@ -158,7 +158,8 @@ def class_name(self): return self.__class__.__name__ def __repr__(self): - return f'{self.__class__.__name__}: {self.owner.__class__.__name__}.{self.name}' + postfix = '' if self.src is None else f'.{self.src}' + return f'{self.__class__.__name__}: {self.owner.__class__.__name__}' + postfix def get_idx(self): """ From bb07dfde4d10cd1f41430a3d2b7a14595fca598e Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 8 Nov 2023 13:27:11 -0500 Subject: [PATCH 34/77] Fix solver stats empty info --- ams/routines/routine.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ams/routines/routine.py b/ams/routines/routine.py index fffb2f84..29fb7c68 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -353,7 +353,11 @@ def run(self, force_init=False, no_code=True, **kwargs): _, s = elapsed(t0) self.exec_time = float(s.split(" ")[0]) sstats = self.om.mdl.solver_stats # solver stats - n_iter = int(sstats.num_iters) + print(sstats.num_iters) + if sstats.num_iters is None: + n_iter = -1 + else: + n_iter = int(sstats.num_iters) n_iter_str = f"{n_iter} iterations " if n_iter > 1 else f"{n_iter} iteration " if self.exit_code == 0: msg = f"{self.class_name} solved as {status} in {s}, converged after " From 804b08de8f76b7bda9add72097003267b5e60c40 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 8 Nov 2023 13:46:32 -0500 Subject: [PATCH 35/77] Add documenter support to gamma symbol --- ams/core/documenter.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/ams/core/documenter.py b/ams/core/documenter.py index 20f46716..261067ee 100644 --- a/ams/core/documenter.py +++ b/ams/core/documenter.py @@ -498,23 +498,26 @@ def _tex_pre(docm, p, tex_map): """ # NOTE: in the future, there might occur special math symbols - special_map = OrderedDict([ + map_before = { + 'sum': 'SUM', + r'\sum': 'SUM', + r'\eta': 'ETA', + r'\gamma': 'GAMMA', + r'\frac': 'FRAC', + } + map_post = OrderedDict([ ('SUM', r'\sum'), ('ETA', r'\eta'), + ('GAMMA', r'\gamma'), ('FRAC', r'\frac'), ]) expr = p.e_str for pattern, replacement in tex_map.items(): - if r'\sum' in replacement: - replacement = replacement.replace(r'\sum', 'SUM') - if r'sum' in expr: - expr = expr.replace('sum', 'SUM') - if '\eta' in replacement: - replacement = replacement.replace('\eta', 'ETA') - if r'\frac' in replacement: - replacement = replacement.replace(r'\frac', 'FRAC') + for key, val in map_before.items(): + if key in replacement: + replacement = replacement.replace(key, val) if r'\p' in replacement: continue try: @@ -529,7 +532,7 @@ def _tex_pre(docm, p, tex_map): except re.error: logger.error('Remains '*' in the expression.') - for pattern, replacement in special_map.items(): + for pattern, replacement in map_post.items(): expr = expr.replace(pattern, replacement) return expr From 4b85b491b86e39247aeb1eb3c98c9548827080ed Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 9 Nov 2023 09:19:47 -0500 Subject: [PATCH 36/77] Fix VarSelect --- ams/core/service.py | 49 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/ams/core/service.py b/ams/core/service.py index efc76fae..d9ac5d8a 100644 --- a/ams/core/service.py +++ b/ams/core/service.py @@ -623,17 +623,45 @@ class VarSelect(NumOp): A numerical matrix to select a subset of a 2D variable, ``u.v[:, idx]``. + For example, if nned to select Energy Storage output + power from StaticGen `pg`, following definition can be used: + ```python + class RTED: + ... + self.ce = VarSelect(u=self.pg, indexer='genE') + ... + ``` + Parameters ---------- u : Callable The input matrix variable. - idx : list - The index of the subset. + indexer: str + The name of the indexer source. + gamma : str, optional + The name of the indexer gamma. + name : str, optional + The name of the instance. + tex_name : str, optional + The TeX name for the instance. + unit : str, optional + The unit of the output. + info : str, optional + A description of the operation. + vtype : Type, optional + The variable type. + rfun : Callable, optional + Function to apply to the output of ``fun``. + rargs : dict, optional + Keyword arguments to pass to ``rfun``. + array_out : bool, optional + Whether to force the output to be an array. """ def __init__(self, u: Callable, indexer: str, + gamma: str = None, name: str = None, tex_name: str = None, unit: str = None, @@ -641,12 +669,15 @@ def __init__(self, vtype: Type = None, rfun: Callable = None, rargs: dict = {}, + array_out: bool = True, **kwargs ): super().__init__(name=name, tex_name=tex_name, unit=unit, info=info, vtype=vtype, u=u, fun=None, - rfun=rfun, rargs=rargs, **kwargs) + rfun=rfun, rargs=rargs, array_out=array_out, + **kwargs) self.indexer = indexer + self.gamma = gamma @property def v0(self): @@ -684,12 +715,12 @@ def v0(self): if not is_subset: raise ValueError(f'{indexer.model} contains undefined {indexer.src}, likey a data error.') - out = [1 if item in ref else 0 for item in uidx] - out = np.array(out) - if self.u.horizon is not None: - out = out[:, np.newaxis] - out = np.repeat(out, self.u.horizon.n, axis=1) - return np.array(out) + row, col = np.meshgrid(uidx, ref) + out = (row == col).astype(int) + if self.gamma: + vgamma = getattr(self.rtn, self.gamma) + out = vgamma.v[:, np.newaxis] * out + return out class VarReduction(NumOp): From 43057100cae6ee9c78cd938edf91f9ed6ca90a55 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 9 Nov 2023 09:19:57 -0500 Subject: [PATCH 37/77] Typo --- ams/models/distributed/esd1.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ams/models/distributed/esd1.py b/ams/models/distributed/esd1.py index 293f3e78..baa69d9c 100644 --- a/ams/models/distributed/esd1.py +++ b/ams/models/distributed/esd1.py @@ -32,12 +32,12 @@ def __init__(self): ) self.gammap = NumParam(default=1.0, tex_name=r'\gamma_p', - info='Ratio of PVD1.pref0 w.r.t to that of static PV', + info='Ratio of ESD1.pref0 w.r.t to that of static PV', vrange='(0, 1]', ) self.gammaq = NumParam(default=1.0, tex_name=r'\gamma_q', - info='Ratio of PVD1.qref0 w.r.t to that of static PV', + info='Ratio of ESD1.qref0 w.r.t to that of static PV', vrange='(0, 1]', ) From e949c7dcfadea29f7a10cf9870fb73567e4bc3d6 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 9 Nov 2023 09:20:41 -0500 Subject: [PATCH 38/77] Fix property method shape --- ams/opt/omodel.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index 28cf47fe..c0e125ad 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -85,10 +85,10 @@ def shape(self): """ Return the shape. """ - if self.rtn.initialized: + try: return self.om.__dict__[self.name].shape - else: - logger.warning(f'<{self.rtn.class_name}> is not initialized yet.') + except KeyError: + logger.warning('Shape info is not ready before initialziation.') return None @property From e39bbe54d7d23244c9295980e7cf4bd78e474b2b Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 9 Nov 2023 09:21:13 -0500 Subject: [PATCH 39/77] Fix routine tests --- tests/test_routine.py | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/tests/test_routine.py b/tests/test_routine.py index 5886d756..a834f775 100644 --- a/tests/test_routine.py +++ b/tests/test_routine.py @@ -1,8 +1,32 @@ import unittest +from functools import wraps import numpy as np import ams -from ams.shared import require_igraph + +try: + from ams.shared import igraph + getattr(igraph, '__version__') + HAVE_IGRAPH = True +except (ImportError, AttributeError): + HAVE_IGRAPH = False + + +def require_igraph(f): + """ + Decorator for functions that require igraph. + """ + + @wraps(f) + def wrapper(*args, **kwds): + try: + getattr(igraph, '__version__') + except AttributeError: + raise ModuleNotFoundError("igraph needs to be manually installed.") + + return f(*args, **kwds) + + return wrapper class TestRoutineMethods(unittest.TestCase): @@ -32,7 +56,7 @@ def test_routine_get(self): np.testing.assert_equal(self.ss.DCOPF.get('ug', 'PV_30'), 1) # get an unpacked var value - self.ss.DCOPF.run(solver='OSQP') + self.ss.DCOPF.run() 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')) @@ -66,12 +90,12 @@ def test_Init(self): f"{rtn.class_name} initialization failed!") +@unittest.skipUnless(HAVE_IGRAPH, "igaph not available") class TestRoutineGraph(unittest.TestCase): """ Test routine graph. """ - @require_igraph def test_5bus_graph(self): """ Test routine graph of PJM 5-bus system. @@ -83,7 +107,6 @@ def test_5bus_graph(self): _, g = ss.DCOPF.igraph() self.assertGreaterEqual(np.min(g.degree()), 1) - @require_igraph def test_ieee14_graph(self): """ Test routine graph of IEEE 14-bus system. @@ -95,7 +118,6 @@ def test_ieee14_graph(self): _, g = ss.DCOPF.igraph() self.assertGreaterEqual(np.min(g.degree()), 1) - @require_igraph def test_ieee39_graph(self): """ Test routine graph of IEEE 39-bus system. @@ -107,7 +129,6 @@ def test_ieee39_graph(self): _, g = ss.DCOPF.igraph() self.assertGreaterEqual(np.min(g.degree()), 1) - @require_igraph def test_npcc_graph(self): """ Test routine graph of NPCC 140-bus system. @@ -119,7 +140,6 @@ def test_npcc_graph(self): _, g = ss.DCOPF.igraph() self.assertGreaterEqual(np.min(g.degree()), 1) - @require_igraph def test_wecc_graph(self): """ Test routine graph of WECC 179-bus system. From 7bb714123157471d39fa189837a025268ce284ce Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 9 Nov 2023 09:21:47 -0500 Subject: [PATCH 40/77] Update RTD conf --- docs/source/conf.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 770c56d5..d6a17b89 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -9,9 +9,8 @@ # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration -# TODO: fix the importing error later on import ams -# import shutil +import shutil extensions = [ 'sphinx.ext.autodoc', @@ -184,11 +183,12 @@ exec(open("genmodelref.py").read()) exec(open("genroutineref.py").read()) -jupyter_execute_notebooks = "off" - # import and execute model reference generation script -# TODO: use this for routines doumentation later on -# exec(open("genroutineref.py").read()) +shutil.rmtree("_examples", ignore_errors=True) +shutil.copytree("../../examples", "_examples", ) +# shutil.rmtree("_examples/demonstration") + +jupyter_execute_notebooks = "off" # sphinx-panels shouldn't add bootstrap css since the pydata-sphinx-theme # already loads it From dc44d950942ba9160d055f54ecc0014b4b342095 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 9 Nov 2023 09:22:46 -0500 Subject: [PATCH 41/77] Ignore .conda in git --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ecc52bc9..3efdd590 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,7 @@ _generated icebar # example excel files cache -~$*.xlsx \ No newline at end of file +~$*.xlsx + +# conda +.conda/* \ No newline at end of file From d7a5b9506d714cede9a7523295ef51473001c874 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 9 Nov 2023 09:23:14 -0500 Subject: [PATCH 42/77] [WIP] Fix RTED2 --- ams/routines/rted.py | 45 ++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/ams/routines/rted.py b/ams/routines/rted.py index 9ad82601..53de7468 100644 --- a/ams/routines/rted.py +++ b/ams/routines/rted.py @@ -52,6 +52,9 @@ def __init__(self): name='R10', tex_name=r'R_{10}', model='StaticGen', src='R10', unit='p.u./h',) + self.gammape = RParam(info='Ratio of ESD1.pge w.r.t to that of static generator', + name='gammape', tex_name=r'\gamma_{p,e}', + model='ESD1', src='gammap',) class RTEDModel(DCOPFModel): @@ -260,8 +263,8 @@ def __init__(self): name='EtaD', src='EtaD', tex_name=r'\eta_d', unit='%', model='ESD1',) - self.genE = RParam(info='gen of ESD1', - name='genE', tex_name=r'g_{ES}', + self.gene = RParam(info='gen of ESD1', + name='gene', tex_name=r'g_{E}', model='ESD1', src='gen',) # --- service --- @@ -278,31 +281,27 @@ def __init__(self): # --- vars --- self.SOC = Var(info='ESD1 SOC', unit='%', name='SOC', tex_name=r'SOC', - model='ESD1', pos=True,) - self.ce = VarSelect(u=self.pg, indexer='genE', - name='ce', tex_name=r'C_{ES}', - info='Select pge from pg',) + model='ESD1', pos=True, + v0=self.SOCinit, + lb=self.SOCmin, ub=self.SOCmax,) + self.ce = VarSelect(u=self.pg, indexer='gene', + name='ce', tex_name=r'C_{E}', + info='Select zue from pg', + gamma='gammape',) self.pge = Var(info='ESD1 output power (system base)', - unit='p.u.', name='pge', tex_name=r'p_{g,ES}', + unit='p.u.', name='pge', tex_name=r'p_{g,E}', model='ESD1',) - self.ued = Var(info='ESD1 commitment decision', - name='ued', tex_name=r'u_{ES,d}', + self.ued = Var(info='ESD1 charging decision', + name='ued', tex_name=r'u_{E,d}', model='ESD1', boolean=True,) - self.zue = Var(info='Aux var, :math:`z_{ue} = u_{e,d} * p_{g,ES}`', + self.zue = Var(info='Aux var, :math:`z_{ue} = u_{e,d} * p_{g,E}`', 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) - zue',) - - self.SOClb = Constraint(name='SOClb', type='uq', - info='ESD1 SOC lower bound', - e_str='-SOC + SOCmin',) - self.SOCub = Constraint(name='SOCub', type='uq', - info='ESD1 SOC upper bound', - e_str='SOC - SOCmax',) + info='Select zue from pg', + e_str='ce @ pg + zue',) self.zclb = Constraint(name='zclb', type='uq', info='zue lower bound', e_str='- zue + pge',) @@ -311,10 +310,12 @@ def __init__(self): self.zcub2 = Constraint(name='zcub2', type='uq', info='zue upper bound', e_str='zue - Mb dot ued',) - SOCb = 'SOC - SOCinit - t dot REn * EtaC * zue' - SOCb += '- t dot REn * REtaD * (pge - zue)' + # NOTE: SOC balance is wrong! + SOCb = 'En dot (SOC - SOCinit) - t dot EtaC * zue' + SOCb += '+ t dot REtaD * (pge - zue)' self.SOCb = Constraint(name='SOCb', type='eq', - info='ESD1 SOC balance', e_str=SOCb,) + info='ESD1 SOC balance', + e_str=SOCb,) class RTED2(RTEDData, RTEDModel, ESD1Base): From e971f78ccb2e666385af74ae8038342bd5f90258 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 13 Nov 2023 18:44:32 -0500 Subject: [PATCH 43/77] Add alias for symprocessor --- ams/core/symprocessor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ams/core/symprocessor.py b/ams/core/symprocessor.py index eb5ea42e..b704c93a 100644 --- a/ams/core/symprocessor.py +++ b/ams/core/symprocessor.py @@ -67,6 +67,7 @@ def __init__(self, parent): (r'\bvar\b', f'{lang}.Variable'), (r'\bproblem\b', f'{lang}.Problem'), (r'\bmultiply\b', f'{lang}.multiply'), + (r'\bmul\b', f'{lang}.multiply'), # alias for multiply (r'\bvstack\b', f'{lang}.vstack'), (r'\bnorm\b', f'{lang}.norm'), (r'\bpos\b', f'{lang}.pos'), From 7e5200cb238e0b4ed07f5c802eaf2c7392516a4b Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 13 Nov 2023 18:46:04 -0500 Subject: [PATCH 44/77] Add registration of var boundary to omodel --- ams/opt/omodel.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index c0e125ad..b58ff7fb 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -181,7 +181,7 @@ def __init__(self, unit: Optional[str] = None, model: Optional[str] = None, shape: Optional[Union[tuple, int]] = None, - lb: Optional[str] = None, + lb: Optional[RParam] = None, ub: Optional[str] = None, ctrl: Optional[str] = None, v0: Optional[str] = None, @@ -321,6 +321,7 @@ def parse(self): # fit variable shape if horizon exists elv = np.tile(elv, (nc, 1)).T if nc > 0 else elv exec("om.constrs[self.lb.name] = tmp >= elv") + exec("setattr(om, self.lb.name, om.constrs[self.lb.name])") if self.ub: uv = self.ub.owner.get(src=self.ub.name, idx=self.get_idx(), attr='v') u = self.lb.owner.get(src='u', idx=self.get_idx(), attr='v') @@ -329,6 +330,7 @@ def parse(self): # fit variable shape if horizon exists euv = np.tile(euv, (nc, 1)).T if nc > 0 else euv exec("om.constrs[self.ub.name] = tmp <= euv") + exec("setattr(om, self.ub.name, om.constrs[self.ub.name])") return True def __repr__(self): From 8ddb95b1414f757905febfae3f27356914d5bde5 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 13 Nov 2023 18:47:10 -0500 Subject: [PATCH 45/77] [WIP] Fix ED --- ams/routines/ed.py | 50 +- ams/routines/routine.py | 1 - ams/routines/rted.py | 76 +-- ams/routines/uc.py | 8 +- dev/demo/8.ESD1.ipynb | 248 ---------- examples/demonstration/1. ESD1.ipynb | 667 +++++++++++++++++++++++++++ examples/ex1.ipynb | 2 +- 7 files changed, 757 insertions(+), 295 deletions(-) delete mode 100644 dev/demo/8.ESD1.ipynb create mode 100644 examples/demonstration/1. ESD1.ipynb diff --git a/ams/routines/ed.py b/ams/routines/ed.py index 0d052bfa..df5ce12f 100644 --- a/ams/routines/ed.py +++ b/ams/routines/ed.py @@ -1,18 +1,17 @@ """ Real-time economic dispatch. """ -import logging # NOQA -from collections import OrderedDict # NOQA -import numpy as np # NOQA +import logging +from collections import OrderedDict +import numpy as np -from ams.core.param import RParam # NOQA +from ams.core.param import RParam from ams.core.service import (ZonalSum, NumOpDual, NumHstack, - RampSub, NumOp, LoadScale) # NOQA + RampSub, NumOp, LoadScale) -from ams.routines.rted import RTEDData, ESD1Base # NOQA -from ams.routines.dcopf import DCOPFModel # NOQA +from ams.routines.rted import RTEDData, ESD1Base +from ams.routines.dcopf import DCOPFModel -from ams.core.service import VarSelect # NOQA from ams.opt.omodel import Var, Constraint # NOQA logger = logging.getLogger(__name__) @@ -215,7 +214,36 @@ def __init__(self, system, config): self.info = 'Economic dispatch with energy storage' self.type = 'DCED' + # NOTE: extend vars to 2D self.SOC.horizon = self.timeslot - self.pge.horizon = self.timeslot - self.ued.horizon = self.timeslot - self.zue.horizon = self.timeslot + self.pce.horizon = self.timeslot + self.pde.horizon = self.timeslot + self.uce.horizon = self.timeslot + self.ude.horizon = self.timeslot + self.zce.horizon = self.timeslot + self.zde.horizon = self.timeslot + + self.Mre = RampSub(u=self.SOC, name='Mre', tex_name=r'M_{r,E}', + info='Subtraction matrix for SOC',) + self.EnR = NumHstack(u=self.En, ref=self.Mre, + name='EnR', tex_name=r'E_{n,R}', + info='Repeated En as 2D matrix, (ng, ng-1)',) + self.EtaCR = NumHstack(u=self.EtaC, ref=self.Mre, + name='EtaCR', tex_name=r'\eta_{c,R}', + info='Repeated Etac as 2D matrix, (ng, ng-1)',) + self.REtaDR = NumHstack(u=self.REtaD, ref=self.Mre, + name='REtaDR', tex_name=r'R_{\eta_d,R}', + info='Repeated REtaD as 2D matrix, (ng, ng-1)',) + SOCb = 'mul(EnR, SOC @ Mre) - t dot mul(EtaCR, zce[:, 1:])' + SOCb += ' + t dot mul(REtaDR, zde[:, 1:])' + self.SOCb.e_str = SOCb + + SOCb0 = 'mul(En, SOC[:, 0] - SOCinit) - t dot mul(EtaC, zce[:, 0])' + SOCb0 += ' + t dot mul(REtaD, zde[:, 0])' + self.SOCb0 = Constraint(name='SOCb', type='eq', + info='ESD1 SOC initial balance', + e_str=SOCb0,) + + self.SOCr = Constraint(name='SOCr', type='eq', + info='SOC requirement', + e_str='SOC[:, -1] - SOCinit',) diff --git a/ams/routines/routine.py b/ams/routines/routine.py index 29fb7c68..0064d464 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -353,7 +353,6 @@ def run(self, force_init=False, no_code=True, **kwargs): _, s = elapsed(t0) self.exec_time = float(s.split(" ")[0]) sstats = self.om.mdl.solver_stats # solver stats - print(sstats.num_iters) if sstats.num_iters is None: n_iter = -1 else: diff --git a/ams/routines/rted.py b/ams/routines/rted.py index 53de7468..6b18fac4 100644 --- a/ams/routines/rted.py +++ b/ams/routines/rted.py @@ -122,22 +122,22 @@ def __init__(self, system, config): info='zonal RegDn reserve requirement',) self.rbu = Constraint(name='rbu', type='eq', info='RegUp reserve balance', - e_str='gs @ multiply(ug, pru) - dud',) + e_str='gs @ mul(ug, pru) - dud',) self.rbd = Constraint(name='rbd', type='eq', info='RegDn reserve balance', - e_str='gs @ multiply(ug, prd) - ddd',) + e_str='gs @ mul(ug, prd) - ddd',) self.rru = Constraint(name='rru', type='uq', info='RegUp reserve ramp', - e_str='multiply(ug, pg + pru) - pmax',) + e_str='mul(ug, pg + pru) - pmax',) self.rrd = Constraint(name='rrd', type='uq', info='RegDn reserve ramp', - e_str='multiply(ug, -pg + prd) - pmin',) + e_str='mul(ug, -pg + prd) - pmin',) self.rgu = Constraint(name='rgu', type='uq', info='ramp up limit of generator output', - e_str='multiply(ug, pg-pg0-R10)',) + e_str='mul(ug, pg-pg0-R10)',) self.rgd = Constraint(name='rgd', type='uq', info='ramp down limit of generator output', - e_str='multiply(ug, -pg+pg0-R10)',) + e_str='mul(ug, -pg+pg0-R10)',) # --- objective --- self.obj.info = 'total generation and reserve cost' # NOTE: the product of dt and pg is processed using ``dot``, because dt is a numnber @@ -270,8 +270,6 @@ def __init__(self): # --- service --- self.REtaD = NumOp(name='REtaD', tex_name=r'\frac{1}{\eta_d}', u=self.EtaD, fun=np.reciprocal,) - self.REn = NumOp(name='REn', tex_name=r'\frac{1}{E_n}', - u=self.En, fun=np.reciprocal,) self.Mb = NumOp(info='10 times of max of pmax as big M', name='Mb', tex_name=r'M_{big}', u=self.pmax, fun=np.max, @@ -288,31 +286,49 @@ def __init__(self): name='ce', tex_name=r'C_{E}', info='Select zue from pg', gamma='gammape',) - self.pge = Var(info='ESD1 output power (system base)', - unit='p.u.', name='pge', tex_name=r'p_{g,E}', - model='ESD1',) - self.ued = Var(info='ESD1 charging decision', - name='ued', tex_name=r'u_{E,d}', + self.pce = Var(info='ESD1 charging power (system base)', + unit='p.u.', name='pce', tex_name=r'p_{c,E}', + model='ESD1', nonneg=True,) + self.pde = Var(info='ESD1 discharging power (system base)', + unit='p.u.', name='pde', tex_name=r'p_{d,E}', + model='ESD1', nonneg=True,) + self.uce = Var(info='ESD1 charging decision', + name='uce', tex_name=r'u_{c,E}', model='ESD1', boolean=True,) - self.zue = Var(info='Aux var, :math:`z_{ue} = u_{e,d} * p_{g,E}`', - name='zue', tex_name=r'z_{ue}', - model='ESD1', pos=True,) + self.ude = Var(info='ESD1 discharging decision', + name='ude', tex_name=r'u_{d,E}', + model='ESD1', boolean=True,) + self.zce = Var(info='Aux var for charging, :math:`z_{c,e}=u_{c,E}p_{c,E}`', + name='zce', tex_name=r'z_{c,E}', + model='ESD1', nonneg=True,) + self.zde = Var(info='Aux var for discharging, :math:`z_{d,e}=u_{d,E}*p_{d,E}`', + name='zde', tex_name=r'z_{d,E}', + model='ESD1', nonneg=True,) # --- constraints --- - self.cpge = Constraint(name='cpge', type='eq', - info='Select zue from pg', - e_str='ce @ pg + zue',) - - 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',) - - # NOTE: SOC balance is wrong! - SOCb = 'En dot (SOC - SOCinit) - t dot EtaC * zue' - SOCb += '+ t dot REtaD * (pge - zue)' + self.ceb = Constraint(name='ceb', type='eq', + info='Charging decision bound', + e_str='uce + ude - 1',) + self.cpe = Constraint(name='cpe', type='eq', + info='Select pce from pg', + e_str='ce @ pg - zce - zde',) + + self.zce1 = Constraint(name='zce1', type='uq', info='zce bound 1', + e_str='-zce + pce',) + self.zce2 = Constraint(name='zce2', type='uq', info='zce bound 2', + e_str='zce - pce - Mb dot (1-uce)',) + self.zce3 = Constraint(name='zce3', type='uq', info='zce bound 3', + e_str='zce - Mb dot uce',) + + self.zde1 = Constraint(name='zde1', type='uq', info='zde bound 1', + e_str='-zde + pde',) + self.zde2 = Constraint(name='zde2', type='uq', info='zde bound 2', + e_str='zde - pde - Mb dot (1-ude)',) + self.zde3 = Constraint(name='zde3', type='uq', info='zde bound 3', + e_str='zde - Mb dot ude',) + + SOCb = 'mul(En, (SOC - SOCinit)) - t dot mul(EtaC, zce)' + SOCb += '+ t dot mul(REtaD, zde)' 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 ce92a824..b2fa1cea 100644 --- a/ams/routines/uc.py +++ b/ams/routines/uc.py @@ -270,7 +270,7 @@ def __init__(self, system, config): self.info = 'unit commitment with energy storage' self.type = 'DCUC' - self.SOC.horizon = self.timeslot - self.pge.horizon = self.timeslot - self.ued.horizon = self.timeslot - self.zue.horizon = self.timeslot + # self.SOC.horizon = self.timeslot + # self.pge.horizon = self.timeslot + # self.ude.horizon = self.timeslot + # self.zue.horizon = self.timeslot diff --git a/dev/demo/8.ESD1.ipynb b/dev/demo/8.ESD1.ipynb deleted file mode 100644 index 65be00d3..00000000 --- a/dev/demo/8.ESD1.ipynb +++ /dev/null @@ -1,248 +0,0 @@ -{ - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# ESD" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "\n", - "import andes\n", - "import ams\n", - "\n", - "import pandas as pd\n", - "\n", - "import json\n", - "\n", - "import cvxpy as cp\n", - "\n", - "import re" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'0.6.7.post44.dev0+gc6b8426'" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "ams.__version__" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "ams.config_logger(stream_level=10)" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Input format guessed as xlsx.\n", - "Parsing input file \"/Users/jinningwang/Documents/work/ams/ams/cases/ieee39/ieee39_esd1.xlsx\"...\n", - "Input file parsed in 0.1659 seconds.\n", - "Adjust bus index to start from 0.\n", - "System set up in 0.0040 seconds.\n" - ] - } - ], - "source": [ - "sp = ams.load(ams.get_case('ieee39/ieee39_esd1.xlsx'),\n", - " setup=True,\n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Setup model of RTED\n", - "RTED data check passed.\n", - "- Generating symbols for RTED\n", - "Set constrs pb: sum(pd) - sum(pg) == 0\n", - "Set constrs pinj: Cg@(pn - pd) - pg == 0\n", - "Set constrs lub: PTDF @ (pn - pd) - rate_a <= 0\n", - "Set constrs llb: - PTDF @ (pn - pd) - rate_a <= 0\n", - "Set constrs rbu: gs @ pru - du == 0\n", - "Set constrs rbd: gs @ prd - dd == 0\n", - "Set constrs rru: pg + pru - pmax <= 0\n", - "Set constrs rrd: -pg + prd - pmin <= 0\n", - "Set constrs rgu: pg - pg0 - R10h <= 0\n", - "Set constrs rgd: -pg + pg0 - R10h <= 0\n", - "RTED model set up in 0.0168 seconds.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Set parameter Username\n", - "Academic license - for non-commercial use only - expires 2024-05-21\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "RTED solved as optimal in 0.0218 seconds with exit code 0.\n" - ] - }, - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "sp.RTED.run(solver=cp.GUROBI, reoptimize=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Setup model of RTED2\n", - "RTED2 data check passed.\n", - "- Generating symbols for RTED2\n", - "Set constrs pb: sum(pd) - sum(pg) == 0\n", - "Set constrs pinj: Cg@(pn - pd) - pg == 0\n", - "Set constrs lub: PTDF @ (pn - pd) - rate_a <= 0\n", - "Set constrs llb: - PTDF @ (pn - pd) - rate_a <= 0\n", - "Set constrs rbu: gs @ pru - du == 0\n", - "Set constrs rbd: gs @ prd - dd == 0\n", - "Set constrs rru: pg + pru - pmax <= 0\n", - "Set constrs rrd: -pg + prd - pmin <= 0\n", - "Set constrs rgu: pg - pg0 - R10h <= 0\n", - "Set constrs rgd: -pg + pg0 - R10h <= 0\n", - "Set constrs pges: e1s@pg - zc == 0\n", - "Set constrs SOClb: -SOC + SOCmin <= 0\n", - "Set constrs SOCub: SOC - SOCmax <= 0\n", - "Set constrs zclb: - zc + pec <= 0\n", - "Set constrs zcub: zc - pec - Mb@(1-uc) <= 0\n", - "Set constrs zcub2: zc - Mb@uc <= 0\n", - "Set constrs SOCb: SOC - SOCinit - power(dth, 1)*REn*EtaC*zc - power(dth, 1)*REn*REtaD*(pec - zc) == 0\n", - "RTED2 model set up in 0.0144 seconds.\n", - "RTED2 solved as optimal in 0.0198 seconds with exit code 0.\n" - ] - }, - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "sp.RTED2.run(solver=cp.GUROBI, reoptimize=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Routine: RTED\n", - "Objective: 629.8546048020892\n", - "------------------\n", - "Routine: RTED2\n", - "Objective: 629.8546048240468\n", - "ESD1 SOC level: [0.20548643]\n", - "Charging: uc=[1.]; pec=[6.58371675], zc=[6.58371675]\n" - ] - } - ], - "source": [ - "rtn = sp.RTED\n", - "\n", - "print(\"Routine: RTED\")\n", - "print(f\"Objective: {rtn.obj.v}\")\n", - "print(\"------------------\")\n", - "\n", - "rtn = sp.RTED2\n", - "\n", - "print(\"Routine: RTED2\")\n", - "print(f\"Objective: {rtn.obj.v}\")\n", - "print(f\"ESD1 SOC level: {rtn.SOC.v}\")\n", - "print(f\"Charging: uc={rtn.uc.v}; pec={rtn.pec.v}, zc={rtn.zc.v}\")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "ams", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.16" - }, - "orig_nbformat": 4, - "vscode": { - "interpreter": { - "hash": "d2b3bf80176349caa68dc4a3c77bd06eaade8abc678330f7d1c813c53380e5d2" - } - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/demonstration/1. ESD1.ipynb b/examples/demonstration/1. ESD1.ipynb new file mode 100644 index 00000000..7441f09e --- /dev/null +++ b/examples/demonstration/1. ESD1.ipynb @@ -0,0 +1,667 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Dispatch with Energy Storage" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import ams\n", + "\n", + "import datetime\n", + "\n", + "import numpy as np\n", + "\n", + "import cvxpy as cp" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Last run time: 2023-11-10 00:12:54\n", + "ams:0.7.3.post42.dev0+gd7a5b95\n" + ] + } + ], + "source": [ + "print(\"Last run time:\", datetime.datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\"))\n", + "\n", + "print(f'ams:{ams.__version__}')" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "ams.config_logger(stream_level=10)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Input format guessed as xlsx.\n", + "Parsing input file \"/Users/jinningwang/Documents/work/ams/ams/cases/ieee39/ieee39_uced_esd1_t2.xlsx\"...\n", + "Input file parsed in 0.1132 seconds.\n", + "System set up in 0.0038 seconds.\n" + ] + } + ], + "source": [ + "sp = ams.load(ams.get_case('ieee39/ieee39_uced_esd1_t2.xlsx'),\n", + " setup=True,)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "RTED2 data check passed.\n", + "- Generating symbols for RTED2\n", + "Set constrs pb: sum(pl) - sum(pg) == 0\n", + "Set constrs pinj: CftT@plf - pl - pn == 0\n", + "Set constrs lub: PTDF @ (pn - pl) - rate_a <= 0\n", + "Set constrs llb: - PTDF @ (pn - pl) - rate_a <= 0\n", + "Set constrs rbu: gs @ mul(ug, pru) - dud == 0\n", + "Set constrs rbd: gs @ mul(ug, prd) - ddd == 0\n", + "Set constrs rru: mul(ug, pg + pru) - pmax <= 0\n", + "Set constrs rrd: mul(ug, -pg + prd) - pmin <= 0\n", + "Set constrs rgu: mul(ug, pg-pg0-R10) <= 0\n", + "Set constrs rgd: mul(ug, -pg+pg0-R10) <= 0\n", + "Set constrs ceb: uce + ude - 1 == 0\n", + "Set constrs cpe: ce @ pg - zce - zde == 0\n", + "Set constrs zce1: -zce + pce <= 0\n", + "Set constrs zce2: zce - pce - Mb dot (1-uce) <= 0\n", + "Set constrs zce3: zce - Mb dot uce <= 0\n", + "Set constrs zde1: -zde + pde <= 0\n", + "Set constrs zde2: zde - pde - Mb dot (1-ude) <= 0\n", + "Set constrs zde3: zde - Mb dot ude <= 0\n", + "Set constrs SOCb: mul(En, (SOC - SOCinit)) - t dot mul(EtaC, zce)+ t dot mul(REtaD, zde) == 0\n", + "Set obj tc: min. sum(c2 @ (t dot pg)**2) + sum(c1 @ (t dot pg)) + ug * c0 + sum(cru * pru + crd * prd)\n", + "Routine initialized in 0.0559 seconds.\n" + ] + }, + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sp.RTED2.init()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "RTED2 has already been initialized.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Set parameter Username\n", + "Academic license - for non-commercial use only - expires 2024-05-21\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "RTED2 solved as optimal in 0.0793 seconds, converged after 33 iterations using solver GUROBI.\n" + ] + }, + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sp.RTED2.run(solver='GUROBI')" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "pg=[6.01822025 5.99427957 5.97043409 5.08 5.98234496 5.8\n", + " 5.64 6.00623798 6.03022646 6.04225669]\n", + "uce=[0. 0.]\n", + "pce=[0. 0.]\n", + "zce=[0. 0.]\n", + "ude=[1. 1.]\n", + "pde=[3.00911012 3.00911012]\n", + "zde=[3.00911012 3.00911012]\n", + "SOC=[0.49749241 0.49749241]\n", + "obj=592.8203580808986\n" + ] + } + ], + "source": [ + "print(f\"pg={sp.RTED2.pg.v}\")\n", + "\n", + "print(f\"uce={sp.RTED2.uce.v}\")\n", + "print(f\"pce={sp.RTED2.pce.v}\")\n", + "print(f\"zce={sp.RTED2.zce.v}\")\n", + "\n", + "print(f\"ude={sp.RTED2.ude.v}\")\n", + "print(f\"pde={sp.RTED2.pde.v}\")\n", + "print(f\"zde={sp.RTED2.zde.v}\")\n", + "\n", + "print(f\"SOC={sp.RTED2.SOC.v}\")\n", + "print(f\"obj={sp.RTED2.obj.v}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "RTED2 data check passed.\n", + "Set constrs pb: sum(pl) - sum(pg) == 0\n", + "Set constrs pinj: CftT@plf - pl - pn == 0\n", + "Set constrs lub: PTDF @ (pn - pl) - rate_a <= 0\n", + "Set constrs llb: - PTDF @ (pn - pl) - rate_a <= 0\n", + "Set constrs rbu: gs @ mul(ug, pru) - dud == 0\n", + "Set constrs rbd: gs @ mul(ug, prd) - ddd == 0\n", + "Set constrs rru: mul(ug, pg + pru) - pmax <= 0\n", + "Set constrs rrd: mul(ug, -pg + prd) - pmin <= 0\n", + "Set constrs rgu: mul(ug, pg-pg0-R10) <= 0\n", + "Set constrs rgd: mul(ug, -pg+pg0-R10) <= 0\n", + "Set constrs ceb: uce + ude - 1 == 0\n", + "Set constrs cpe: ce @ pg - zce - zde == 0\n", + "Set constrs zce1: -zce + pce <= 0\n", + "Set constrs zce2: zce - pce - Mb dot (1-uce) <= 0\n", + "Set constrs zce3: zce - Mb dot uce <= 0\n", + "Set constrs zde1: -zde + pde <= 0\n", + "Set constrs zde2: zde - pde - Mb dot (1-ude) <= 0\n", + "Set constrs zde3: zde - Mb dot ude <= 0\n", + "Set constrs SOCb: mul(En, (SOC - SOCinit)) - t dot mul(EtaC, zce)+ t dot mul(REtaD, zde) == 0\n", + "Set obj tc: min. sum(c2 @ (t dot pg)**2) + sum(c1 @ (t dot pg)) + ug * c0 + sum(cru * pru + crd * prd)\n", + "Routine initialized in 0.0663 seconds.\n", + "RTED2 solved as optimal in 0.0754 seconds, converged after 33 iterations using solver GUROBI.\n" + ] + }, + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sp.RTED2.set(src='c1', attr='v', idx='GCost_1', value=999)\n", + "sp.RTED2.run(force_init=True, solver='GUROBI')" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "pg=[0. 7.13724518 6.52 5.08 6.87 5.8\n", + " 5.64 7.15034313 7.17661754 7.18979416]\n", + "uce=[0. 0.]\n", + "pce=[0. 0.]\n", + "zce=[0. 0.]\n", + "ude=[1. 1.]\n", + "pde=[0. 0.]\n", + "zde=[0. 0.]\n", + "SOC=[0.5 0.5]\n", + "obj=622.4342380817811\n" + ] + } + ], + "source": [ + "print(f\"pg={sp.RTED2.pg.v}\")\n", + "\n", + "print(f\"uce={sp.RTED2.uce.v}\")\n", + "print(f\"pce={sp.RTED2.pce.v}\")\n", + "print(f\"zce={sp.RTED2.zce.v}\")\n", + "\n", + "print(f\"ude={sp.RTED2.ude.v}\")\n", + "print(f\"pde={sp.RTED2.pde.v}\")\n", + "print(f\"zde={sp.RTED2.zde.v}\")\n", + "\n", + "print(f\"SOC={sp.RTED2.SOC.v}\")\n", + "print(f\"obj={sp.RTED2.obj.v}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Input format guessed as xlsx.\n", + "Parsing input file \"/Users/jinningwang/Documents/work/ams/ams/cases/ieee39/ieee39_uced_esd1_t2.xlsx\"...\n", + "Input file parsed in 0.0892 seconds.\n", + "System set up in 0.0100 seconds.\n" + ] + } + ], + "source": [ + "sp = ams.load(ams.get_case('ieee39/ieee39_uced_esd1_t2.xlsx'),\n", + " setup=True,)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "NumOp: ED2.REtaD, v in shape of (2,)" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sp.ED2.REtaD" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "ED2 data check passed.\n", + "- Generating symbols for ED2\n", + "Set constrs pb: - gs @ pg + pds == 0\n", + "Code Constr: om.constrs[\"pb\"]=- self.om.rtn.gs.v @ self.om.pg + self.om.rtn.pds.v == 0\n", + "Set constrs pinj: Cg @ (pn - Rpd) - pg == 0\n", + "Code Constr: om.constrs[\"pinj\"]=self.om.rtn.Cg.v @ (self.om.pn - self.om.rtn.Rpd.v) - self.om.pg == 0\n", + "Set constrs lub: PTDF @ (pn - Rpd) - RRA <= 0\n", + "Code Constr: om.constrs[\"lub\"]=self.om.rtn.PTDF.v @ (self.om.pn - self.om.rtn.Rpd.v) - self.om.rtn.RRA.v <= 0\n", + "Set constrs llb: -PTDF @ (pn - Rpd) - RRA <= 0\n", + "Code Constr: om.constrs[\"llb\"]=-self.om.rtn.PTDF.v @ (self.om.pn - self.om.rtn.Rpd.v) - self.om.rtn.RRA.v <= 0\n", + "Set constrs sr: -gs@multiply(Rpmax - pg, Rug) + dsr <= 0\n", + "Code Constr: om.constrs[\"sr\"]=-self.om.rtn.gs.v@cp.multiply(self.om.rtn.Rpmax.v - self.om.pg, self.om.rtn.Rug.v) + self.om.rtn.dsr.v <= 0\n", + "Set constrs rgu: pg @ Mr - t dot RR30 <= 0\n", + "Code Constr: om.constrs[\"rgu\"]=self.om.pg @ self.om.rtn.Mr.v - self.rtn.config.t * self.om.rtn.RR30.v <= 0\n", + "Set constrs rgd: -pg @ Mr - t dot RR30 <= 0\n", + "Code Constr: om.constrs[\"rgd\"]=-self.om.pg @ self.om.rtn.Mr.v - self.rtn.config.t * self.om.rtn.RR30.v <= 0\n", + "Set constrs rgu0: pg[:, 0] - pg0 - R30 <= 0\n", + "Code Constr: om.constrs[\"rgu0\"]=self.om.pg[:, 0] - self.om.rtn.pg0.v - self.om.rtn.R30.v <= 0\n", + "Set constrs rgd0: - pg[:, 0] + pg0 - R30 <= 0\n", + "Code Constr: om.constrs[\"rgd0\"]=- self.om.pg[:, 0] + self.om.rtn.pg0.v - self.om.rtn.R30.v <= 0\n", + "Set constrs ceb: uce + ude - 1 == 0\n", + "Code Constr: om.constrs[\"ceb\"]=self.om.uce + self.om.ude - 1 == 0\n", + "Set constrs cpe: ce @ pg - zce - zde == 0\n", + "Code Constr: om.constrs[\"cpe\"]=self.om.rtn.ce.v @ self.om.pg - self.om.zce - self.om.zde == 0\n", + "Set constrs zce1: -zce + pce <= 0\n", + "Code Constr: om.constrs[\"zce1\"]=-self.om.zce + self.om.pce <= 0\n", + "Set constrs zce2: zce - pce - Mb dot (1-uce) <= 0\n", + "Code Constr: om.constrs[\"zce2\"]=self.om.zce - self.om.pce - self.om.rtn.Mb.v * (1-self.om.uce) <= 0\n", + "Set constrs zce3: zce - Mb dot uce <= 0\n", + "Code Constr: om.constrs[\"zce3\"]=self.om.zce - self.om.rtn.Mb.v * self.om.uce <= 0\n", + "Set constrs zde1: -zde + pde <= 0\n", + "Code Constr: om.constrs[\"zde1\"]=-self.om.zde + self.om.pde <= 0\n", + "Set constrs zde2: zde - pde - Mb dot (1-ude) <= 0\n", + "Code Constr: om.constrs[\"zde2\"]=self.om.zde - self.om.pde - self.om.rtn.Mb.v * (1-self.om.ude) <= 0\n", + "Set constrs zde3: zde - Mb dot ude <= 0\n", + "Code Constr: om.constrs[\"zde3\"]=self.om.zde - self.om.rtn.Mb.v * self.om.ude <= 0\n", + "Set constrs SOCb: mul(EnR, SOC @ Mre) - t dot mul(EtaCR, zce[:, 1:]) + t dot mul(REtaDR, zde[:, 1:]) == 0\n", + "Code Constr: om.constrs[\"SOCb\"]=cp.multiply(self.om.rtn.EnR.v, self.om.SOC @ self.om.rtn.Mre.v) - self.rtn.config.t * cp.multiply(self.om.rtn.EtaCR.v, self.om.zce[:, 1:]) + self.rtn.config.t * cp.multiply(self.om.rtn.REtaDR.v, self.om.zde[:, 1:]) == 0\n", + "Set constrs SOCb: mul(En, SOC[:, 0] - SOCinit) - t dot mul(EtaC, zce[:, 0]) + t dot mul(REtaD, zde[:, 0]) == 0\n", + "Code Constr: om.constrs[\"SOCb\"]=cp.multiply(self.om.rtn.En.v, self.om.SOC[:, 0] - self.om.rtn.SOCinit.v) - self.rtn.config.t * cp.multiply(self.om.rtn.EtaC.v, self.om.zce[:, 0]) + self.rtn.config.t * cp.multiply(self.om.rtn.REtaD.v, self.om.zde[:, 0]) == 0\n", + "Set constrs SOCr: SOC[:, -1] - SOCinit == 0\n", + "Code Constr: om.constrs[\"SOCr\"]=self.om.SOC[:, -1] - self.om.rtn.SOCinit.v == 0\n", + "Set obj tc: min. sum(c2 @ (t dot pg)**2 + c1 @ (t dot pg) + ug * c0) + sum(csr * ug * (Rpmax - pg))\n", + "Code Obj: om.obj=cp.Minimize(cp.sum(self.om.rtn.c2.v @ (self.rtn.config.t * self.om.pg)**2 + self.om.rtn.c1.v @ (self.rtn.config.t * self.om.pg) + self.om.rtn.ug.v @ self.om.rtn.c0.v) + cp.sum(self.om.rtn.csr.v @ self.om.rtn.ug.v * (self.om.rtn.Rpmax.v - self.om.pg)))\n", + "Routine initialized in 0.1141 seconds.\n" + ] + }, + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sp.ED2.init(no_code=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "ED2 has already been initialized.\n", + "ED2 solved as optimal in 0.0905 seconds, converged after 68 iterations using solver GUROBI.\n" + ] + }, + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sp.ED2.run(solver=\"GUROBI\")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "pg=[3.76453119 3.72343444 3.65885384]\n", + "uce=[[0. 0. 1.]\n", + " [0. 0. 1.]]\n", + "pce=[[0. 0. 1.829]\n", + " [0. 0. 1.829]]\n", + "zce=[[0. 0. 1.829]\n", + " [0. 0. 1.829]]\n", + "ude=[[1. 1. 0.]\n", + " [1. 1. 0.]]\n", + "pde=[[1.882 1.862 0. ]\n", + " [1.882 1.862 0. ]]\n", + "zde=[[1.882 1.862 0. ]\n", + " [1.882 1.862 0. ]]\n", + "SOC=[[0.481 0.1 0.5 ]\n", + " [0.481 0.1 0.5 ]]\n", + "obj=45508.58507547713\n" + ] + } + ], + "source": [ + "print(f\"pg={sp.ED2.pg.v[0, :]}\")\n", + "\n", + "nr = 3\n", + "\n", + "print(f\"uce={sp.ED2.uce.v}\")\n", + "print(f\"pce={sp.ED2.pce.v.round(nr)}\")\n", + "print(f\"zce={sp.ED2.zce.v.round(nr)}\")\n", + "\n", + "print(f\"ude={sp.ED2.ude.v}\")\n", + "print(f\"pde={sp.ED2.pde.v.round(nr)}\")\n", + "print(f\"zde={sp.ED2.zde.v.round(nr)}\")\n", + "\n", + "print(f\"SOC={sp.ED2.SOC.v.round(nr)}\")\n", + "print(f\"obj={sp.ED2.obj.v}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'mul(EnR, SOC @ Mre) - t dot mul(EtaCR, zce[:, 1:]) + t dot mul(REtaDR, zde[:, 1:])'" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sp.ED2.SOCb.e_str" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[-38.11773441, 40. ],\n", + " [-38.11773441, 40. ]])" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "np.multiply(sp.ED2.EnR.v, np.matmul(sp.ED2.SOC.v, sp.ED2.Mre.v))" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[-0. , -1.82942692],\n", + " [-0. , -1.82942692]])" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "-sp.ED2.config.t * np.multiply(sp.ED2.EtaCR.v, sp.ED2.zce.v[:, 1:])" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[1.86171722, 0. ],\n", + " [1.86171722, 0. ]])" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sp.ED2.config.t * np.multiply(sp.ED2.REtaDR.v, sp.ED2.zde.v[:, 1:])" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'mul(En, SOC[:, 0] - SOCinit) - t dot mul(EtaC, zce[:, 0]) + t dot mul(REtaD, zde[:, 0])'" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sp.ED2.SOCb0.e_str" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([-1.88226559, -1.88226559])" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "np.multiply(sp.ED2.En.v, sp.ED2.SOC.v[:, 0]-sp.ED2.SOCinit.v)" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([-0., -0.])" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "-sp.ED2.config.t * np.multiply(sp.ED2.EtaC.v, sp.ED2.zce.v[:, 0])" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([1.88226559, 1.88226559])" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sp.ED2.config.t * np.multiply(sp.ED2.REtaD.v, sp.ED2.zde.v[:, 0])" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "ams", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "d2b3bf80176349caa68dc4a3c77bd06eaade8abc678330f7d1c813c53380e5d2" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/ex1.ipynb b/examples/ex1.ipynb index 66ebb923..2fd38dc9 100644 --- a/examples/ex1.ipynb +++ b/examples/ex1.ipynb @@ -114,7 +114,7 @@ "source": [ "AMS support multiple input file formats, including AMS ``.xlsx`` file, MATPOWER ``.m`` file, PYPOWER ``.py`` file, and PSS/E ``.raw`` file.\n", "\n", - "Here we use the AMS ``.xlsx`` file as an example. The source file locates at ``$HOME/ams/ams/cases/``." + "Here we use the AMS ``.xlsx`` file as an example. The source file locates at ``$HOME/ams/ams/cases/ieee39/ieee39_uced.xlsx``." ] }, { From bc1ab208aecc880272b67c121a9e020bd05bc27a Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 13 Nov 2023 22:06:20 -0500 Subject: [PATCH 46/77] Refactor OModel Vars and Constrs setattr --- ams/opt/omodel.py | 51 ++++++++++++++++++++++++++++------------- ams/routines/routine.py | 2 ++ 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index b58ff7fb..e11c9fd4 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -4,7 +4,7 @@ import logging # NOQA -from typing import Optional, Union # NOQA +from typing import Any, Optional, Union # NOQA from collections import OrderedDict # NOQA import re # NOQA @@ -49,6 +49,7 @@ def __init__(self, self.info = info self.unit = unit self.is_disabled = False + self.optz = None # corresponding optimization element def parse(self): """ @@ -222,6 +223,9 @@ def __init__(self, self._shape = shape self._v = None + self.optz_lb = None + self.optz_ub = None + self.config = Config(name=self.class_name) # `config` that can be exported self.config.add(OrderedDict((('nonneg', nonneg), @@ -308,9 +312,8 @@ def parse(self): code_var = f"tmp=var({shape}, **config)" for pattern, replacement, in sub_map.items(): code_var = re.sub(pattern, replacement, code_var) - exec(code_var) - exec("om.vars[self.name] = tmp") - exec(f'setattr(om, self.name, om.vars["{self.name}"])') + exec(code_var) # build the Var object + exec("self.optz = tmp") # assign the to the optz attribute u_ctrl = self.ctrl.v if self.ctrl else np.ones(nr) v0 = self.v0.v if self.v0 else np.zeros(nr) if self.lb: @@ -320,8 +323,7 @@ def parse(self): elv = u_ctrl * u * lv + (1 - u_ctrl) * v0 # fit variable shape if horizon exists elv = np.tile(elv, (nc, 1)).T if nc > 0 else elv - exec("om.constrs[self.lb.name] = tmp >= elv") - exec("setattr(om, self.lb.name, om.constrs[self.lb.name])") + exec("self.optz_lb = tmp >= elv") if self.ub: uv = self.ub.owner.get(src=self.ub.name, idx=self.get_idx(), attr='v') u = self.lb.owner.get(src='u', idx=self.get_idx(), attr='v') @@ -329,8 +331,7 @@ def parse(self): euv = u_ctrl * u * uv + (1 - u_ctrl) * v0 # fit variable shape if horizon exists euv = np.tile(euv, (nc, 1)).T if nc > 0 else euv - exec("om.constrs[self.ub.name] = tmp <= euv") - exec("setattr(om, self.ub.name, om.constrs[self.ub.name])") + exec("self.optz_ub = tmp <= euv") return True def __repr__(self): @@ -411,17 +412,16 @@ def parse(self, no_code=True): logger.error(f"Error in parsing constr <{self.name}>.") raise e if self.type == 'uq': - code_constr = f'{code_constr} <= 0' + code_constr = f'tmp = {code_constr} <= 0' elif self.type == 'eq': - code_constr = f'{code_constr} == 0' + code_constr = f'tmp = {code_constr} == 0' else: raise ValueError(f'Constraint type {self.type} is not supported.') - code_constr = f'om.constrs["{self.name}"]=' + code_constr logger.debug(f"Set constrs {self.name}: {self.e_str} {'<= 0' if self.type == 'uq' else '== 0'}") if not no_code: logger.info(f"Code Constr: {code_constr}") exec(code_constr) - exec(f'setattr(om, self.name, om.constrs["{self.name}"])') + exec("self.optz = tmp") return True def __repr__(self): @@ -594,12 +594,15 @@ def setup(self, no_code=True, force_generate=False): """ rtn = self.rtn rtn.syms.generate_symbols(force_generate=force_generate) + # TODO: add Service as cp.Parameter # --- add decision variables --- - for ovar in rtn.vars.values(): - ovar.parse() + for key, val in rtn.vars.items(): + val.parse() + setattr(self, key, val.optz) # --- add constraints --- - for constr in rtn.constrs.values(): - constr.parse(no_code=no_code) + for key, val in rtn.constrs.items(): + val.parse(no_code=no_code) + setattr(self, key, val.optz) # --- parse objective functions --- if rtn.type == 'PF': # NOTE: power flow type has no objective function @@ -631,3 +634,19 @@ def class_name(self): Return the class name """ return self.__class__.__name__ + + def __register_attribute(self, key, value): + """ + Register a pair of attributes to OModel instance. + + Called within ``__setattr__``, this is where the magic happens. + Subclass attributes are automatically registered based on the variable type. + """ + if isinstance(value, cp.Variable): + self.vars[key] = value + elif isinstance(value, cp.Constraint): + self.constrs[key] = value + + def __setattr__(self, __name: str, __value: Any): + self.__register_attribute(__name, __value) + super().__setattr__(__name, __value) diff --git a/ams/routines/routine.py b/ams/routines/routine.py index 0064d464..fe1e1bbf 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -449,8 +449,10 @@ def _register_attribute(self, key, value): value.om = self.om if isinstance(value, Var): self.vars[key] = value + self.om.vars[key] = None elif isinstance(value, Constraint): self.constrs[key] = value + self.om.constrs[key] = None elif isinstance(value, RParam): self.rparams[key] = value elif isinstance(value, RBaseService): From d57bf829ddb7bf6bb92539543d1b7c61f320f504 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Tue, 14 Nov 2023 01:24:40 -0500 Subject: [PATCH 47/77] Refactor OModel to collect params --- ams/core/matprocessor.py | 20 +++++---- ams/core/param.py | 5 ++- ams/core/service.py | 9 ++-- ams/core/symprocessor.py | 1 + ams/opt/omodel.py | 88 +++++++++++++++++++++++++++++++--------- ams/routines/routine.py | 23 +++++++++-- 6 files changed, 110 insertions(+), 36 deletions(-) diff --git a/ams/core/matprocessor.py b/ams/core/matprocessor.py index c0855530..ff9163f4 100644 --- a/ams/core/matprocessor.py +++ b/ams/core/matprocessor.py @@ -2,20 +2,23 @@ Module for system matrix make. """ -import logging # NOQA -from typing import Optional # NOQA +import logging +from typing import Optional -import numpy as np # NOQA -from ams.pypower.make import makePTDF, makeBdc # NOQA -from ams.io.pypower import system2ppc # NOQA +import numpy as np -from scipy.sparse import csr_matrix as c_sparse # NOQA -from scipy.sparse import lil_matrix as l_sparse # NOQA +from scipy.sparse import csr_matrix as c_sparse +from scipy.sparse import lil_matrix as l_sparse + +from ams.pypower.make import makePTDF, makeBdc +from ams.io.pypower import system2ppc + +from ams.opt.omodel import Param logger = logging.getLogger(__name__) -class MParam: +class MParam(Param): """ Class for matrix parameters built from the system. @@ -47,6 +50,7 @@ def __init__(self, unit: Optional[str] = None, v: Optional[np.ndarray] = None, ): + Param.__init__(self, name=name, info=info) self.name = name self.tex_name = tex_name if (tex_name is not None) else name self.info = info diff --git a/ams/core/param.py b/ams/core/param.py index aa989c4d..134f81c6 100644 --- a/ams/core/param.py +++ b/ams/core/param.py @@ -16,10 +16,12 @@ from ams.core.var import Algeb # NOQA +from ams.opt.omodel import Param # NOQA + logger = logging.getLogger(__name__) -class RParam: +class RParam(Param): """ Class for parameters used in a routine. This class is developed to simplify the routine definition. @@ -79,6 +81,7 @@ def __init__(self, indexer: Optional[str] = None, imodel: Optional[str] = None, ): + Param.__init__(self, name=name, info=info, src=src, unit=unit) self.name = name self.tex_name = tex_name if (tex_name is not None) else name diff --git a/ams/core/service.py b/ams/core/service.py index d9ac5d8a..d58d6041 100644 --- a/ams/core/service.py +++ b/ams/core/service.py @@ -9,11 +9,13 @@ from andes.core.service import BaseService, BackRef, RefFlatten # NOQA +from ams.opt.omodel import Param + logger = logging.getLogger(__name__) -class RBaseService(BaseService): +class RBaseService(BaseService, Param): """ Base class for services that are used in a routine. Revised from module `andes.core.service.BaseService`. @@ -41,8 +43,9 @@ def __init__(self, info: str = None, vtype: Type = None, ): - super().__init__(name=name, tex_name=tex_name, unit=unit, - info=info, vtype=vtype) + Param.__init__(self, name=name, unit=unit, info=info) + BaseService.__init__(self, name=name, tex_name=tex_name, unit=unit, + info=info, vtype=vtype) self.export = False self.is_group = False self.rtn = None diff --git a/ams/core/symprocessor.py b/ams/core/symprocessor.py index b704c93a..dc38bd1e 100644 --- a/ams/core/symprocessor.py +++ b/ams/core/symprocessor.py @@ -65,6 +65,7 @@ def __init__(self, parent): (r' dot ', r' * '), (r'\bsum\b', f'{lang}.sum'), (r'\bvar\b', f'{lang}.Variable'), + (r'\bparam\b', f'{lang}.Parameter'), (r'\bproblem\b', f'{lang}.Problem'), (r'\bmultiply\b', f'{lang}.multiply'), (r'\bmul\b', f'{lang}.multiply'), # alias for multiply diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index e11c9fd4..f96343ea 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -12,9 +12,6 @@ from andes.core.common import Config # NOQA -from ams.core.param import RParam # NOQA -from ams.core.var import Algeb # NOQA - from ams.utils import timer # NOQA import cvxpy as cp # NOQA @@ -49,6 +46,7 @@ def __init__(self, self.info = info self.unit = unit self.is_disabled = False + self.rtn = None self.optz = None # corresponding optimization element def parse(self): @@ -74,13 +72,6 @@ def n(self): else: return self.owner.n - @property - def rtn(self): - """ - Return the owner routine. - """ - return self.om.rtn - @property def shape(self): """ @@ -104,7 +95,44 @@ def size(self): return None -class Var(Algeb, OptzBase): +class Param(OptzBase): + """ + Base class for parameters used in a routine. + """ + + def __init__(self, + name: Optional[str] = None, + info: Optional[str] = None, + src: Optional[str] = None, + unit: Optional[str] = None, + ): + OptzBase.__init__(self, name=name, info=info, unit=unit) + self.src = src + + def parse(self): + """ + Parse the parameter. + """ + sub_map = self.om.rtn.syms.sub_map + code_param = f"tmp=param(shape={self.v.shape})" + for pattern, replacement, in sub_map.items(): + code_param = re.sub(pattern, replacement, code_param) + exec(code_param) + exec("self.optz = tmp") + exec("self.optz.value = self.v") + return True + + def update(self): + """ + Update the parameter value from RParam, MParam, or Service. + """ + self.optz.value = self.v + + def __repr__(self): + return f'{self.__class__.__name__}: {self.name}' + + +class Var(OptzBase): """ Base class for variables used in a routine. @@ -182,11 +210,11 @@ def __init__(self, unit: Optional[str] = None, model: Optional[str] = None, shape: Optional[Union[tuple, int]] = None, - lb: Optional[RParam] = None, - ub: Optional[str] = None, + lb=None, + ub=None, ctrl: Optional[str] = None, v0: Optional[str] = None, - horizon: Optional[RParam] = None, + horizon=None, nonneg: Optional[bool] = False, nonpos: Optional[bool] = False, complex: Optional[bool] = False, @@ -201,8 +229,6 @@ def __init__(self, pos: Optional[bool] = False, neg: Optional[bool] = False, ): - # Algeb.__init__(self, name=name, tex_name=tex_name, info=info, unit=unit) - # below info is the same as Algeb self.name = name self.info = info self.unit = unit @@ -275,7 +301,6 @@ def parse(self): """ Parse the variable. """ - om = self.om # NOQA sub_map = self.om.rtn.syms.sub_map # only used for CVXPY # NOTE: Config only allow lower case letters, do a conversion here @@ -403,7 +428,6 @@ def parse(self, no_code=True): sub_map = self.om.rtn.syms.sub_map if self.is_disabled: return True - om = self.om # NOQA code_constr = self.e_str for pattern, replacement in sub_map.items(): try: @@ -511,7 +535,6 @@ def parse(self, no_code=True): no_code : bool, optional Flag indicating if the code should be shown, True by default. """ - om = self.om # NOQA sub_map = self.om.rtn.syms.sub_map code_obj = self.e_str for pattern, replacement, in sub_map.items(): @@ -522,7 +545,7 @@ def parse(self, no_code=True): code_obj = f'cp.Maximize({code_obj})' else: raise ValueError(f'Objective sense {self.sense} is not supported.') - code_obj = 'om.obj=' + code_obj + code_obj = 'self.om.obj=' + code_obj logger.debug(f"Set obj {self.name}: {self.sense}. {self.e_str}") if not no_code: logger.info(f"Code Obj: {code_obj}") @@ -567,6 +590,7 @@ class OModel: def __init__(self, routine): self.rtn = routine self.mdl = None + self.params = OrderedDict() self.vars = OrderedDict() self.constrs = OrderedDict() self.obj = None @@ -595,6 +619,12 @@ def setup(self, no_code=True, force_generate=False): rtn = self.rtn rtn.syms.generate_symbols(force_generate=force_generate) # TODO: add Service as cp.Parameter + # --- add RParams and Services as parameters --- + # NOTE: items in ``rtn.params`` are not + # ``RParam`` or ``Service`` instances + for key, val in rtn.params.items(): + val.parse() + setattr(self, key, val.optz) # --- add decision variables --- for key, val in rtn.vars.items(): val.parse() @@ -646,7 +676,25 @@ def __register_attribute(self, key, value): self.vars[key] = value elif isinstance(value, cp.Constraint): self.constrs[key] = value + elif isinstance(value, cp.Parameter): + self.params[key] = value def __setattr__(self, __name: str, __value: Any): self.__register_attribute(__name, __value) super().__setattr__(__name, __value) + + def update_param(self, params=Optional[Union[Param, str, list]]): + """ + Update the Parameter values. + """ + if params is None: + for _, val in self.params.items(): + val.update() + elif isinstance(params, Param): + params.update() + elif isinstance(params, str): + self.params[params].update() + elif isinstance(params, list): + for param in params: + param.update() + return True diff --git a/ams/routines/routine.py b/ams/routines/routine.py index fe1e1bbf..77a850e0 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -17,7 +17,7 @@ from ams.core.symprocessor import SymProcessor from ams.core.documenter import RDocumenter from ams.core.service import RBaseService, ValueService -from ams.opt.omodel import OModel, Var, Constraint, Objective +from ams.opt.omodel import OModel, Param, Var, Constraint, Objective from ams.shared import igraph as ig from ams.shared import require_igraph @@ -33,6 +33,9 @@ class RoutineData: def __init__(self): self.rparams = OrderedDict() # list out RParam in a routine + self.params = OrderedDict() # list out Params in a routine + # --- optimization modeling --- + self.om = OModel(routine=self) class RoutineModel: @@ -55,6 +58,7 @@ def __init__(self, system=None, config=None): self.services = OrderedDict() # list out services in a routine + self.params = OrderedDict() # list out Params in a routine self.vars = OrderedDict() # list out Vars in a routine self.constrs = OrderedDict() self.obj = None @@ -445,18 +449,29 @@ def _register_attribute(self, key, value): Called within ``__setattr__``, this is where the magic happens. Subclass attributes are automatically registered based on the variable type. """ - if isinstance(value, (Var, Constraint, Objective)): + if isinstance(value, (Param, Var, Constraint, Objective)): value.om = self.om + value.rtn = self if isinstance(value, Var): self.vars[key] = value - self.om.vars[key] = None + self.om.vars[key] = None # cp.Variable elif isinstance(value, Constraint): self.constrs[key] = value - self.om.constrs[key] = None + self.om.constrs[key] = None # cp.Constraint elif isinstance(value, RParam): self.rparams[key] = value + self.params[key] = value + self.om.params[key] = None # cp.Parameter elif isinstance(value, RBaseService): self.services[key] = value + self.params[key] = value + self.om.params[key] = None # cp.Parameter + + def update_param(self, params=Optional[Union[Param, str, list]]): + """ + Update parameters in the optimization model. + """ + return self.om.update_param(params=params) def __delattr__(self, name): """ From ba5420e130473116eb885f80a7fae1749c45c497 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 20 Nov 2023 20:31:00 -0500 Subject: [PATCH 48/77] Refactor routines to include parameter in OModel --- ams/core/param.py | 112 +++++++++++--- ams/core/service.py | 83 +++++++--- ams/core/symprocessor.py | 12 +- ams/opt/omodel.py | 127 +++++++++++++--- ams/routines/acopf.py | 50 ++---- ams/routines/cpf.py | 36 ++--- ams/routines/dcopf.py | 97 ++++++------ ams/routines/dopf.py | 93 ++++-------- ams/routines/ed.py | 179 ++++++++++------------ ams/routines/pflow.py | 46 ++---- ams/routines/routine.py | 26 ++-- ams/routines/rted.py | 321 ++++++++++++++++++++++----------------- ams/routines/uc.py | 159 ++++++++----------- 13 files changed, 725 insertions(+), 616 deletions(-) diff --git a/ams/core/param.py b/ams/core/param.py index 134f81c6..9519ce9a 100644 --- a/ams/core/param.py +++ b/ams/core/param.py @@ -3,20 +3,19 @@ """ -import logging # NOQA +import logging -from typing import Optional # NOQA +from typing import Optional +from collections import Iterable -import numpy as np # NOQA -from scipy.sparse import issparse # NOQA +import numpy as np +from scipy.sparse import issparse -from andes.core.common import Config # NOQA from andes.core import BaseParam, DataParam, IdxParam, NumParam, ExtParam # NOQA from andes.models.group import GroupBase # NOQA from ams.core.var import Algeb # NOQA - -from ams.opt.omodel import Param # NOQA +from ams.opt.omodel import Param logger = logging.getLogger(__name__) @@ -26,6 +25,23 @@ class RParam(Param): Class for parameters used in a routine. This class is developed to simplify the routine definition. + `RParm` is further used to define `Parameter` or `Constant` + in the optimization model. + + `no_parse` is used to skip parsing the `RParam` in optimization + model. + This is useful when the RParam contains non-numeric values, + or it is not necessary to be added to the optimization model. + + `const` is used to define the parameter as a `Constant`, + otherwise it will be defined as a `Parameter`. + The key difference between `Parameter` and `Constant` in optimization + is that `Parameter` is mutable but `Constant` is not. + + Note that if `const=True`, following input parameters will + be ignored: `nonneg`, `nonpos`, `complex`, `imag`, `symmetric`, + `diag`, `hermitian`, `boolean`, `integer`, `pos`, `neg`, `sparsity`. + Parameters ---------- name : str, optional @@ -48,6 +64,34 @@ class RParam(Param): Indexer of the parameter. imodel : str, optional Name of the owner model or group of the indexer. + no_parse: bool, optional + True to skip parsing the parameter. + const: bool, optional + True to set the parameter as constant. + nonneg: bool, optional + True to set the parameter as non-negative. + nonpos: bool, optional + True to set the parameter as non-positive. + complex: bool, optional + True to set the parameter as complex. + imag: bool, optional + True to set the parameter as imaginary. + symmetric: bool, optional + True to set the parameter as symmetric. + diag: bool, optional + True to set the parameter as diagonal. + hermitian: bool, optional + True to set the parameter as hermitian. + boolean: bool, optional + True to set the parameter as boolean. + integer: bool, optional + True to set the parameter as integer. + pos: bool, optional + True to set the parameter as positive. + neg: bool, optional + True to set the parameter as negative. + sparsity: list, optional + Sparsity pattern of the parameter. Examples -------- @@ -80,9 +124,26 @@ def __init__(self, v: Optional[np.ndarray] = None, indexer: Optional[str] = None, imodel: Optional[str] = None, + expand_dims: Optional[int] = None, + no_parse: Optional[bool] = False, + const: Optional[bool] = False, + nonneg: Optional[bool] = False, + nonpos: Optional[bool] = False, + complex: Optional[bool] = False, + imag: Optional[bool] = False, + symmetric: Optional[bool] = False, + diag: Optional[bool] = False, + hermitian: Optional[bool] = False, + boolean: Optional[bool] = False, + integer: Optional[bool] = False, + pos: Optional[bool] = False, + neg: Optional[bool] = False, + sparsity: Optional[list] = None, ): - Param.__init__(self, name=name, info=info, src=src, unit=unit) - + Param.__init__(self, const=const, nonneg=nonneg, nonpos=nonpos, + complex=complex, imag=imag, symmetric=symmetric, + diag=diag, hermitian=hermitian, boolean=boolean, + integer=integer, pos=pos, neg=neg, sparsity=sparsity) self.name = name self.tex_name = tex_name if (tex_name is not None) else name self.info = info @@ -92,6 +153,8 @@ def __init__(self, self.model = model # name of a group or model self.indexer = indexer # name of the indexer self.imodel = imodel # name of a group or model of the indexer + self.expand_dims = expand_dims + self.no_parse = no_parse self.owner = None # instance of the owner model or group self.rtn = None # instance of the owner routine self.is_ext = False # indicate if the value is set externally @@ -109,18 +172,19 @@ def v(self): - This property is a wrapper for the ``get`` method of the owner class. - The value will sort by the indexer if indexed, used for optmization modeling. """ + out = None if self.indexer is None: if self.is_ext: if issparse(self._v): - return self._v.toarray() + out = self._v.toarray() else: - return self._v + out = self._v elif self.is_group: - return self.owner.get(src=self.src, attr='v', - idx=self.owner.get_idx()) + out = self.owner.get(src=self.src, attr='v', + idx=self.owner.get_idx()) else: src_param = getattr(self.owner, self.src) - return getattr(src_param, 'v') + out = getattr(src_param, 'v') else: try: imodel = getattr(self.rtn.system, self.imodel) @@ -133,15 +197,29 @@ def v(self): except Exception as e: raise e model = getattr(self.rtn.system, self.model) - sorted_v = model.get(src=self.src, attr='v', idx=sorted_idx) - return sorted_v + out = model.get(src=self.src, attr='v', idx=sorted_idx) + if self.expand_dims is not None: + out = np.expand_dims(out, axis=self.expand_dims) + return out @property def shape(self): """ Return the shape of the parameter. """ - return self.v.shape + return np.shape(self.v) + + @property + def dtype(self): + """ + Return the data type of the parameter value. + """ + if isinstance(self.v, (str, bytes)): + return str + elif isinstance(self.v, Iterable): + return type(self.v[0]) + else: + return type(self.v) @property def n(self): diff --git a/ams/core/service.py b/ams/core/service.py index d58d6041..98c69c0c 100644 --- a/ams/core/service.py +++ b/ams/core/service.py @@ -34,6 +34,8 @@ class RBaseService(BaseService, Param): Variable type. model : str, optional Model name. + no_parse: bool, optional + True to skip parsing the service. """ def __init__(self, @@ -42,8 +44,11 @@ def __init__(self, unit: str = None, info: str = None, vtype: Type = None, + no_parse: bool = False, + const: bool = False, ): - Param.__init__(self, name=name, unit=unit, info=info) + Param.__init__(self, name=name, unit=unit, info=info, + no_parse=no_parse, const=const) BaseService.__init__(self, name=name, tex_name=tex_name, unit=unit, info=info, vtype=vtype) self.export = False @@ -122,9 +127,12 @@ def __init__(self, unit: str = None, info: str = None, vtype: Type = None, + const: bool = False, + no_parse: bool = False, ): super().__init__(name=name, tex_name=tex_name, unit=unit, - info=info, vtype=vtype) + info=info, vtype=vtype, const=const, + no_parse=no_parse) self._v = value @property @@ -163,9 +171,12 @@ def __init__(self, tex_name: str = None, unit: str = None, info: str = None, - vtype: Type = None,): + vtype: Type = None, + const: bool = False, + no_parse: bool = False,): super().__init__(name=name, tex_name=tex_name, unit=unit, - info=info, vtype=vtype) + info=info, vtype=vtype, const=const, + no_parse=no_parse) self.u = u @@ -199,10 +210,12 @@ def __init__(self, tex_name: str = None, unit: str = None, info: str = None, + const: bool = False, + no_parse: bool = False, ): tex_name = tex_name if tex_name is not None else u.tex_name super().__init__(name=name, tex_name=tex_name, unit=unit, - info=info, u=u,) + info=info, u=u, const=const, no_parse=no_parse) self.sd = sd self.Cl = Cl @@ -264,10 +277,13 @@ def __init__(self, rfun: Callable = None, rargs: dict = {}, expand_dims: int = None, - array_out=True): + array_out=True, + const: bool = False, + no_parse: bool = False,): tex_name = tex_name if tex_name is not None else u.tex_name super().__init__(name=name, tex_name=tex_name, unit=unit, - info=info, vtype=vtype, u=u,) + info=info, vtype=vtype, u=u, + const=const, no_parse=no_parse) self.fun = fun self.args = args self.rfun = rfun @@ -335,11 +351,14 @@ def __init__(self, unit: str = None, info: str = None, vtype: Type = None, - array_out: bool = True,): + array_out: bool = True, + const: bool = False, + no_parse: bool = False,): super().__init__(name=name, tex_name=tex_name, unit=unit, info=info, vtype=vtype, u=u, fun=np.expand_dims, args=args, - array_out=array_out) + array_out=array_out, const=const, + no_parse=no_parse) self.axis = axis @property @@ -397,14 +416,17 @@ def __init__(self, rfun: Callable = None, rargs: dict = {}, expand_dims: int = None, - array_out=True): + array_out=True, + const: bool = False, + no_parse: bool = False,): tex_name = tex_name if tex_name is not None else u.tex_name super().__init__(name=name, tex_name=tex_name, unit=unit, info=info, vtype=vtype, u=u, fun=fun, args=args, rfun=rfun, rargs=rargs, expand_dims=expand_dims, - array_out=array_out) + array_out=array_out, const=const, + no_parse=no_parse) self.u2 = u2 @property @@ -444,13 +466,16 @@ def __init__(self, tex_name: str = None, unit: str = None, info: str = None, - vtype: Type = None,): + vtype: Type = None, + const: bool = False, + no_parse: bool = False,): tex_name = tex_name if tex_name is not None else u.tex_name super().__init__(name=name, tex_name=tex_name, unit=unit, info=info, vtype=vtype, u=u, u2=u2, fun=None, args=None, rfun=None, rargs=None, - expand_dims=None) + expand_dims=None, const=const, + no_parse=no_parse) if self.u.horizon is None: msg = f'{self.class_name} {self.name}.u {self.u.name} has no horizon, likely a modeling error.' logger.error(msg) @@ -475,7 +500,8 @@ def v(self): class NumHstack(NumOp): """ - Repeat an array along the second axis nc times + Repeat an array along the second axis nc times or the length of + reference array, using NumPy's hstack function, where nc is the column number of the reference array, ``np.hstack([u.v[:, np.newaxis] * ref.shape[1]], **kwargs)``. @@ -510,20 +536,23 @@ def __init__(self, info: str = None, vtype: Type = None, rfun: Callable = None, - rargs: dict = {}): + rargs: dict = {}, + const: bool = False, + no_parse: bool = False,): super().__init__(name=name, tex_name=tex_name, unit=unit, info=info, vtype=vtype, u=u, fun=np.hstack, args=args, - rfun=rfun, rargs=rargs) + rfun=rfun, rargs=rargs, const=const, + no_parse=no_parse) self.ref = ref @property def v0(self): nc = 1 - if hasattr(self.ref, "shape"): - nc = self.ref.shape[1] - elif isinstance(self.ref.v, (list, tuple)): + if isinstance(self.ref.v, (list, tuple)): nc = len(self.ref.v) + elif hasattr(self.ref, "shape"): + nc = self.ref.shape[1] else: raise AttributeError(f"{self.rtn.class_name}: ref {self.ref.name} has no attribute shape nor length.") return self.fun([self.u.v[:, np.newaxis]] * nc, @@ -592,11 +621,14 @@ def __init__(self, vtype: Type = None, rfun: Callable = None, rargs: dict = {}, + const: bool = False, + no_parse: bool = False, ): super().__init__(name=name, tex_name=tex_name, unit=unit, info=info, vtype=vtype, u=u, fun=None, args={}, - rfun=rfun, rargs=rargs) + rfun=rfun, rargs=rargs, + const=const, no_parse=no_parse) self.zone = zone @property @@ -673,11 +705,14 @@ def __init__(self, rfun: Callable = None, rargs: dict = {}, array_out: bool = True, + const: bool = False, + no_parse: bool = False, **kwargs ): super().__init__(name=name, tex_name=tex_name, unit=unit, info=info, vtype=vtype, u=u, fun=None, rfun=rfun, rargs=rargs, array_out=array_out, + const=const, no_parse=no_parse, **kwargs) self.indexer = indexer self.gamma = gamma @@ -761,11 +796,14 @@ def __init__(self, vtype: Type = None, rfun: Callable = None, rargs: dict = {}, + const: bool = False, + no_parse: bool = False, **kwargs ): super().__init__(name=name, tex_name=tex_name, unit=unit, info=info, vtype=vtype, u=u, fun=None, rfun=rfun, rargs=rargs, + const=const, no_parse=no_parse, **kwargs) self.fun = fun @@ -814,10 +852,13 @@ def __init__(self, vtype: Type = None, rfun: Callable = None, rargs: dict = {}, + const: bool = False, + no_parse: bool = False, ): super().__init__(name=name, tex_name=tex_name, unit=unit, info=info, vtype=vtype, - u=u, fun=None, rfun=rfun, rargs=rargs,) + u=u, fun=None, rfun=rfun, rargs=rargs, + const=const, no_parse=no_parse,) @property def v0(self): diff --git a/ams/core/symprocessor.py b/ams/core/symprocessor.py index dc38bd1e..14d2685f 100644 --- a/ams/core/symprocessor.py +++ b/ams/core/symprocessor.py @@ -66,6 +66,7 @@ def __init__(self, parent): (r'\bsum\b', f'{lang}.sum'), (r'\bvar\b', f'{lang}.Variable'), (r'\bparam\b', f'{lang}.Parameter'), + (r'\bconst\b', f'{lang}.Constant'), (r'\bproblem\b', f'{lang}.Problem'), (r'\bmultiply\b', f'{lang}.multiply'), (r'\bmul\b', f'{lang}.multiply'), # alias for multiply @@ -74,6 +75,11 @@ def __init__(self, parent): (r'\bpos\b', f'{lang}.pos'), (r'\bpower\b', f'{lang}.power'), (r'\bsign\b', f'{lang}.sign'), + (r'\bsquare\b', f'{lang}.square'), + (r'\bquad_over_lin\b', f'{lang}.quad_over_lin'), + (r'\bdiag\b', f'{lang}.diag'), + (r'\bquad_form\b', f'{lang}.quad_form'), + (r'\bsum_squares\b', f'{lang}.sum_squares'), ]) self.tex_map = OrderedDict([ @@ -125,7 +131,8 @@ def generate_symbols(self, force_generate=False): for rpname, rparam in self.parent.rparams.items(): tmp = sp.symbols(f'{rparam.name}') self.inputs_dict[rpname] = tmp - self.sub_map[rf"\b{rpname}\b"] = f'self.om.rtn.{rpname}.v' + sub_name = f'self.rtn.{rpname}.v' if rparam.no_parse else f'self.om.{rpname}' + self.sub_map[rf"\b{rpname}\b"] = sub_name self.tex_map[rf"\b{rpname}\b"] = f'{rparam.tex_name}' # Routine Services @@ -133,7 +140,8 @@ def generate_symbols(self, force_generate=False): tmp = sp.symbols(f'{service.name}') self.services_dict[sname] = tmp self.inputs_dict[sname] = tmp - self.sub_map[rf"\b{sname}\b"] = f'self.om.rtn.{sname}.v' + sub_name = f'self.rtn.{sname}.v' if service.no_parse else f'self.om.{sname}' + self.sub_map[rf"\b{sname}\b"] = sub_name self.tex_map[rf"\b{sname}\b"] = f'{service.tex_name}' # store tex names defined in `self.config` diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index f96343ea..1a1bcffd 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -1,20 +1,19 @@ """ -Module for optimization models. +Module for optimization modeling. """ +import logging -import logging # NOQA +from typing import Any, Optional, Union +from collections import OrderedDict +import re -from typing import Any, Optional, Union # NOQA -from collections import OrderedDict # NOQA -import re # NOQA +import numpy as np -import numpy as np # NOQA +from andes.core.common import Config -from andes.core.common import Config # NOQA +from ams.utils import timer -from ams.utils import timer # NOQA - -import cvxpy as cp # NOQA +import cvxpy as cp logger = logging.getLogger(__name__) @@ -98,28 +97,103 @@ def size(self): class Param(OptzBase): """ Base class for parameters used in a routine. + + Parameters + ---------- + const: bool, optional + True to set the parameter as constant. + no_parse: bool, optional + True to skip parsing the parameter. + nonneg: bool, optional + True to set the parameter as non-negative. + nonpos: bool, optional + True to set the parameter as non-positive. + complex: bool, optional + True to set the parameter as complex. + imag: bool, optional + True to set the parameter as imaginary. + symmetric: bool, optional + True to set the parameter as symmetric. + diag: bool, optional + True to set the parameter as diagonal. + hermitian: bool, optional + True to set the parameter as hermitian. + boolean: bool, optional + True to set the parameter as boolean. + integer: bool, optional + True to set the parameter as integer. + pos: bool, optional + True to set the parameter as positive. + neg: bool, optional + True to set the parameter as negative. + sparsity: list, optional + Sparsity pattern of the parameter. """ def __init__(self, name: Optional[str] = None, info: Optional[str] = None, - src: Optional[str] = None, unit: Optional[str] = None, + no_parse: Optional[bool] = False, + const: Optional[bool] = False, + nonneg: Optional[bool] = False, + nonpos: Optional[bool] = False, + complex: Optional[bool] = False, + imag: Optional[bool] = False, + symmetric: Optional[bool] = False, + diag: Optional[bool] = False, + hermitian: Optional[bool] = False, + boolean: Optional[bool] = False, + integer: Optional[bool] = False, + pos: Optional[bool] = False, + neg: Optional[bool] = False, + sparsity: Optional[list] = None, ): OptzBase.__init__(self, name=name, info=info, unit=unit) - self.src = src + self.no_parse = no_parse # True to skip parsing the parameter + self.sparsity = sparsity # sparsity pattern + + self.config = Config(name=self.class_name) # `config` that can be exported + + self.config.add(OrderedDict((('nonneg', nonneg), + ('nonpos', nonpos), + ('complex', complex), + ('imag', imag), + ('symmetric', symmetric), + ('diag', diag), + ('hermitian', hermitian), + ('boolean', boolean), + ('integer', integer), + ('pos', pos), + ('neg', neg), + ('const', const), + ))) def parse(self): """ Parse the parameter. """ + config = self.config.as_dict() # NOQA sub_map = self.om.rtn.syms.sub_map - code_param = f"tmp=param(shape={self.v.shape})" + if self.config.const: + code_param = "tmp=const(value=self.v)" + else: + shape = np.shape(self.v) + config.pop('const', None) + code_param = f"tmp=param(shape={shape}, **config)" for pattern, replacement, in sub_map.items(): code_param = re.sub(pattern, replacement, code_param) exec(code_param) - exec("self.optz = tmp") - exec("self.optz.value = self.v") + exec("self.optz=tmp") + if not self.config.const: + try: + exec("self.optz.value = self.v") + except ValueError: + msg = f"Parameter <{self.name}> has non-numeric value, " + msg += "no_parse=True is applied." + logger.warning(msg) + self.no_parse = True + return False return True def update(self): @@ -559,7 +633,7 @@ def __repr__(self): class OModel: - r""" + """ Base class for optimization models. Parameters @@ -571,6 +645,8 @@ class OModel: ---------- mdl: cvxpy.Problem Optimization model. + params: OrderedDict + Parameters. vars: OrderedDict Decision variables. constrs: OrderedDict @@ -581,15 +657,12 @@ class OModel: Number of decision variables. m: int Number of constraints. - - TODO: - - Add _check_attribute and _register_attribute for vars, constrs, and obj. - - Add support for user-defined vars, constrs, and obj. """ def __init__(self, routine): self.rtn = routine self.mdl = None + self.consts = OrderedDict() self.params = OrderedDict() self.vars = OrderedDict() self.constrs = OrderedDict() @@ -618,13 +691,16 @@ def setup(self, no_code=True, force_generate=False): """ rtn = self.rtn rtn.syms.generate_symbols(force_generate=force_generate) - # TODO: add Service as cp.Parameter # --- add RParams and Services as parameters --- - # NOTE: items in ``rtn.params`` are not - # ``RParam`` or ``Service`` instances - for key, val in rtn.params.items(): + # logger.debug(f"params: {rtn.params.keys()}") + for key, val in rtn.consts.items(): val.parse() setattr(self, key, val.optz) + for key, val in rtn.params.items(): + # logger.debug(f"Parsing param {key}") + if not val.no_parse: + val.parse() + setattr(self, key, val.optz) # --- add decision variables --- for key, val in rtn.vars.items(): val.parse() @@ -698,3 +774,6 @@ def update_param(self, params=Optional[Union[Param, str, list]]): for param in params: param.update() return True + + def __repr__(self) -> str: + return f'{self.rtn.class_name}.{self.__class__.__name__} at {hex(id(self))}' diff --git a/ams/routines/acopf.py b/ams/routines/acopf.py index 082cecd0..24444b36 100644 --- a/ams/routines/acopf.py +++ b/ams/routines/acopf.py @@ -10,26 +10,12 @@ from ams.io.pypower import system2ppc # NOQA from ams.core.param import RParam # NOQA -from ams.routines.dcopf import DCOPFData # NOQA from ams.routines.dcpf import DCPFlowBase # NOQA from ams.opt.omodel import Var, Constraint, Objective # NOQA logger = logging.getLogger(__name__) -class ACOPFData(DCOPFData): - """ - ACOPF data. - """ - - def __init__(self): - DCOPFData.__init__(self) - self.ql = RParam(info='reactive power demand (system base)', - name='ql', tex_name=r'q_{l}', - model='mats', src='ql', - unit='p.u.',) - - class ACOPFBase(DCPFlowBase): """ Base class for ACOPF model. @@ -37,8 +23,6 @@ class ACOPFBase(DCPFlowBase): def __init__(self, system, config): DCPFlowBase.__init__(self, system, config) - self.info = 'AC Optimal Power Flow' - self.type = 'ACED' # NOTE: ACOPF does not receive data from dynamic self.map1 = OrderedDict() self.map2 = OrderedDict([ @@ -102,15 +86,28 @@ def run(self, force_init=False, no_code=True, **kwargs, ) -class ACOPFModel(ACOPFBase): +class ACOPF(ACOPFBase): """ - ACOPF model. + Standard AC optimal power flow. + + Notes + ----- + 1. ACOPF is solved with PYPOWER ``runopf`` function. + 2. ACOPF formulation in AMS style is NOT DONE YET, + but this does not affect the results + because the data are passed to PYPOWER for solving. """ def __init__(self, system, config): ACOPFBase.__init__(self, system, config) self.info = 'AC Optimal Power Flow' self.type = 'ACED' + + # --- params --- + self.ql = RParam(info='reactive power demand (system base)', + name='ql', tex_name=r'q_{l}', + model='mats', src='ql', + unit='p.u.',) # --- bus --- self.aBus = Var(info='Bus voltage angle', unit='rad', @@ -141,20 +138,3 @@ def __init__(self, system, config): info='total cost', e_str='sum(c2 * pg**2 + c1 * pg + c0)', sense='min',) - - -class ACOPF(ACOPFData, ACOPFModel): - """ - Standard AC optimal power flow. - - Notes - ----- - 1. ACOPF is solved with PYPOWER ``runopf`` function. - 2. ACOPF formulation in AMS style is NOT DONE YET, - but this does not affect the results - because the data are passed to PYPOWER for solving. - """ - - def __init__(self, system=None, config=None): - ACOPFData.__init__(self) - ACOPFModel.__init__(self, system, config) diff --git a/ams/routines/cpf.py b/ams/routines/cpf.py index 851ab069..cfff40fe 100644 --- a/ams/routines/cpf.py +++ b/ams/routines/cpf.py @@ -1,33 +1,29 @@ """ Continuous power flow routine. """ -import logging # NOQA +import logging -from ams.pypower import runcpf # NOQA +from ams.pypower import runcpf -from ams.io.pypower import system2ppc # NOQA -from ams.pypower.core import ppoption # NOQA +from ams.io.pypower import system2ppc +from ams.pypower.core import ppoption -from ams.routines.pflow import PFlowData, PFlowModel # NOQA +from ams.routines.pflow import PFlow logger = logging.getLogger(__name__) -class CPFModel(PFlowModel): +class CPF(PFlow): """ - Model for continuous power flow. + Continuous power flow. + + Still under development, not ready for use. """ def __init__(self, system, config): - PFlowModel.__init__(self, system, config) + PFlow.__init__(self, system, config) self.info = 'AC continuous power flow' self.type = 'PF' - # TODO: delete vars, constraints, and objectives - # FIXME: how? - # for v, _ in self.vars.items(): - # delattr(self, v) - # for c, _ in self.constraints.items(): - # delattr(self, c) def solve(self, method=None, **kwargs): """ @@ -67,15 +63,3 @@ def run(self, force_init=False, no_code=True, super().run(force_init=force_init, no_code=no_code, method=method, **kwargs, ) - - -class CPF(PFlowData, CPFModel): - """ - Continuous power flow. - - Still under development, not ready for use. - """ - - def __init__(self, system=None, config=None): - PFlowData.__init__(self) - CPFModel.__init__(self, system, config) diff --git a/ams/routines/dcopf.py b/ams/routines/dcopf.py index d055e4f3..62144920 100644 --- a/ams/routines/dcopf.py +++ b/ams/routines/dcopf.py @@ -1,38 +1,43 @@ """ -OPF routines. +DCOPF routines. """ -import logging # NOQA +import logging -import numpy as np # NOQA -from ams.core.param import RParam # NOQA -from ams.core.service import NumOp # NOQA +import numpy as np +from ams.core.param import RParam +from ams.core.service import NumOp -from ams.routines.routine import RoutineData, RoutineModel # NOQA +from ams.routines.routine import RoutineModel -from ams.opt.omodel import Var, Constraint, Objective # NOQA +from ams.opt.omodel import Var, Constraint, Objective logger = logging.getLogger(__name__) -class DCOPFData(RoutineData): +class DCOPFBase(RoutineModel): """ - DCOPF data. + Base class for DCOPF dispatch model. + + Overload the ``solve``, ``unpack``, and ``run`` methods. """ - def __init__(self): - RoutineData.__init__(self) + def __init__(self, system, config): + RoutineModel.__init__(self, system, config) # --- generator cost --- self.ug = RParam(info='Gen connection status', name='ug', tex_name=r'u_{g}', - model='StaticGen', src='u',) + model='StaticGen', src='u', + const=True) self.ctrl = RParam(info='Gen controllability', name='ctrl', tex_name=r'c_{trl}', - model='StaticGen', src='ctrl',) + model='StaticGen', src='ctrl', + no_parse=True) self.c2 = RParam(info='Gen cost coefficient 2', name='c2', tex_name=r'c_{2}', unit=r'$/(p.u.^2)', model='GCost', - indexer='gen', imodel='StaticGen',) + indexer='gen', imodel='StaticGen', + pos=True) self.c1 = RParam(info='Gen cost coefficient 1', name='c1', tex_name=r'c_{1}', unit=r'$/(p.u.)', model='GCost', @@ -40,23 +45,28 @@ def __init__(self): self.c0 = RParam(info='Gen cost coefficient 0', name='c0', tex_name=r'c_{0}', unit=r'$', model='GCost', - indexer='gen', imodel='StaticGen',) + indexer='gen', imodel='StaticGen', + const=True) # --- generator limit --- self.pmax = RParam(info='Gen maximum active power (system base)', name='pmax', tex_name=r'p_{max}', - unit='p.u.', model='StaticGen',) + unit='p.u.', model='StaticGen', + const=True,) self.pmin = RParam(info='Gen minimum active power (system base)', name='pmin', tex_name=r'p_{min}', - unit='p.u.', model='StaticGen',) + unit='p.u.', model='StaticGen', + const=True,) self.pg0 = RParam(info='Gen initial active power (system base)', name='p0', tex_name=r'p_{g,0}', unit='p.u.', model='StaticGen',) self.Cg = RParam(info='connection matrix for Gen and Bus', name='Cg', tex_name=r'C_{g}', - model='mats', src='Cg',) + model='mats', src='Cg', + const=True,) self.Cft = RParam(info='connection matrix for Line and Bus', name='Cft', tex_name=r'C_{ft}', - model='mats', src='Cft',) + model='mats', src='Cft', + const=True,) # --- load --- self.pl = RParam(info='nodal active load (system base)', name='pl', tex_name=r'p_{l}', @@ -65,21 +75,11 @@ def __init__(self): # --- line --- self.rate_a = RParam(info='long-term flow limit', name='rate_a', tex_name=r'R_{ATEA}', - unit='MVA', model='Line',) + unit='MVA', model='Line') self.PTDF = RParam(info='Power transfer distribution factor matrix', name='PTDF', tex_name=r'P_{TDF}', - model='mats', src='PTDF',) - - -class DCOPFBase(RoutineModel): - """ - Base class for DCOPF dispatch model. - - Overload the ``solve``, ``unpack``, and ``run`` methods. - """ - - def __init__(self, system, config): - RoutineModel.__init__(self, system, config) + model='mats', src='PTDF', + const=True,) def solve(self, **kwargs): """ @@ -158,9 +158,12 @@ def unpack(self, **kwargs): return True -class DCOPFModel(DCOPFBase): +class DCOPF(DCOPFBase): """ - DCOPF model. + Standard DC optimal power flow (DCOPF). + + In this model, the bus injected power ``pn`` is used as internal variable + between generator output and load demand. """ def __init__(self, system, config): @@ -181,11 +184,10 @@ def __init__(self, system, config): name='plf', tex_name=r'p_{lf}', unit='p.u.', model='Line',) # --- constraints --- - self.CftT = NumOp(u=self.Cft, - fun=np.transpose, - name='CftT', - tex_name=r'C_{ft}^{T}', - info='transpose of connection matrix',) + self.CftT = NumOp(u=self.Cft, fun=np.transpose, + name='CftT', tex_name=r'C_{ft}^{T}', + info='transpose of connection matrix', + const=True,) self.pb = Constraint(name='pb', info='power balance', e_str='sum(pl) - sum(pg)', type='eq',) @@ -202,18 +204,7 @@ def __init__(self, system, config): # --- objective --- self.obj = Objective(name='tc', info='total cost', unit='$', - e_str='sum(c2 * pg**2 + c1 * pg + ug * c0)', sense='min',) - - -class DCOPF(DCOPFData, DCOPFModel): - """ - Standard DC optimal power flow (DCOPF). - - In this model, the bus injected power ``pn`` is used as internal variable - between generator output and load demand. - """ - - def __init__(self, system, config): - DCOPFData.__init__(self) - DCOPFModel.__init__(self, system, config) + self.obj.e_str = 'sum_squares(mul(c2, pg))' \ + '+ sum(mul(c1, pg))' \ + '+ sum(mul(ug, c0))' diff --git a/ams/routines/dopf.py b/ams/routines/dopf.py index 3fbd981f..db4633a0 100644 --- a/ams/routines/dopf.py +++ b/ams/routines/dopf.py @@ -1,33 +1,43 @@ """ Distributional optimal power flow (DOPF). """ -import numpy as np # NOQA +import numpy as np -from ams.core.param import RParam # NOQA -from ams.core.service import NumOp # NOQA +from ams.core.param import RParam +from ams.core.service import NumOp -from ams.routines.dcopf import DCOPFData, DCOPFModel # NOQA +from ams.routines.dcopf import DCOPF -from ams.opt.omodel import Var, Constraint, Objective # NOQA +from ams.opt.omodel import Var, Constraint, Objective -class DOPFData(DCOPFData): +class LDOPF(DCOPF): """ - Data for DOPF. + Linearzied distribution OPF, where power loss are ignored. + + Reference: + + [1] L. Bai, J. Wang, C. Wang, C. Chen, and F. Li, “Distribution Locational Marginal Pricing (DLMP) + for Congestion Management and Voltage Support,” IEEE Trans. Power Syst., vol. 33, no. 4, + pp. 4061–4073, Jul. 2018, doi: 10.1109/TPWRS.2017.2767632. """ - def __init__(self): - DCOPFData.__init__(self) + def __init__(self, system, config): + DCOPF.__init__(self, system, config) + self.info = 'Linearzied distribution OPF' + self.type = 'DED' + + # --- params --- self.ql = RParam(info='reactive power demand connected to Bus (system base)', name='ql', tex_name=r'q_{l}', unit='p.u.', model='mats', src='ql',) self.vmax = RParam(info="Bus voltage upper limit", name='vmax', tex_name=r'v_{max}', unit='p.u.', - model='Bus', src='vmax', + model='Bus', src='vmax', no_parse=True, ) self.vmin = RParam(info="Bus voltage lower limit", name='vmin', tex_name=r'v_{min}', unit='p.u.', - model='Bus', src='vmin', ) + model='Bus', src='vmin', no_parse=True,) self.r = RParam(info='line resistance', name='r', tex_name='r', unit='p.u.', model='Line', src='r') @@ -40,17 +50,6 @@ def __init__(self): self.qmin = RParam(info='generator minimum reactive power (system base)', name='qmin', tex_name=r'q_{min}', unit='p.u.', model='StaticGen', src='qmin',) - - -class LDOPFModel(DCOPFModel): - """ - Linearzied distribution OPF model. - """ - - def __init__(self, system, config): - DCOPFModel.__init__(self, system, config) - self.info = 'Linearzied distribution OPF' - self.type = 'DED' # --- vars --- self.qg = Var(info='Gen reactive power (system base)', name='qg', tex_name=r'q_{g}', unit='p.u.', @@ -77,11 +76,6 @@ def __init__(self, system, config): model='Line',) # --- constraints --- - self.CftT = NumOp(u=self.Cft, - fun=np.transpose, - name='CftT', - tex_name=r'C_{ft}^{T}', - info='transpose of connection matrix',) self.pinj.e_str = 'CftT@plf - pl - pn' self.qinj = Constraint(name='qinj', info='node reactive power injection', @@ -106,9 +100,12 @@ def unpack(self, **kwargs): self.system.Bus.set(src='v', attr='v', value=vBus, idx=self.vsq.get_idx()) -class LDOPF(DOPFData, LDOPFModel): +class LDOPF2(LDOPF): """ - Linearzied distribution OPF, where power loss are ignored. + Linearzied distribution OPF with variables for virtual inertia and damping from from REGCV1, + where power loss are ignored. + + ERROR: the formulation is problematic, check later. Reference: @@ -118,17 +115,9 @@ class LDOPF(DOPFData, LDOPFModel): """ def __init__(self, system, config): - DOPFData.__init__(self) - LDOPFModel.__init__(self, system, config) - + LDOPF.__init__(self, system, config) -class DOPF2Data(DOPFData): - """ - Data for DOPF with PFRCost for virtual inertia and damping from REGCV1. - """ - - def __init__(self): - DOPFData.__init__(self) + # --- params --- self.cm = RParam(info='Virtual inertia cost', name='cm', src='cm', tex_name=r'c_{m}', unit=r'$/s', @@ -139,15 +128,6 @@ def __init__(self): tex_name=r'c_{d}', unit=r'$/(p.u.)', model='REGCV1Cost', indexer='reg', imodel='REGCV1',) - - -class LDOPF2Model(LDOPFModel): - """ - Linearzied distribution OPF model with VSG. - """ - - def __init__(self, system, config): - LDOPFModel.__init__(self, system, config) # --- vars --- self.M = Var(info='Emulated startup time constant (M=2H) from REGCV1', name='M', tex_name=r'M', unit='s', @@ -159,20 +139,3 @@ def __init__(self, system, config): info='total cost', unit='$', e_str='sum(c2 * pg**2 + c1 * pg + ug * c0 + cm * M + cd * D)', sense='min',) - - -class LDOPF2(DOPF2Data, LDOPF2Model): - """ - Linearzied distribution OPF with variables for virtual inertia and damping from from REGCV1, - where power loss are ignored. - - Reference: - - [1] L. Bai, J. Wang, C. Wang, C. Chen, and F. Li, “Distribution Locational Marginal Pricing (DLMP) - for Congestion Management and Voltage Support,” IEEE Trans. Power Syst., vol. 33, no. 4, - pp. 4061–4073, Jul. 2018, doi: 10.1109/TPWRS.2017.2767632. - """ - - def __init__(self, system, config): - DOPF2Data.__init__(self) - LDOPF2Model.__init__(self, system, config) diff --git a/ams/routines/ed.py b/ams/routines/ed.py index df5ce12f..da267dbc 100644 --- a/ams/routines/ed.py +++ b/ams/routines/ed.py @@ -1,5 +1,5 @@ """ -Real-time economic dispatch. +Economic dispatch routines. """ import logging from collections import OrderedDict @@ -9,21 +9,53 @@ from ams.core.service import (ZonalSum, NumOpDual, NumHstack, RampSub, NumOp, LoadScale) -from ams.routines.rted import RTEDData, ESD1Base -from ams.routines.dcopf import DCOPFModel +from ams.routines.rted import RTED, ESD1Base -from ams.opt.omodel import Var, Constraint # NOQA +from ams.opt.omodel import Constraint logger = logging.getLogger(__name__) -class EDData(RTEDData): +class ED(RTED): """ - Economic dispatch data. + 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 + + 2. 2D Vars ``rgu`` and ``rgd`` are introduced + + Notes + ----- + 1. Formulations has been adjusted with interval ``config.t`` + + 2. The tie-line flow is not implemented in this model. """ - def __init__(self): - RTEDData.__init__(self) + def __init__(self, system, config): + RTED.__init__(self, system, config) + + self.config.add(OrderedDict((('t', 1), + ))) + self.config.add_extra("_help", + t="time interval in hours", + ) + + self.info = 'Economic dispatch' + self.type = 'DCED' + + self.ug.expand_dims = 1 + self.c0.expand_dims = 1 + self.pmax.expand_dims = 1 + self.pmin.expand_dims = 1 + self.rate_a.expand_dims = 1 + self.dud.expand_dims = 1 + self.ddd.expand_dims = 1 + + # --- params --- # NOTE: Setting `ED.scale.owner` to `Horizon` will cause an error when calling `ED.scale.v`. # This is because `Horizon` is a group that only contains the model `TimeSlot`. # The `get` method of `Horizon` calls `andes.models.group.GroupBase.get` and results in an error. @@ -32,7 +64,8 @@ def __init__(self): src='sd', model='EDTSlot') self.timeslot = RParam(info='Time slot for multi-period ED', name='timeslot', tex_name=r't_{s,idx}', - src='idx', model='EDTSlot') + src='idx', model='EDTSlot', + no_parse=True) self.R30 = RParam(info='30-min ramp rate (system base)', name='R30', tex_name=r'R_{30}', src='R30', unit='p.u./min', @@ -48,28 +81,12 @@ def __init__(self): indexer='gen', imodel='StaticGen',) self.Cl = RParam(info='connection matrix for Load and Bus', name='Cl', tex_name=r'C_{l}', - model='mats', src='Cl',) + model='mats', src='Cl', + const=True,) self.zl = RParam(info='zone of load', name='zl', tex_name=r'z_{l}', - model='StaticLoad', src='zone',) - - -class EDModel(DCOPFModel): - """ - Economic dispatch model. - """ - - def __init__(self, system, config): - DCOPFModel.__init__(self, system, config) - - self.config.add(OrderedDict((('t', 1), - ))) - self.config.add_extra("_help", - t="time interval in hours", - ) - - self.info = 'Economic dispatch' - self.type = 'DCED' + model='StaticLoad', src='zone', + no_parse=True) # --- vars --- # NOTE: extend pg to 2D matrix, where row is gen and col is timeslot @@ -79,34 +96,27 @@ def __init__(self, system, config): self.pn.horizon = self.timeslot self.pn.info = '2D Bus power injection (system base)' + self.pru.horizon = self.timeslot + self.pru.info = '2D RegUp power (system base)' + + self.prd.horizon = self.timeslot + self.prd.info = '2D RegDn power (system base)' + # --- constraints --- # --- power balance --- - self.ds = ZonalSum(u=self.zb, zone='Region', - name='ds', tex_name=r'S_{d}', - info='Sum pl vector in shape of zone',) - self.pdz = NumOpDual(u=self.ds, u2=self.pl, - fun=np.multiply, - rfun=np.sum, rargs=dict(axis=1), - expand_dims=0, - name='pdz', tex_name=r'p_{d,z}', - unit='p.u.', info='zonal load') self.pds = NumOpDual(u=self.sd, u2=self.pdz, fun=np.multiply, rfun=np.transpose, name='pds', tex_name=r'p_{d,s,t}', unit='p.u.', info='Scaled total load as row vector') - self.gs = ZonalSum(u=self.zg, zone='Region', - name='gs', tex_name=r'S_{g}', - info='Sum Gen vars vector in shape of zone') # NOTE: Spg @ pg returns a row vector self.pb.e_str = '- gs @ pg + pds' # power balance # spinning reserve - self.Rpmax = NumHstack(u=self.pmax, ref=self.timeslot, - name='Rpmax', tex_name=r'p_{max, R}', - info='Repetated pmax',) - self.Rug = NumHstack(u=self.ug, ref=self.timeslot, - name='Rug', tex_name=r'u_{g, R}', - info='Repetated ug',) + self.tlv = NumOp(u=self.timeslot, fun=np.ones_like, + args=dict(dtype=float), + expand_dims=0, + info='time length vector', + no_parse=True) self.dsrpz = NumOpDual(u=self.pdz, u2=self.dsrp, fun=np.multiply, name='dsrpz', tex_name=r'd_{sr, p, z}', info='zonal spinning reserve requirement in percentage',) @@ -114,24 +124,23 @@ def __init__(self, system, config): rfun=np.transpose, name='dsr', tex_name=r'd_{sr}', info='zonal spinning reserve requirement',) + self.sr = Constraint(name='sr', info='spinning reserve', type='uq', - e_str='-gs@multiply(Rpmax - pg, Rug) + dsr') + e_str='-gs@mul(mul(pmax, tlv) - pg, mul(ug, tlv)) + dsr') # --- bus power injection --- self.Cli = NumOp(u=self.Cl, fun=np.linalg.pinv, name='Cli', tex_name=r'C_{l}^{-1}', - info='inverse of Cl',) + info='inverse of Cl', + const=True) self.Rpd = LoadScale(u=self.zl, sd=self.sd, Cl=self.Cl, name='Rpd', tex_name=r'p_{d,R}', info='Scaled nodal load',) self.pinj.e_str = 'Cg @ (pn - Rpd) - pg' # power injection # --- line limits --- - self.RRA = NumHstack(u=self.rate_a, ref=self.timeslot, - name='RRA', tex_name=r'R_{ATEA,R}', - info='Repeated rate_a',) - self.lub.e_str = 'PTDF @ (pn - Rpd) - RRA' - self.llb.e_str = '-PTDF @ (pn - Rpd) - RRA' + self.lub.e_str = 'PTDF @ (pn - Rpd) - mul(rate_a, tlv)' + self.llb.e_str = '-PTDF @ (pn - Rpd) - mul(rate_a, tlv)' # --- ramping --- self.Mr = RampSub(u=self.pg, name='Mr', tex_name=r'M_{r}', @@ -139,13 +148,16 @@ def __init__(self, system, config): self.RR30 = NumHstack(u=self.R30, ref=self.Mr, name='RR30', tex_name=r'R_{30,R}', info='Repeated ramp rate as 2D matrix, (ng, ng-1)',) - self.rgu = Constraint(name='rgu', info='Gen ramping up', - e_str='pg @ Mr - t dot RR30', - type='uq',) - self.rgd = Constraint(name='rgd', - info='Gen ramping down', - e_str='-pg @ Mr - t dot RR30', - type='uq',) + + self.rbu.e_str = 'gs @ mul(mul(ug, tlv), pru) - mul(dud, tlv)' + self.rbd.e_str = 'gs @ mul(mul(ug, tlv), prd) - mul(ddd, tlv)' + + self.rru.e_str = 'mul(mul(ug, tlv), pg + pru) - mul(pmax, tlv)' + self.rrd.e_str = 'mul(mul(ug, tlv), -pg + prd) - mul(pmin, tlv)' + + self.rgu.e_str = 'pg @ Mr - t dot RR30' + self.rgd.e_str = '-pg @ Mr - t dot RR30' + self.rgu0 = Constraint(name='rgu0', info='Initial gen ramping up', e_str='pg[:, 0] - pg0 - R30', @@ -156,10 +168,10 @@ def __init__(self, system, config): type='uq',) # --- objective --- - # NOTE: no need to fix objective function - gcost = 'sum(c2 @ (t dot pg)**2 + c1 @ (t dot pg) + ug * c0)' - rcost = ' + sum(csr * ug * (Rpmax - pg))' - self.obj.e_str = gcost + rcost + cost = 'sum(c2 @ (t dot pg)**2 + c1 @ (t dot pg))' + cost += '+ sum(mul(mul(ug, c0), tlv))' # constant cost + cost += ' + sum(csr @ (mul(mul(ug, pmax), tlv) - pg))' # spinning reserve cost + self.obj.e_str = cost def unpack(self, **kwargs): """ @@ -169,44 +181,20 @@ def unpack(self, **kwargs): return None -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 - - 2. 2D Vars ``rgu`` and ``rgd`` are introduced - - Notes - ----- - 1. Formulations has been adjusted with interval ``config.t`` - - 2. The tie-line flow is not implemented in this model. - """ - - def __init__(self, system, config): - EDData.__init__(self) - EDModel.__init__(self, system, config) - # TODO: add data check # if has model ``TimeSlot``, mandatory # if has model ``Region``, optional # if ``Region``, if ``Bus`` has param ``zone``, optional, if none, auto fill -class ED2(EDData, EDModel, ESD1Base): +class ED2(ED, ESD1Base): """ ED with energy storage :ref:`ESD1`. The bilinear term in the formulation is linearized with big-M method. """ def __init__(self, system, config): - EDData.__init__(self) - EDModel.__init__(self, system, config) + ED.__init__(self, system, config) ESD1Base.__init__(self) self.config.t = 1 # dispatch interval in hour @@ -224,16 +212,17 @@ def __init__(self, system, config): self.zde.horizon = self.timeslot self.Mre = RampSub(u=self.SOC, name='Mre', tex_name=r'M_{r,E}', - info='Subtraction matrix for SOC',) + info='Subtraction matrix for SOC', + no_parse=True) self.EnR = NumHstack(u=self.En, ref=self.Mre, name='EnR', tex_name=r'E_{n,R}', - info='Repeated En as 2D matrix, (ng, ng-1)',) + info='Repeated En as 2D matrix, (ng, ng-1)') self.EtaCR = NumHstack(u=self.EtaC, ref=self.Mre, name='EtaCR', tex_name=r'\eta_{c,R}', - info='Repeated Etac as 2D matrix, (ng, ng-1)',) + info='Repeated Etac as 2D matrix, (ng, ng-1)') self.REtaDR = NumHstack(u=self.REtaD, ref=self.Mre, name='REtaDR', tex_name=r'R_{\eta_d,R}', - info='Repeated REtaD as 2D matrix, (ng, ng-1)',) + info='Repeated REtaD as 2D matrix, (ng, ng-1)') SOCb = 'mul(EnR, SOC @ Mre) - t dot mul(EtaCR, zce[:, 1:])' SOCb += ' + t dot mul(REtaDR, zde[:, 1:])' self.SOCb.e_str = SOCb diff --git a/ams/routines/pflow.py b/ams/routines/pflow.py index 1cb66170..9e689987 100644 --- a/ams/routines/pflow.py +++ b/ams/routines/pflow.py @@ -9,19 +9,29 @@ from ams.pypower.core import ppoption # NOQA from ams.core.param import RParam # NOQA -from ams.routines.dcpf import DCPFlowData, DCPFlowBase # NOQA +from ams.routines.dcpf import DCPFlowBase # NOQA from ams.opt.omodel import Var, Constraint # NOQA logger = logging.getLogger(__name__) -class PFlowData(DCPFlowData): +class PFlow(DCPFlowBase): """ AC Power Flow routine. + + Notes + ----- + 1. AC pwoer flow is solved with PYPOWER ``runpf`` function. + 2. AC power flow formulation in AMS style is NOT DONE YET, + but this does not affect the results + because the data are passed to PYPOWER for solving. """ - def __init__(self): - DCPFlowData.__init__(self) + def __init__(self, system, config): + DCPFlowBase.__init__(self, system, config) + self.info = "AC Power Flow" + self.type = "PF" + self.qd = RParam( info="reactive power load in system base", name="qd", @@ -31,17 +41,6 @@ def __init__(self): model="PQ", ) - -class PFlowModel(DCPFlowBase): - """ - AC power flow model. - """ - - def __init__(self, system, config): - DCPFlowBase.__init__(self, system, config) - self.info = "AC Power Flow" - self.type = "PF" - # --- bus --- self.aBus = Var( info="bus voltage angle", @@ -133,20 +132,3 @@ def run(self, force_init=False, no_code=True, method="newton", **kwargs): method=method, **kwargs, ) - - -class PFlow(PFlowData, PFlowModel): - """ - AC Power Flow routine. - - Notes - ----- - 1. AC pwoer flow is solved with PYPOWER ``runpf`` function. - 2. AC power flow formulation in AMS style is NOT DONE YET, - but this does not affect the results - because the data are passed to PYPOWER for solving. - """ - - def __init__(self, system=None, config=None): - PFlowData.__init__(self) - PFlowModel.__init__(self, system, config) diff --git a/ams/routines/routine.py b/ams/routines/routine.py index 77a850e0..bb254362 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -32,10 +32,7 @@ class RoutineData: """ def __init__(self): - self.rparams = OrderedDict() # list out RParam in a routine - self.params = OrderedDict() # list out Params in a routine - # --- optimization modeling --- - self.om = OModel(routine=self) + pass class RoutineModel: @@ -56,8 +53,10 @@ def __init__(self, system=None, config=None): self.syms = SymProcessor(self) # symbolic processor self._syms = False # flag if symbols has been generated + self.rparams = OrderedDict() # list out RParam in a routine self.services = OrderedDict() # list out services in a routine + self.consts = OrderedDict() # list out Consts in a routine self.params = OrderedDict() # list out Params in a routine self.vars = OrderedDict() # list out Vars in a routine self.constrs = OrderedDict() @@ -269,6 +268,10 @@ def _data_check(self): if rparam.owner.n == 0: no_input.append(rname) owner_list.append(rparam.owner.class_name) + # TODO: add more data config check? + if rparam.config.pos: + if not np.all(rparam.v > 0): + logger.warning(f"RParam <{rname}> should have all positive values.") if len(no_input) > 0: msg = f"Following models are missing in input: {set(owner_list)}" logger.warning(msg) @@ -409,7 +412,7 @@ def _check_attribute(self, key, value): """ if key in self.__dict__: existing_keys = [] - for type in ["constrs", "vars", "rparams"]: + for type in ["constrs", "vars", "rparams", "services"]: if type in self.__dict__: existing_keys += list(self.__dict__[type].keys()) if key in existing_keys: @@ -435,8 +438,6 @@ def __setattr__(self, key, value): # NOTE: value.id is not in use yet if isinstance(value, Var): value.id = len(self.vars) - elif isinstance(value, RParam): - value.id = len(self.rparams) self._check_attribute(key, value) self._register_attribute(key, value) @@ -452,6 +453,13 @@ def _register_attribute(self, key, value): if isinstance(value, (Param, Var, Constraint, Objective)): value.om = self.om value.rtn = self + if isinstance(value, Param): + if value.config.const: + self.consts[key] = value + self.om.consts[key] = None # cp.Constant + else: + self.params[key] = value + self.om.params[key] = None # cp.Parameter if isinstance(value, Var): self.vars[key] = value self.om.vars[key] = None # cp.Variable @@ -460,12 +468,8 @@ def _register_attribute(self, key, value): self.om.constrs[key] = None # cp.Constraint elif isinstance(value, RParam): self.rparams[key] = value - self.params[key] = value - self.om.params[key] = None # cp.Parameter elif isinstance(value, RBaseService): self.services[key] = value - self.params[key] = value - self.om.params[key] = None # cp.Parameter def update_param(self, params=Optional[Union[Param, str, list]]): """ diff --git a/ams/routines/rted.py b/ams/routines/rted.py index 6b18fac4..d2c0e631 100644 --- a/ams/routines/rted.py +++ b/ams/routines/rted.py @@ -7,63 +7,142 @@ from ams.core.param import RParam # NOQA from ams.core.service import ZonalSum, VarSelect, NumOp, NumOpDual # NOQA -from ams.routines.dcopf import DCOPFData, DCOPFModel # NOQA +from ams.routines.dcopf import DCOPFBase, DCOPF # NOQA from ams.opt.omodel import Var, Constraint # NOQA logger = logging.getLogger(__name__) -class RTEDData(DCOPFData): +class RTEDBase(DCOPF): """ - Data for real-time economic dispatch. + Base class for real-time economic dispatch (RTED). + + Overload ``dc2ac``, ``run``. """ - def __init__(self): - DCOPFData.__init__(self) + def __init__(self, system, config): + DCOPF.__init__(self, system, config) - # 1. reserve - # 1.1. reserve cost - self.cru = RParam(info='RegUp reserve coefficient', - name='cru', tex_name=r'c_{r,u}', - model='SFRCost', src='cru', - unit=r'$/(p.u.)',) - self.crd = RParam(info='RegDown reserve coefficient', - name='crd', tex_name=r'c_{r,d}', - model='SFRCost', src='crd', - unit=r'$/(p.u.)',) - # 1.2. reserve requirement - self.du = RParam(info='RegUp reserve requirement in percentage', - name='du', tex_name=r'd_{u}', - model='SFR', src='du', - unit='%',) - self.dd = RParam(info='RegDown reserve requirement in percentage', - name='dd', tex_name=r'd_{d}', - model='SFR', src='dd', - unit='%',) - self.zb = RParam(info='Bus zone', - name='zb', tex_name='z_{one,bus}', - model='Bus', src='zone', ) - self.zg = RParam(info='generator zone data', - name='zg', tex_name='z_{one,g}', - model='StaticGen', src='zone',) - # 2. generator - self.R10 = RParam(info='10-min ramp rate (system base)', - name='R10', tex_name=r'R_{10}', - model='StaticGen', src='R10', - unit='p.u./h',) - self.gammape = RParam(info='Ratio of ESD1.pge w.r.t to that of static generator', - name='gammape', tex_name=r'\gamma_{p,e}', - model='ESD1', src='gammap',) + def dc2ac(self, **kwargs): + """ + Convert the RTED results with ACOPF. + + Overload ``dc2ac`` method. + """ + if self.exec_time == 0 or self.exit_code != 0: + logger.warning('RTED is not executed successfully, quit conversion.') + return False + # set pru and prd into pmin and pmax + pr_idx = self.pru.get_idx() + pmin0 = self.system.StaticGen.get(src='pmin', attr='v', idx=pr_idx) + pmax0 = self.system.StaticGen.get(src='pmax', attr='v', idx=pr_idx) + p00 = self.system.StaticGen.get(src='p0', attr='v', idx=pr_idx) + + # solve ACOPF + ACOPF = self.system.ACOPF + pmin = pmin0 + self.prd.v + pmax = pmax0 - self.pru.v + self.system.StaticGen.set(src='pmin', attr='v', idx=pr_idx, value=pmin) + self.system.StaticGen.set(src='pmax', attr='v', idx=pr_idx, value=pmax) + self.system.StaticGen.set(src='p0', attr='v', idx=pr_idx, value=self.pg.v) + ACOPF.run() + self.pg.v = ACOPF.pg.v + + # NOTE: mock results to fit interface with ANDES + self.vBus = ACOPF.vBus + + # reset pmin, pmax, p0 + self.system.StaticGen.set(src='pmin', attr='v', idx=pr_idx, value=pmin0) + self.system.StaticGen.set(src='pmax', attr='v', idx=pr_idx, value=pmax0) + self.system.StaticGen.set(src='p0', attr='v', idx=pr_idx, value=p00) + self.system.recent = self + self.is_ac = True + logger.warning('RTED is converted to AC.') + return True -class RTEDModel(DCOPFModel): + def run(self, no_code=True, **kwargs): + """ + Run the routine. + + Parameters + ---------- + no_code : bool, optional + If True, print the generated CVXPY code. Defaults to False. + + Other Parameters + ---------------- + solver: str, optional + The solver to use. For example, 'GUROBI', 'ECOS', 'SCS', or 'OSQP'. + verbose : bool, optional + Overrides the default of hiding solver output and prints logging + information describing CVXPY's compilation process. + gp : bool, optional + If True, parses the problem as a disciplined geometric program + instead of a disciplined convex program. + qcp : bool, optional + If True, parses the problem as a disciplined quasiconvex program + instead of a disciplined convex program. + requires_grad : bool, optional + Makes it possible to compute gradients of a solution with respect to Parameters + by calling problem.backward() after solving, or to compute perturbations to the variables + given perturbations to Parameters by calling problem.derivative(). + Gradients are only supported for DCP and DGP problems, not quasiconvex problems. + When computing gradients (i.e., when this argument is True), the problem must satisfy the DPP rules. + enforce_dpp : bool, optional + When True, a DPPError will be thrown when trying to solve a + non-DPP problem (instead of just a warning). + Only relevant for problems involving Parameters. Defaults to False. + ignore_dpp : bool, optional + When True, DPP problems will be treated as non-DPP, which may speed up compilation. Defaults to False. + method : function, optional + A custom solve method to use. + kwargs : keywords, optional + Additional solver specific arguments. See CVXPY documentation for details. + + Notes + ----- + 1. remove ``vBus`` if has been converted with ``dc2ac`` + """ + if self.is_ac: + delattr(self, 'vBus') + self.is_ac = False + return super().run(**kwargs) + + +class RTED(RTEDBase): """ - RTED model. + DC-based real-time economic dispatch (RTED). + RTED extends DCOPF with: + + 1. Param ``pg0``, which can be retrieved from dynamic simulation results. + + 2. RTED has mapping dicts to interface with ANDES. + + 3. RTED routine adds a function ``dc2ac`` to do the AC conversion using ACOPF + + 4. Vars for zonal SFR reserve: ``pru`` and ``prd``; + + 5. Param for linear cost of zonal SFR reserve ``cru`` and ``crd``; + + 6. Param for SFR requirement ``du`` and ``dd``; + + 7. Param for generator ramping: start point ``pg0`` and ramping limit ``R10``; + + The function ``dc2ac`` sets the ``vBus`` value from solved ACOPF. + Without this conversion, dynamic simulation might fail due to the gap between + DC-based dispatch results and AC-based dynamic initialization. + + Notes + ----- + 1. Formulations has been adjusted with interval ``config.t``, 5/60 [Hour] by default. + + 2. The tie-line flow has not been implemented in formulations. """ def __init__(self, system, config): - DCOPFModel.__init__(self, system, config) + RTEDBase.__init__(self, system, config) self.config.add(OrderedDict((('t', 5/60), ))) @@ -90,6 +169,43 @@ def __init__(self, system, config): self.info = 'Real-time economic dispatch' self.type = 'DCED' + # 1. reserve + # 1.1. reserve cost + self.cru = RParam(info='RegUp reserve coefficient', + name='cru', tex_name=r'c_{r,u}', + model='SFRCost', src='cru', + unit=r'$/(p.u.)',) + self.crd = RParam(info='RegDown reserve coefficient', + name='crd', tex_name=r'c_{r,d}', + model='SFRCost', src='crd', + unit=r'$/(p.u.)',) + # 1.2. reserve requirement + self.du = RParam(info='RegUp reserve requirement in percentage', + name='du', tex_name=r'd_{u}', + model='SFR', src='du', + unit='%', no_parse=True,) + self.dd = RParam(info='RegDown reserve requirement in percentage', + name='dd', tex_name=r'd_{d}', + model='SFR', src='dd', + unit='%', no_parse=True,) + self.zb = RParam(info='Bus zone', + name='zb', tex_name='z_{one,bus}', + model='Bus', src='zone', + no_parse=True) + self.zg = RParam(info='generator zone data', + name='zg', tex_name='z_{one,g}', + model='StaticGen', src='zone', + no_parse=True) + # 2. generator + self.R10 = RParam(info='10-min ramp rate (system base)', + name='R10', tex_name=r'R_{10}', + model='StaticGen', src='R10', + unit='p.u./h',) + self.gammape = RParam(info='Ratio of ESD1.pge w.r.t to that of static generator', + name='gammape', tex_name=r'\gamma_{p,e}', + model='ESD1', src='gammap', + no_parse=True,) + # --- service --- self.gs = ZonalSum(u=self.zg, zone='Region', name='gs', tex_name=r'S_{g}', @@ -105,13 +221,15 @@ def __init__(self, system, config): # --- constraints --- self.ds = ZonalSum(u=self.zb, zone='Region', name='ds', tex_name=r'S_{d}', - info='Sum pd vector in shape of zone',) + info='Sum pl vector in shape of zone', + no_parse=True,) self.pdz = NumOpDual(u=self.ds, u2=self.pl, fun=np.multiply, rfun=np.sum, rargs=dict(axis=1), expand_dims=0, name='pdz', tex_name=r'p_{d,z}', - unit='p.u.', info='zonal load') + unit='p.u.', info='zonal load', + no_parse=True,) self.dud = NumOpDual(u=self.pdz, u2=self.du, fun=np.multiply, rfun=np.reshape, rargs=dict(newshape=(-1,)), name='dud', tex_name=r'd_{u, d}', @@ -133,103 +251,20 @@ def __init__(self, system, config): info='RegDn reserve ramp', e_str='mul(ug, -pg + prd) - pmin',) self.rgu = Constraint(name='rgu', type='uq', - info='ramp up limit of generator output', + info='Gen ramping up', e_str='mul(ug, pg-pg0-R10)',) self.rgd = Constraint(name='rgd', type='uq', - info='ramp down limit of generator output', + info='Gen ramping down', e_str='mul(ug, -pg+pg0-R10)',) # --- objective --- self.obj.info = 'total generation and reserve cost' - # NOTE: the product of dt and pg is processed using ``dot``, because dt is a numnber - self.obj.e_str = 'sum(c2 @ (t dot pg)**2) ' + \ - '+ sum(c1 @ (t dot pg)) + ug * c0 ' + \ - '+ sum(cru * pru + crd * prd)' - - -class RTED(RTEDData, RTEDModel): - """ - DC-based real-time economic dispatch (RTED). - RTED extends DCOPF with: - - 1. Param ``pg0``, which can be retrieved from dynamic simulation results. - - 2. RTED has mapping dicts to interface with ANDES. - - 3. RTED routine adds a function ``dc2ac`` to do the AC conversion using ACOPF - - 4. Vars for zonal SFR reserve: ``pru`` and ``prd``; - - 5. Param for linear cost of zonal SFR reserve ``cru`` and ``crd``; - - 6. Param for SFR requirement ``du`` and ``dd``; - - 7. Param for generator ramping: start point ``pg0`` and ramping limit ``R10``; - - The function ``dc2ac`` sets the ``vBus`` value from solved ACOPF. - Without this conversion, dynamic simulation might fail due to the gap between - DC-based dispatch results and AC-based dynamic initialization. - - Notes - ----- - 1. Formulations has been adjusted with interval ``config.t``, 5/60 [Hour] by default. - - 2. The tie-line flow has not been implemented in formulations. - """ - - def __init__(self, system, config): - RTEDData.__init__(self) - RTEDModel.__init__(self, system, config) - - def dc2ac(self, **kwargs): - """ - Convert the RTED results with ACOPF. - - Overload ``dc2ac`` method. - """ - if self.exec_time == 0 or self.exit_code != 0: - logger.warning('RTED is not executed successfully, quit conversion.') - return False - # set pru and prd into pmin and pmax - pr_idx = self.pru.get_idx() - pmin0 = self.system.StaticGen.get(src='pmin', attr='v', idx=pr_idx) - pmax0 = self.system.StaticGen.get(src='pmax', attr='v', idx=pr_idx) - p00 = self.system.StaticGen.get(src='p0', attr='v', idx=pr_idx) - - # solve ACOPF - ACOPF = self.system.ACOPF - pmin = pmin0 + self.prd.v - pmax = pmax0 - self.pru.v - self.system.StaticGen.set(src='pmin', attr='v', idx=pr_idx, value=pmin) - self.system.StaticGen.set(src='pmax', attr='v', idx=pr_idx, value=pmax) - self.system.StaticGen.set(src='p0', attr='v', idx=pr_idx, value=self.pg.v) - ACOPF.run() - self.pg.v = ACOPF.pg.v - - # NOTE: mock results to fit interface with ANDES - self.vBus = ACOPF.vBus - - # reset pmin, pmax, p0 - self.system.StaticGen.set(src='pmin', attr='v', idx=pr_idx, value=pmin0) - self.system.StaticGen.set(src='pmax', attr='v', idx=pr_idx, value=pmax0) - self.system.StaticGen.set(src='p0', attr='v', idx=pr_idx, value=p00) - self.system.recent = self - - self.is_ac = True - logger.warning('RTED is converted to AC.') - return True - - def run(self, **kwargs): - """ - Overload ``run()`` method. - - Notes - ----- - 1. remove ``vBus`` if has been converted with ``dc2ac`` - """ - if self.is_ac: - delattr(self, 'vBus') - self.is_ac = False - return super().run(**kwargs) + # NOTE: the product of dt and pg is processed using ``dot``, + # because dt is a numnber + cost = 'sum_squares(mul(c2, pg))' + cost += '+ sum(c1 @ (t dot pg))' + cost += '+ ug * c0' # constant cost + cost += '+ sum(cru * pru + crd * prd)' # reserve cost + self.obj.e_str = cost class ESD1Base: @@ -242,7 +277,7 @@ def __init__(self): self.En = RParam(info='Rated energy capacity', name='En', src='En', tex_name='E_n', unit='MWh', - model='ESD1',) + model='ESD1', const=True,) self.SOCmin = RParam(info='Minimum required value for SOC in limiter', name='SOCmin', src='SOCmin', tex_name='SOC_{min}', unit='%', @@ -258,14 +293,15 @@ def __init__(self): self.EtaC = RParam(info='Efficiency during charging', name='EtaC', src='EtaC', tex_name=r'\eta_c', unit='%', - model='ESD1',) + model='ESD1', const=True,) self.EtaD = RParam(info='Efficiency during discharging', name='EtaD', src='EtaD', tex_name=r'\eta_d', unit='%', - model='ESD1',) + model='ESD1', no_parse=True,) self.gene = RParam(info='gen of ESD1', name='gene', tex_name=r'g_{E}', - model='ESD1', src='gen',) + model='ESD1', src='gen', + no_parse=True,) # --- service --- self.REtaD = NumOp(name='REtaD', tex_name=r'\frac{1}{\eta_d}', @@ -285,7 +321,7 @@ def __init__(self): self.ce = VarSelect(u=self.pg, indexer='gene', name='ce', tex_name=r'C_{E}', info='Select zue from pg', - gamma='gammape',) + gamma='gammape', const=True,) self.pce = Var(info='ESD1 charging power (system base)', unit='p.u.', name='pce', tex_name=r'p_{c,E}', model='ESD1', nonneg=True,) @@ -334,15 +370,14 @@ def __init__(self): e_str=SOCb,) -class RTED2(RTEDData, RTEDModel, ESD1Base): +class RTED2(RTED, ESD1Base): """ RTED with energy storage :ref:`ESD1`. The bilinear term in the formulation is linearized with big-M method. """ def __init__(self, system, config): - RTEDData.__init__(self) - RTEDModel.__init__(self, system, config) + RTED.__init__(self, system, config) ESD1Base.__init__(self) self.info = 'Real-time economic dispatch with energy storage' self.type = 'DCED' diff --git a/ams/routines/uc.py b/ams/routines/uc.py index b2fa1cea..a99ec81a 100644 --- a/ams/routines/uc.py +++ b/ams/routines/uc.py @@ -1,29 +1,65 @@ """ -Real-time economic dispatch. +Unit commitment routines. """ -import logging # NOQA -from collections import OrderedDict # NOQA -import numpy as np # NOQA -import pandas as pd # NOQA +import logging +from collections import OrderedDict +import numpy as np +import pandas as pd -from ams.core.param import RParam # NOQA -from ams.core.service import (NumOp, NumHstack, - NumOpDual, MinDur, ZonalSum) # NOQA -from ams.routines.ed import EDData, EDModel # NOQA -from ams.routines.rted import ESD1Base # NOQA +from ams.core.param import RParam +from ams.core.service import (NumOp, NumOpDual, MinDur) +from ams.routines.ed import ED +from ams.routines.rted import ESD1Base -from ams.opt.omodel import Var, Constraint # NOQA +from ams.opt.omodel import Var, Constraint logger = logging.getLogger(__name__) -class UCData(EDData): +class UC(ED): """ - UC data. + 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, + non-spinning reserve cost, and unserved energy penalty. + + Method ``_initial_guess`` is used to make initial guess for commitment decision if all + generators are online at initial. It is a simple heuristic method, which may not be optimal. + + Notes + ----- + 1. Formulations has been adjusted with interval ``config.t`` + + 3. The tie-line flow has not been implemented in formulations. + + References + ---------- + 1. Huang, Y., Pardalos, P. M., & Zheng, Q. P. (2017). Electrical power unit commitment: deterministic and + two-stage stochastic programming models and algorithms. Springer. + + 2. D. A. Tejada-Arango, S. Lumbreras, P. Sánchez-Martín and A. Ramos, "Which Unit-Commitment Formulation + is Best? A Comparison Framework," in IEEE Transactions on Power Systems, vol. 35, no. 4, pp. 2926-2936, + July 2020, doi: 10.1109/TPWRS.2019.2962024. """ - def __init__(self): - EDData.__init__(self) + def __init__(self, system, config): + ED.__init__(self, system, config) + + 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' + self.timeslot.model = 'UCTSlot' self.csu = RParam(info='startup cost', name='csu', tex_name=r'c_{su}', @@ -48,7 +84,7 @@ def __init__(self): self.timeslot.info = 'Time slot for multi-period UC' self.timeslot.model = 'UCTSlot' - self.cnsr = RParam(info='cost for spinning reserve', + self.cnsr = RParam(info='cost for non-spinning reserve', name='cnsr', tex_name=r'c_{nsr}', model='NSRCost', src='cnsr', unit=r'$/(p.u.*h)', @@ -58,24 +94,6 @@ def __init__(self): model='NSR', src='demand', unit='%',) - -class UCModel(EDModel): - """ - UC model. - """ - - def __init__(self, system, config): - EDModel.__init__(self, system, config) - - 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' - # --- vars --- self.ugd = Var(info='commitment decision', horizon=self.timeslot, @@ -104,13 +122,13 @@ def __init__(self, system, config): e_str='ugd @ Mr - vgd[:, 1:]',) self.actv0 = Constraint(name='actv0', type='eq', info='initial startup action', - e_str='ugd[:, 0] - ug - vgd[:, 0]',) + e_str='ugd[:, 0] - ug[:, 0] - vgd[:, 0]',) self.actw = Constraint(name='actw', type='eq', info='shutdown action', e_str='-ugd @ Mr - wgd[:, 1:]',) self.actw0 = Constraint(name='actw0', type='eq', info='initial shutdown action', - e_str='-ugd[:, 0] + ug - wgd[:, 0]',) + e_str='-ugd[:, 0] + ug[:, 0] - wgd[:, 0]',) # --- constraints --- self.pb.e_str = '- gs @ zug + pds' # power balance @@ -130,7 +148,7 @@ def __init__(self, system, config): type='uq', e_str='zug - Mzug dot ugd') # --- reserve --- - # 1) non-spinning reserve + # supplement non-spinning reserve self.dnsrpz = NumOpDual(u=self.pdz, u2=self.dnsrp, fun=np.multiply, name='dnsrpz', tex_name=r'd_{nsr, p, z}', info='zonal non-spinning reserve requirement in percentage',) @@ -139,15 +157,7 @@ def __init__(self, system, config): name='dnsr', tex_name=r'd_{nsr}', info='zonal non-spinning reserve requirement',) self.nsr = Constraint(name='nsr', info='non-spinning reserve', type='uq', - e_str='-gs@(multiply((1 - ugd), Rpmax)) + dnsr') - # 2) spinning reserve - self.dsrpz = NumOpDual(u=self.pdz, u2=self.dsrp, fun=np.multiply, - name='dsrpz', tex_name=r'd_{sr, p, z}', - info='zonal spinning reserve requirement in percentage',) - self.dsr = NumOpDual(u=self.dsrpz, u2=self.sd, fun=np.multiply, - rfun=np.transpose, - name='dsr', tex_name=r'd_{sr}', - info='zonal spinning reserve requirement',) + e_str='-gs@(multiply((1 - ugd), mul(pmax, tlv))) + dnsr') # --- minimum ON/OFF duration --- self.Con = MinDur(u=self.pg, u2=self.td1, @@ -166,14 +176,16 @@ def __init__(self, system, config): # --- penalty for unserved load --- self.Cgi = NumOp(u=self.Cg, fun=np.linalg.pinv, name='Cgi', tex_name=r'C_{g}^{-1}', - info='inverse of Cg',) + info='inverse of Cg', + no_parse=True) # --- objective --- - gcost = 'sum(c2 @ (t dot zug)**2 + c1 @ (t dot zug) + c0 * ugd)' - acost = ' + sum(csu * vgd + csd * wgd)' - srcost = ' + sum(csr @ (multiply(Rpmax, ugd) - zug))' - nsrcost = ' + sum(cnsr @ multiply((1 - ugd), Rpmax))' - dcost = ' + sum(cul dot pos(gs @ pg - pds))' + gcost = 'sum(c2 @ (t dot zug)**2 + c1 @ (t dot zug))' + gcost += '+ sum(mul(mul(ug, c0), tlv))' + acost = ' + sum(csu * vgd + csd * wgd)' # action + srcost = ' + sum(csr @ (mul(mul(pmax, tlv), ugd) - zug))' # spinning reserve + nsrcost = ' + sum(cnsr @ mul((1 - ugd), mul(pmax, tlv)))' # non-spinning reserve + dcost = ' + sum(cul dot pos(gs @ pg - pds))' # unserved energy self.obj.e_str = gcost + acost + srcost + nsrcost + dcost def _initial_guess(self): @@ -220,56 +232,19 @@ def init(self, **kwargs): return super().init(**kwargs) -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, - non-spinning reserve cost, and unserved energy penalty. - - Method ``_initial_guess`` is used to make initial guess for commitment decision if all - generators are online at initial. It is a simple heuristic method, which may not be optimal. - - Notes - ----- - 1. Formulations has been adjusted with interval ``config.t`` - - 3. The tie-line flow has not been implemented in formulations. - - References - ---------- - 1. Huang, Y., Pardalos, P. M., & Zheng, Q. P. (2017). Electrical power unit commitment: deterministic and - two-stage stochastic programming models and algorithms. Springer. - - 2. D. A. Tejada-Arango, S. Lumbreras, P. Sánchez-Martín and A. Ramos, "Which Unit-Commitment Formulation - is Best? A Comparison Framework," in IEEE Transactions on Power Systems, vol. 35, no. 4, pp. 2926-2936, - July 2020, doi: 10.1109/TPWRS.2019.2962024. - """ - - def __init__(self, system, config): - UCData.__init__(self) - UCModel.__init__(self, system, config) - - -class UC2(UCData, UCModel, ESD1Base): +class UC2(UC, ESD1Base): """ UC with energy storage :ref:`ESD1`. """ def __init__(self, system, config): - UCData.__init__(self) - UCModel.__init__(self, system, config) + UC.__init__(self, system, config) ESD1Base.__init__(self) self.info = 'unit commitment with energy storage' self.type = 'DCUC' + # TODO: finish UC with energy storage formulation # self.SOC.horizon = self.timeslot # self.pge.horizon = self.timeslot # self.ude.horizon = self.timeslot From 990a139c85bd566f5d2f5e1b556c3a6c05ad374d Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 20 Nov 2023 20:31:18 -0500 Subject: [PATCH 49/77] Refactor routines to include parameter in OModel --- ams/routines/dcpf.py | 72 +++++++++++++++++--------------------------- 1 file changed, 27 insertions(+), 45 deletions(-) diff --git a/ams/routines/dcpf.py b/ams/routines/dcpf.py index 2088b6e6..cd20ec27 100644 --- a/ams/routines/dcpf.py +++ b/ams/routines/dcpf.py @@ -1,30 +1,35 @@ """ Power flow routines. """ -import logging # NOQA +import logging -from andes.shared import deg2rad # NOQA -from andes.utils.misc import elapsed # NOQA +from andes.shared import deg2rad +from andes.utils.misc import elapsed -from ams.routines.routine import RoutineData, RoutineModel # NOQA -from ams.opt.omodel import Var # NOQA -from ams.pypower import runpf # NOQA -from ams.pypower.core import ppoption # NOQA +from ams.routines.routine import RoutineModel +from ams.opt.omodel import Var +from ams.pypower import runpf +from ams.pypower.core import ppoption -from ams.io.pypower import system2ppc # NOQA -from ams.core.param import RParam # NOQA +from ams.io.pypower import system2ppc +from ams.core.param import RParam logger = logging.getLogger(__name__) -class DCPFlowData(RoutineData): +class DCPFlowBase(RoutineModel): """ - Data class for power flow routines. + Base class for power flow. + + Overload the ``solve``, ``unpack``, and ``run`` methods. """ - def __init__(self): - RoutineData.__init__(self) - # --- line --- + def __init__(self, system, config): + RoutineModel.__init__(self, system, config) + self.info = 'DC Power Flow' + self.type = 'PF' + + # --- routine data --- self.x = RParam(info="line reactance", name='x', tex_name='x', unit='p.u.', @@ -44,19 +49,6 @@ def __init__(self): unit='p.u.', model='mats', src='pl') - -class DCPFlowBase(RoutineModel): - """ - Base class for power flow. - - Overload the ``solve``, ``unpack``, and ``run`` methods. - """ - - def __init__(self, system, config): - RoutineModel.__init__(self, system, config) - self.info = 'DC Power Flow' - self.type = 'PF' - def unpack(self, res): """ Unpack results from PYPOWER. @@ -174,9 +166,15 @@ def disable(self, name): raise NotImplementedError -class DCPFlowModel(DCPFlowBase): +class DCPF(DCPFlowBase): """ - Base class for power flow model. + DC power flow. + + Notes + ----- + 1. DCPF is solved with PYPOWER ``runpf`` function. + 2. DCPF formulation is not complete yet, but this does not affect the + results because the data are passed to PYPOWER for solving. """ def __init__(self, system, config): @@ -193,19 +191,3 @@ def __init__(self, system, config): unit='p.u.', name='pg', tex_name=r'p_{g}', model='StaticGen', src='p',) - - -class DCPF(DCPFlowData, DCPFlowModel): - """ - DC power flow. - - Notes - ----- - 1. DCPF is solved with PYPOWER ``runpf`` function. - 2. DCPF formulation is not complete yet, but this does not affect the - results because the data are passed to PYPOWER for solving. - """ - - def __init__(self, system=None, config=None): - DCPFlowData.__init__(self) - DCPFlowModel.__init__(self, system, config) From fa1dc91f3900636795335fe66a75c8262c9696f5 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 20 Nov 2023 20:43:15 -0500 Subject: [PATCH 50/77] Change const as no_parse --- ams/routines/dcopf.py | 16 ++++++++-------- ams/routines/ed.py | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/ams/routines/dcopf.py b/ams/routines/dcopf.py index 62144920..2331a2df 100644 --- a/ams/routines/dcopf.py +++ b/ams/routines/dcopf.py @@ -28,7 +28,7 @@ def __init__(self, system, config): self.ug = RParam(info='Gen connection status', name='ug', tex_name=r'u_{g}', model='StaticGen', src='u', - const=True) + no_parse=True) self.ctrl = RParam(info='Gen controllability', name='ctrl', tex_name=r'c_{trl}', model='StaticGen', src='ctrl', @@ -46,27 +46,27 @@ def __init__(self, system, config): name='c0', tex_name=r'c_{0}', unit=r'$', model='GCost', indexer='gen', imodel='StaticGen', - const=True) + no_parse=True) # --- generator limit --- self.pmax = RParam(info='Gen maximum active power (system base)', name='pmax', tex_name=r'p_{max}', unit='p.u.', model='StaticGen', - const=True,) + no_parse=True,) self.pmin = RParam(info='Gen minimum active power (system base)', name='pmin', tex_name=r'p_{min}', unit='p.u.', model='StaticGen', - const=True,) + no_parse=True,) self.pg0 = RParam(info='Gen initial active power (system base)', name='p0', tex_name=r'p_{g,0}', unit='p.u.', model='StaticGen',) self.Cg = RParam(info='connection matrix for Gen and Bus', name='Cg', tex_name=r'C_{g}', model='mats', src='Cg', - const=True,) + no_parse=True,) self.Cft = RParam(info='connection matrix for Line and Bus', name='Cft', tex_name=r'C_{ft}', model='mats', src='Cft', - const=True,) + no_parse=True,) # --- load --- self.pl = RParam(info='nodal active load (system base)', name='pl', tex_name=r'p_{l}', @@ -79,7 +79,7 @@ def __init__(self, system, config): self.PTDF = RParam(info='Power transfer distribution factor matrix', name='PTDF', tex_name=r'P_{TDF}', model='mats', src='PTDF', - const=True,) + no_parse=True,) def solve(self, **kwargs): """ @@ -187,7 +187,7 @@ def __init__(self, system, config): self.CftT = NumOp(u=self.Cft, fun=np.transpose, name='CftT', tex_name=r'C_{ft}^{T}', info='transpose of connection matrix', - const=True,) + no_parse=True,) self.pb = Constraint(name='pb', info='power balance', e_str='sum(pl) - sum(pg)', type='eq',) diff --git a/ams/routines/ed.py b/ams/routines/ed.py index da267dbc..fdde635b 100644 --- a/ams/routines/ed.py +++ b/ams/routines/ed.py @@ -82,7 +82,7 @@ def __init__(self, system, config): self.Cl = RParam(info='connection matrix for Load and Bus', name='Cl', tex_name=r'C_{l}', model='mats', src='Cl', - const=True,) + no_parse=True,) self.zl = RParam(info='zone of load', name='zl', tex_name=r'z_{l}', model='StaticLoad', src='zone', @@ -132,7 +132,7 @@ def __init__(self, system, config): self.Cli = NumOp(u=self.Cl, fun=np.linalg.pinv, name='Cli', tex_name=r'C_{l}^{-1}', info='inverse of Cl', - const=True) + no_parse=True) self.Rpd = LoadScale(u=self.zl, sd=self.sd, Cl=self.Cl, name='Rpd', tex_name=r'p_{d,R}', info='Scaled nodal load',) From 5ee48316331f01502d6b6374421dabb7252a7977 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 20 Nov 2023 21:05:06 -0500 Subject: [PATCH 51/77] Deprecat Constant --- ams/core/param.py | 18 +++--------------- ams/core/service.py | 39 +++++++++++++-------------------------- ams/opt/omodel.py | 34 ++++++++++------------------------ ams/routines/routine.py | 9 ++------- ams/routines/rted.py | 6 +++--- 5 files changed, 31 insertions(+), 75 deletions(-) diff --git a/ams/core/param.py b/ams/core/param.py index 9519ce9a..52df6977 100644 --- a/ams/core/param.py +++ b/ams/core/param.py @@ -25,23 +25,14 @@ class RParam(Param): Class for parameters used in a routine. This class is developed to simplify the routine definition. - `RParm` is further used to define `Parameter` or `Constant` - in the optimization model. + `RParm` is further used to define `Parameter` in the optimization model. `no_parse` is used to skip parsing the `RParam` in optimization model. + It means that the `RParam` will not be added to the optimization model. This is useful when the RParam contains non-numeric values, or it is not necessary to be added to the optimization model. - `const` is used to define the parameter as a `Constant`, - otherwise it will be defined as a `Parameter`. - The key difference between `Parameter` and `Constant` in optimization - is that `Parameter` is mutable but `Constant` is not. - - Note that if `const=True`, following input parameters will - be ignored: `nonneg`, `nonpos`, `complex`, `imag`, `symmetric`, - `diag`, `hermitian`, `boolean`, `integer`, `pos`, `neg`, `sparsity`. - Parameters ---------- name : str, optional @@ -66,8 +57,6 @@ class RParam(Param): Name of the owner model or group of the indexer. no_parse: bool, optional True to skip parsing the parameter. - const: bool, optional - True to set the parameter as constant. nonneg: bool, optional True to set the parameter as non-negative. nonpos: bool, optional @@ -126,7 +115,6 @@ def __init__(self, imodel: Optional[str] = None, expand_dims: Optional[int] = None, no_parse: Optional[bool] = False, - const: Optional[bool] = False, nonneg: Optional[bool] = False, nonpos: Optional[bool] = False, complex: Optional[bool] = False, @@ -140,7 +128,7 @@ def __init__(self, neg: Optional[bool] = False, sparsity: Optional[list] = None, ): - Param.__init__(self, const=const, nonneg=nonneg, nonpos=nonpos, + Param.__init__(self, nonneg=nonneg, nonpos=nonpos, complex=complex, imag=imag, symmetric=symmetric, diag=diag, hermitian=hermitian, boolean=boolean, integer=integer, pos=pos, neg=neg, sparsity=sparsity) diff --git a/ams/core/service.py b/ams/core/service.py index 98c69c0c..cd94e29d 100644 --- a/ams/core/service.py +++ b/ams/core/service.py @@ -45,10 +45,9 @@ def __init__(self, info: str = None, vtype: Type = None, no_parse: bool = False, - const: bool = False, ): Param.__init__(self, name=name, unit=unit, info=info, - no_parse=no_parse, const=const) + no_parse=no_parse) BaseService.__init__(self, name=name, tex_name=tex_name, unit=unit, info=info, vtype=vtype) self.export = False @@ -127,11 +126,10 @@ def __init__(self, unit: str = None, info: str = None, vtype: Type = None, - const: bool = False, no_parse: bool = False, ): super().__init__(name=name, tex_name=tex_name, unit=unit, - info=info, vtype=vtype, const=const, + info=info, vtype=vtype, no_parse=no_parse) self._v = value @@ -172,10 +170,9 @@ def __init__(self, unit: str = None, info: str = None, vtype: Type = None, - const: bool = False, no_parse: bool = False,): super().__init__(name=name, tex_name=tex_name, unit=unit, - info=info, vtype=vtype, const=const, + info=info, vtype=vtype, no_parse=no_parse) self.u = u @@ -210,12 +207,11 @@ def __init__(self, tex_name: str = None, unit: str = None, info: str = None, - const: bool = False, no_parse: bool = False, ): tex_name = tex_name if tex_name is not None else u.tex_name super().__init__(name=name, tex_name=tex_name, unit=unit, - info=info, u=u, const=const, no_parse=no_parse) + info=info, u=u, no_parse=no_parse) self.sd = sd self.Cl = Cl @@ -278,12 +274,11 @@ def __init__(self, rargs: dict = {}, expand_dims: int = None, array_out=True, - const: bool = False, no_parse: bool = False,): tex_name = tex_name if tex_name is not None else u.tex_name super().__init__(name=name, tex_name=tex_name, unit=unit, info=info, vtype=vtype, u=u, - const=const, no_parse=no_parse) + no_parse=no_parse) self.fun = fun self.args = args self.rfun = rfun @@ -352,12 +347,11 @@ def __init__(self, info: str = None, vtype: Type = None, array_out: bool = True, - const: bool = False, no_parse: bool = False,): super().__init__(name=name, tex_name=tex_name, unit=unit, info=info, vtype=vtype, u=u, fun=np.expand_dims, args=args, - array_out=array_out, const=const, + array_out=array_out, no_parse=no_parse) self.axis = axis @@ -417,7 +411,6 @@ def __init__(self, rargs: dict = {}, expand_dims: int = None, array_out=True, - const: bool = False, no_parse: bool = False,): tex_name = tex_name if tex_name is not None else u.tex_name super().__init__(name=name, tex_name=tex_name, unit=unit, @@ -425,7 +418,7 @@ def __init__(self, u=u, fun=fun, args=args, rfun=rfun, rargs=rargs, expand_dims=expand_dims, - array_out=array_out, const=const, + array_out=array_out, no_parse=no_parse) self.u2 = u2 @@ -467,14 +460,13 @@ def __init__(self, unit: str = None, info: str = None, vtype: Type = None, - const: bool = False, no_parse: bool = False,): tex_name = tex_name if tex_name is not None else u.tex_name super().__init__(name=name, tex_name=tex_name, unit=unit, info=info, vtype=vtype, u=u, u2=u2, fun=None, args=None, rfun=None, rargs=None, - expand_dims=None, const=const, + expand_dims=None, no_parse=no_parse) if self.u.horizon is None: msg = f'{self.class_name} {self.name}.u {self.u.name} has no horizon, likely a modeling error.' @@ -537,12 +529,11 @@ def __init__(self, vtype: Type = None, rfun: Callable = None, rargs: dict = {}, - const: bool = False, no_parse: bool = False,): super().__init__(name=name, tex_name=tex_name, unit=unit, info=info, vtype=vtype, u=u, fun=np.hstack, args=args, - rfun=rfun, rargs=rargs, const=const, + rfun=rfun, rargs=rargs, no_parse=no_parse) self.ref = ref @@ -621,14 +612,13 @@ def __init__(self, vtype: Type = None, rfun: Callable = None, rargs: dict = {}, - const: bool = False, no_parse: bool = False, ): super().__init__(name=name, tex_name=tex_name, unit=unit, info=info, vtype=vtype, u=u, fun=None, args={}, rfun=rfun, rargs=rargs, - const=const, no_parse=no_parse) + no_parse=no_parse) self.zone = zone @property @@ -705,14 +695,13 @@ def __init__(self, rfun: Callable = None, rargs: dict = {}, array_out: bool = True, - const: bool = False, no_parse: bool = False, **kwargs ): super().__init__(name=name, tex_name=tex_name, unit=unit, info=info, vtype=vtype, u=u, fun=None, rfun=rfun, rargs=rargs, array_out=array_out, - const=const, no_parse=no_parse, + no_parse=no_parse, **kwargs) self.indexer = indexer self.gamma = gamma @@ -796,14 +785,13 @@ def __init__(self, vtype: Type = None, rfun: Callable = None, rargs: dict = {}, - const: bool = False, no_parse: bool = False, **kwargs ): super().__init__(name=name, tex_name=tex_name, unit=unit, info=info, vtype=vtype, u=u, fun=None, rfun=rfun, rargs=rargs, - const=const, no_parse=no_parse, + no_parse=no_parse, **kwargs) self.fun = fun @@ -852,13 +840,12 @@ def __init__(self, vtype: Type = None, rfun: Callable = None, rargs: dict = {}, - const: bool = False, no_parse: bool = False, ): super().__init__(name=name, tex_name=tex_name, unit=unit, info=info, vtype=vtype, u=u, fun=None, rfun=rfun, rargs=rargs, - const=const, no_parse=no_parse,) + no_parse=no_parse,) @property def v0(self): diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index 1a1bcffd..bf5ea3e7 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -100,8 +100,6 @@ class Param(OptzBase): Parameters ---------- - const: bool, optional - True to set the parameter as constant. no_parse: bool, optional True to skip parsing the parameter. nonneg: bool, optional @@ -135,7 +133,6 @@ def __init__(self, info: Optional[str] = None, unit: Optional[str] = None, no_parse: Optional[bool] = False, - const: Optional[bool] = False, nonneg: Optional[bool] = False, nonpos: Optional[bool] = False, complex: Optional[bool] = False, @@ -166,7 +163,6 @@ def __init__(self, ('integer', integer), ('pos', pos), ('neg', neg), - ('const', const), ))) def parse(self): @@ -175,25 +171,20 @@ def parse(self): """ config = self.config.as_dict() # NOQA sub_map = self.om.rtn.syms.sub_map - if self.config.const: - code_param = "tmp=const(value=self.v)" - else: - shape = np.shape(self.v) - config.pop('const', None) - code_param = f"tmp=param(shape={shape}, **config)" + shape = np.shape(self.v) + code_param = f"tmp=param(shape={shape}, **config)" for pattern, replacement, in sub_map.items(): code_param = re.sub(pattern, replacement, code_param) exec(code_param) exec("self.optz=tmp") - if not self.config.const: - try: - exec("self.optz.value = self.v") - except ValueError: - msg = f"Parameter <{self.name}> has non-numeric value, " - msg += "no_parse=True is applied." - logger.warning(msg) - self.no_parse = True - return False + try: + exec("self.optz.value = self.v") + except ValueError: + msg = f"Parameter <{self.name}> has non-numeric value, " + msg += "no_parse=True is applied." + logger.warning(msg) + self.no_parse = True + return False return True def update(self): @@ -662,7 +653,6 @@ class OModel: def __init__(self, routine): self.rtn = routine self.mdl = None - self.consts = OrderedDict() self.params = OrderedDict() self.vars = OrderedDict() self.constrs = OrderedDict() @@ -692,10 +682,6 @@ def setup(self, no_code=True, force_generate=False): rtn = self.rtn rtn.syms.generate_symbols(force_generate=force_generate) # --- add RParams and Services as parameters --- - # logger.debug(f"params: {rtn.params.keys()}") - for key, val in rtn.consts.items(): - val.parse() - setattr(self, key, val.optz) for key, val in rtn.params.items(): # logger.debug(f"Parsing param {key}") if not val.no_parse: diff --git a/ams/routines/routine.py b/ams/routines/routine.py index bb254362..08032fcd 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -56,7 +56,6 @@ def __init__(self, system=None, config=None): self.rparams = OrderedDict() # list out RParam in a routine self.services = OrderedDict() # list out services in a routine - self.consts = OrderedDict() # list out Consts in a routine self.params = OrderedDict() # list out Params in a routine self.vars = OrderedDict() # list out Vars in a routine self.constrs = OrderedDict() @@ -454,12 +453,8 @@ def _register_attribute(self, key, value): value.om = self.om value.rtn = self if isinstance(value, Param): - if value.config.const: - self.consts[key] = value - self.om.consts[key] = None # cp.Constant - else: - self.params[key] = value - self.om.params[key] = None # cp.Parameter + self.params[key] = value + self.om.params[key] = None # cp.Parameter if isinstance(value, Var): self.vars[key] = value self.om.vars[key] = None # cp.Variable diff --git a/ams/routines/rted.py b/ams/routines/rted.py index d2c0e631..ca73b906 100644 --- a/ams/routines/rted.py +++ b/ams/routines/rted.py @@ -277,7 +277,7 @@ def __init__(self): self.En = RParam(info='Rated energy capacity', name='En', src='En', tex_name='E_n', unit='MWh', - model='ESD1', const=True,) + model='ESD1', no_parse=True,) self.SOCmin = RParam(info='Minimum required value for SOC in limiter', name='SOCmin', src='SOCmin', tex_name='SOC_{min}', unit='%', @@ -293,7 +293,7 @@ def __init__(self): self.EtaC = RParam(info='Efficiency during charging', name='EtaC', src='EtaC', tex_name=r'\eta_c', unit='%', - model='ESD1', const=True,) + model='ESD1', no_parse=True,) self.EtaD = RParam(info='Efficiency during discharging', name='EtaD', src='EtaD', tex_name=r'\eta_d', unit='%', @@ -321,7 +321,7 @@ def __init__(self): self.ce = VarSelect(u=self.pg, indexer='gene', name='ce', tex_name=r'C_{E}', info='Select zue from pg', - gamma='gammape', const=True,) + gamma='gammape', no_parse=True,) self.pce = Var(info='ESD1 charging power (system base)', unit='p.u.', name='pce', tex_name=r'p_{c,E}', model='ESD1', nonneg=True,) From 8d74295eb1e1cfd4c2032512f6431062d485a8fc Mon Sep 17 00:00:00 2001 From: jinningwang Date: Tue, 21 Nov 2023 09:28:20 -0500 Subject: [PATCH 52/77] Refactor parameter update --- ams/opt/omodel.py | 25 +++++++++---------- ams/routines/routine.py | 53 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 61 insertions(+), 17 deletions(-) diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index bf5ea3e7..d30e927c 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -189,9 +189,13 @@ def parse(self): def update(self): """ - Update the parameter value from RParam, MParam, or Service. + Update the Parameter value. """ + # NOTE: skip no_parse parameters + if self.optz is None: + return None self.optz.value = self.v + return True def __repr__(self): return f'{self.__class__.__name__}: {self.name}' @@ -745,20 +749,17 @@ def __setattr__(self, __name: str, __value: Any): self.__register_attribute(__name, __value) super().__setattr__(__name, __value) - def update_param(self, params=Optional[Union[Param, str, list]]): + def update(self, params): """ Update the Parameter values. + + Parameters + ---------- + params: list + List of parameters to be updated. """ - if params is None: - for _, val in self.params.items(): - val.update() - elif isinstance(params, Param): - params.update() - elif isinstance(params, str): - self.params[params].update() - elif isinstance(params, list): - for param in params: - param.update() + for param in params: + param.update() return True def __repr__(self) -> str: diff --git a/ams/routines/routine.py b/ams/routines/routine.py index 08032fcd..e94bc42c 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -289,7 +289,6 @@ def init(self, force=True, no_code=True, **kwargs): no_code: bool Whether to show generated code. """ - # TODO: add input check, e.g., if GCost exists if not force and self.initialized: logger.debug(f"{self.class_name} has already been initialized.") return True @@ -299,7 +298,7 @@ def init(self, force=True, no_code=True, **kwargs): msg = f"{self.class_name} data check failed, setup may run into error!" logger.warning(msg) self._constr_check() - # FIXME: build the system matrices every init might slow down the process + # FIXME: build the system matrices every init might slow down self.system.mats.make() results, elapsed_time = self.om.setup(no_code=no_code) common_msg = f"Routine <{self.class_name}> " @@ -466,11 +465,55 @@ def _register_attribute(self, key, value): elif isinstance(value, RBaseService): self.services[key] = value - def update_param(self, params=Optional[Union[Param, str, list]]): + def update(self, params=None, mat_make=True,): """ - Update parameters in the optimization model. + Update the values of Parameters in the optimization model. + + This method is particularly important when some `RParams` are + linked with system matrices. + In such cases, setting `mat_make=True` is necessary to rebuild + these matrices for the changes to take effect. + This is common in scenarios involving topology changes, connection statuses, + or load value modifications. + If unsure, it is advisable to use `mat_make=True` as a precautionary measure. + + Parameters + ---------- + params: Parameter, str, or list + Parameter, Parameter name, or a list of parameter names to be updated. + If None, all parameters will be updated. + mat_make: bool + True to rebuild the system matrices. Set to False to speed up the process + if no system matrices are changed. """ - return self.om.update_param(params=params) + t0, _ = elapsed() + re_setup = False + # sanitize input + sparams = [] + if params is None: + sparams = [val for val in self.params.values()] + mat_make = True + elif isinstance(params, Param): + sparams = [params] + elif isinstance(params, str): + sparams = [self.params[params]] + elif isinstance(params, list): + sparams = [self.params[param] for param in params if isinstance(param, str)] + for param in params: + param.update() + for param in sparams: + if param.optz is None: # means no_parse=True + re_setup = True + break + if mat_make: + self.system.mats.make() + if re_setup: + logger.warning(f"Resetup {self.class_name} OModel due to non-parametric change.") + _, _ = self.om.setup(no_code=True) + results = self.om.update(params=sparams) + t0, s0 = elapsed(t0) + logger.debug(f"Update params in {s0}.") + return results def __delattr__(self, name): """ From 191793bfeaf9b6654bcb9d7efb3076f4ec078d8d Mon Sep 17 00:00:00 2001 From: jinningwang Date: Tue, 21 Nov 2023 15:17:02 -0500 Subject: [PATCH 53/77] Deprecate lb and ub in Var --- ams/opt/omodel.py | 30 ------------------------------ ams/routines/dcopf.py | 26 ++++++++++++++++++-------- ams/routines/dopf.py | 10 ++++++++-- ams/routines/routine.py | 38 ++++++++++---------------------------- ams/routines/rted.py | 9 +++++++-- 5 files changed, 43 insertions(+), 70 deletions(-) diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index d30e927c..4ca7ea03 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -226,12 +226,6 @@ class Var(OptzBase): Source variable name. If is None, the value of `name` will be used. model : str, optional Name of the owner model or group. - lb : str, optional - Lower bound - ub : str, optional - Upper bound - ctrl : str, optional - Controllability horizon : ams.routines.RParam, optional Horizon idx. nonneg : bool, optional @@ -279,9 +273,6 @@ def __init__(self, unit: Optional[str] = None, model: Optional[str] = None, shape: Optional[Union[tuple, int]] = None, - lb=None, - ub=None, - ctrl: Optional[str] = None, v0: Optional[str] = None, horizon=None, nonneg: Optional[bool] = False, @@ -310,9 +301,6 @@ def __init__(self, self.is_group = False self.model = model # indicate if this variable is a group variable self.owner = None # instance of the owner model or group - self.lb = lb - self.ub = ub - self.ctrl = ctrl self.v0 = v0 self.horizon = horizon self._shape = shape @@ -408,24 +396,6 @@ def parse(self): code_var = re.sub(pattern, replacement, code_var) exec(code_var) # build the Var object exec("self.optz = tmp") # assign the to the optz attribute - u_ctrl = self.ctrl.v if self.ctrl else np.ones(nr) - v0 = self.v0.v if self.v0 else np.zeros(nr) - if self.lb: - lv = self.lb.owner.get(src=self.lb.name, idx=self.get_idx(), attr='v') - u = self.lb.owner.get(src='u', idx=self.get_idx(), attr='v') - # element-wise lower bound considering online status - elv = u_ctrl * u * lv + (1 - u_ctrl) * v0 - # fit variable shape if horizon exists - elv = np.tile(elv, (nc, 1)).T if nc > 0 else elv - exec("self.optz_lb = tmp >= elv") - if self.ub: - uv = self.ub.owner.get(src=self.ub.name, idx=self.get_idx(), attr='v') - u = self.lb.owner.get(src='u', idx=self.get_idx(), attr='v') - # element-wise upper bound considering online status - euv = u_ctrl * u * uv + (1 - u_ctrl) * v0 - # fit variable shape if horizon exists - euv = np.tile(euv, (nc, 1)).T if nc > 0 else euv - exec("self.optz_ub = tmp <= euv") return True def __repr__(self): diff --git a/ams/routines/dcopf.py b/ams/routines/dcopf.py index 2331a2df..8302ef5b 100644 --- a/ams/routines/dcopf.py +++ b/ams/routines/dcopf.py @@ -51,11 +51,11 @@ def __init__(self, system, config): self.pmax = RParam(info='Gen maximum active power (system base)', name='pmax', tex_name=r'p_{max}', unit='p.u.', model='StaticGen', - no_parse=True,) + no_parse=False,) self.pmin = RParam(info='Gen minimum active power (system base)', name='pmin', tex_name=r'p_{min}', unit='p.u.', model='StaticGen', - no_parse=True,) + no_parse=False,) self.pg0 = RParam(info='Gen initial active power (system base)', name='p0', tex_name=r'p_{g,0}', unit='p.u.', model='StaticGen',) @@ -79,7 +79,7 @@ def __init__(self, system, config): self.PTDF = RParam(info='Power transfer distribution factor matrix', name='PTDF', tex_name=r'P_{TDF}', model='mats', src='PTDF', - no_parse=True,) + no_parse=False,) def solve(self, **kwargs): """ @@ -172,11 +172,10 @@ def __init__(self, system, config): self.type = 'DCED' # --- vars --- self.pg = Var(info='Gen active power (system base)', - unit='p.u.', name='pg', src='p', - tex_name=r'p_{g}', - model='StaticGen', - lb=self.pmin, ub=self.pmax, - ctrl=self.ctrl, v0=self.pg0) + unit='p.u.', + name='pg', tex_name=r'p_{g}', + model='StaticGen', src='p', + v0=self.pg0) self.pn = Var(info='Bus active power injection (system base)', unit='p.u.', name='pn', tex_name=r'p_{n}', model='Bus',) @@ -184,6 +183,17 @@ def __init__(self, system, config): name='plf', tex_name=r'p_{lf}', unit='p.u.', model='Line',) # --- constraints --- + # NOTE: `ug*pmin` results in unexpected error + self.nctrl = NumOp(u=self.ctrl, fun=np.logical_not, + name='nctrl', tex_name=r'-c_{trl}', + info='gen uncontrollability', + no_parse=True,) + pglb = '-pg + mul(nctrl, pg0) + mul(ctrl, mul(ug, pmin))' + self.pglb = Constraint(name='pglb', info='pg min', + e_str=pglb, type='uq',) + pgub = 'pg - mul(nctrl, pg0) - mul(ctrl, mul(ug, pmax))' + self.pgub = Constraint(name='pgub', info='pg max', + e_str=pgub, type='uq',) self.CftT = NumOp(u=self.Cft, fun=np.transpose, name='CftT', tex_name=r'C_{ft}^{T}', info='transpose of connection matrix', diff --git a/ams/routines/dopf.py b/ams/routines/dopf.py index db4633a0..cd2e2337 100644 --- a/ams/routines/dopf.py +++ b/ams/routines/dopf.py @@ -53,8 +53,14 @@ def __init__(self, system, config): # --- vars --- self.qg = Var(info='Gen reactive power (system base)', name='qg', tex_name=r'q_{g}', unit='p.u.', - model='StaticGen', src='q', - lb=self.qmin, ub=self.qmax,) + model='StaticGen', src='q',) + self.qglb = Constraint(name='qglb', type='uq', + info='Gen reactive power lower limit', + e_str='-qg + qmin',) + self.qgub = Constraint(name='qgub', type='uq', + info='Gen reactive power upper limit', + e_str='qg - qmax',) + # TODO: add qg limit, lb=self.qmin, ub=self.qmax, self.qn = Var(info='Bus reactive power', name='qn', tex_name=r'q_{n}', unit='p.u.', model='Bus',) diff --git a/ams/routines/routine.py b/ams/routines/routine.py index e94bc42c..744ec6ae 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -370,7 +370,7 @@ def run(self, force_init=False, no_code=True, **kwargs): self.unpack(**kwargs) return True else: - msg = f"{self.class_name} failed after " + msg = f"{self.class_name} failed as {status} after " msg += n_iter_str + f"using solver {sstats.solver_name}!" logger.warning(msg) return False @@ -757,8 +757,6 @@ def addVars(self, info: Optional[str] = None, src: Optional[str] = None, unit: Optional[str] = None, - lb: Optional[str] = None, - ub: Optional[str] = None, horizon: Optional[RParam] = None, nonneg: Optional[bool] = False, nonpos: Optional[bool] = False, @@ -831,31 +829,15 @@ def addVars(self, """ if model is None and shape is None: raise ValueError("Either model or shape must be specified.") - item = Var( - name=name, - tex_name=tex_name, - info=info, - src=src, - unit=unit, - model=model, - shape=shape, - lb=lb, - ub=ub, - horizon=horizon, - nonneg=nonneg, - nonpos=nonpos, - complex=complex, - imag=imag, - symmetric=symmetric, - diag=diag, - psd=psd, - nsd=nsd, - hermitian=hermitian, - bool=bool, - integer=integer, - pos=pos, - neg=neg, - ) + item = Var(name=name, tex_name=tex_name, + info=info, src=src, unit=unit, + model=model, shape=shape, horizon=horizon, + nonneg=nonneg, nonpos=nonpos, + complex=complex, imag=imag, + symmetric=symmetric, diag=diag, + psd=psd, nsd=nsd, hermitian=hermitian, + bool=bool, integer=integer, + pos=pos, neg=neg, ) # add the variable as an routine attribute setattr(self, name, item) diff --git a/ams/routines/rted.py b/ams/routines/rted.py index ca73b906..6442554c 100644 --- a/ams/routines/rted.py +++ b/ams/routines/rted.py @@ -316,8 +316,13 @@ def __init__(self): self.SOC = Var(info='ESD1 SOC', unit='%', name='SOC', tex_name=r'SOC', model='ESD1', pos=True, - v0=self.SOCinit, - lb=self.SOCmin, ub=self.SOCmax,) + v0=self.SOCinit,) + self.SOClb = Constraint(name='SOClb', type='uq', + info='SOC lower bound', + e_str='-SOC + SOCmin',) + self.SOCub = Constraint(name='SOCub', type='uq', + info='SOC upper bound', + e_str='SOC - SOCmax',) self.ce = VarSelect(u=self.pg, indexer='gene', name='ce', tex_name=r'C_{E}', info='Select zue from pg', From 2cd36a66cf0636305e24910e38eeb1e1fe4c0cf8 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Tue, 21 Nov 2023 21:46:17 -0500 Subject: [PATCH 54/77] Refactor DCOPF to include bus angle --- ams/core/matprocessor.py | 7 +++ ams/routines/dcopf.py | 121 +++++++++++++++++++++++++-------------- 2 files changed, 86 insertions(+), 42 deletions(-) diff --git a/ams/core/matprocessor.py b/ams/core/matprocessor.py index ff9163f4..a94b7fa5 100644 --- a/ams/core/matprocessor.py +++ b/ams/core/matprocessor.py @@ -117,6 +117,9 @@ def __init__(self, system): self.Cg = MParam(name='Cg', tex_name=r'C_g', info='Generator connectivity matrix', v=None) + self.Cs = MParam(name='Cs', tex_name=r'C_s', + info='Slack connectivity matrix', + v=None) self.Cl = MParam(name='Cl', tex_name=r'Cl', info='Load connectivity matrix', v=None) @@ -137,6 +140,8 @@ def make(self): # FIXME: hard coded here gen_bus = system.StaticGen.get(src='bus', attr='v', idx=system.StaticGen.get_idx()) + slack_bus = system.Slack.get(src='bus', attr='v', + idx=system.Slack.idx.v) all_bus = system.Bus.idx.v load_bus = system.StaticLoad.get(src='bus', attr='v', idx=system.StaticLoad.get_idx()) @@ -145,6 +150,8 @@ def make(self): self.pl._v = c_sparse(system.PQ.get(src='p0', attr='v', idx=idx_PD)) self.ql._v = np.array(system.PQ.get(src='q0', attr='v', idx=idx_PD)) + row, col = np.meshgrid(all_bus, slack_bus) + self.Cs._v = c_sparse((row == col).astype(int)) row, col = np.meshgrid(all_bus, gen_bus) self.Cg._v = c_sparse((row == col).astype(int)) row, col = np.meshgrid(all_bus, load_bus) diff --git a/ams/routines/dcopf.py b/ams/routines/dcopf.py index 8302ef5b..334d4bbe 100644 --- a/ams/routines/dcopf.py +++ b/ams/routines/dcopf.py @@ -5,7 +5,7 @@ import numpy as np from ams.core.param import RParam -from ams.core.service import NumOp +from ams.core.service import NumOp, VarSelect, NumOpDual from ams.routines.routine import RoutineModel @@ -47,39 +47,68 @@ def __init__(self, system, config): unit=r'$', model='GCost', indexer='gen', imodel='StaticGen', no_parse=True) - # --- generator limit --- - self.pmax = RParam(info='Gen maximum active power (system base)', + # --- generator --- + self.pmax = RParam(info='Gen maximum active power', name='pmax', tex_name=r'p_{max}', unit='p.u.', model='StaticGen', no_parse=False,) - self.pmin = RParam(info='Gen minimum active power (system base)', + self.pmin = RParam(info='Gen minimum active power', name='pmin', tex_name=r'p_{min}', unit='p.u.', model='StaticGen', no_parse=False,) - self.pg0 = RParam(info='Gen initial active power (system base)', + self.pg0 = RParam(info='Gen initial active power', name='p0', tex_name=r'p_{g,0}', unit='p.u.', model='StaticGen',) - self.Cg = RParam(info='connection matrix for Gen and Bus', - name='Cg', tex_name=r'C_{g}', - model='mats', src='Cg', - no_parse=True,) - self.Cft = RParam(info='connection matrix for Line and Bus', - name='Cft', tex_name=r'C_{ft}', - model='mats', src='Cft', - no_parse=True,) + self.idxg = RParam(info='Gen bus', + name='idxg', tex_name=r'idx_{g}', + model='StaticGen', src='bus', + no_parse=True,) + # --- load --- - self.pl = RParam(info='nodal active load (system base)', - name='pl', tex_name=r'p_{l}', - model='mats', src='pl', + self.pd = RParam(info='nodal active demand', + name='pd', tex_name=r'p_{d}', + model='StaticLoad', src='p0', unit='p.u.',) + # --- line --- + self.x = RParam(info='line reactance', + name='x', tex_name=r'x', + model='Line', src='x', + unit='p.u.', no_parse=True,) self.rate_a = RParam(info='long-term flow limit', name='rate_a', tex_name=r'R_{ATEA}', unit='MVA', model='Line') - self.PTDF = RParam(info='Power transfer distribution factor matrix', - name='PTDF', tex_name=r'P_{TDF}', + + # --- connection matrix --- + self.Cg = RParam(info='Gen connection matrix', + name='Cg', tex_name=r'C_{g}', + model='mats', src='Cg', + no_parse=True,) + self.Cs = RParam(info='Slack connection matrix', + name='Cs', tex_name=r'C_{s}', + model='mats', src='Cs', + no_parse=True,) + self.Cl = RParam(info='Load connection matrix', + name='Cl', tex_name=r'C_{l}', + model='mats', src='Cl', + no_parse=True,) + self.Cgi = NumOp(u=self.Cg, fun=np.linalg.pinv, + name='Cgi', tex_name=r'C_{g}^{-1}', + info='inverse of Cg', + no_parse=True,) + self.Cli = NumOp(u=self.Cl, fun=np.linalg.pinv, + name='Cli', tex_name=r'C_{l}^{-1}', + info='inverse of Cl', + no_parse=True,) + + self.Cft = RParam(info='Line connection matrix', + name='Cft', tex_name=r'C_{ft}', + model='mats', src='Cft', + no_parse=True,) + self.PTDF = RParam(info='Power Transfer Distribution Factor', + name='PTDF', tex_name=r'PTDF', model='mats', src='PTDF', - no_parse=False,) + no_parse=True,) def solve(self, **kwargs): """ @@ -171,18 +200,11 @@ def __init__(self, system, config): self.info = 'DC Optimal Power Flow' self.type = 'DCED' # --- vars --- - self.pg = Var(info='Gen active power (system base)', + self.pg = Var(info='Gen active power', unit='p.u.', name='pg', tex_name=r'p_{g}', model='StaticGen', src='p', v0=self.pg0) - self.pn = Var(info='Bus active power injection (system base)', - unit='p.u.', name='pn', tex_name=r'p_{n}', - model='Bus',) - self.plf = Var(info='line active power', - name='plf', tex_name=r'p_{lf}', unit='p.u.', - model='Line',) - # --- constraints --- # NOTE: `ug*pmin` results in unexpected error self.nctrl = NumOp(u=self.ctrl, fun=np.logical_not, name='nctrl', tex_name=r'-c_{trl}', @@ -194,23 +216,38 @@ def __init__(self, system, config): pgub = 'pg - mul(nctrl, pg0) - mul(ctrl, mul(ug, pmax))' self.pgub = Constraint(name='pgub', info='pg max', e_str=pgub, type='uq',) - self.CftT = NumOp(u=self.Cft, fun=np.transpose, - name='CftT', tex_name=r'C_{ft}^{T}', - info='transpose of connection matrix', - no_parse=True,) + + self.aBus = Var(info='Bus voltage angle', + name='aBus', tex_name=r'\theta_{n}', + unit='rad', model='Bus',) + self.pn = Var(info='Bus active power injection', + name='pn', tex_name=r'p_{n}', + unit='p.u.', model='Bus',) + + self.plf = Var(info='Line active power', + name='plf', tex_name=r'p_{lf}', + unit='p.u.', model='Line',) + self.plflb = Constraint(name='plflb', info='Line power lower bound', + e_str='-plf - rate_a', type='uq',) + self.plfub = Constraint(name='plfub', info='Line power upper bound', + e_str='plf - rate_a', type='uq',) + + # --- constraints --- self.pb = Constraint(name='pb', info='power balance', - e_str='sum(pl) - sum(pg)', + e_str='sum(pd) - sum(pg)', type='eq',) - self.pinj = Constraint(name='pinj', - info='nodal power injection', - e_str='CftT@plf - pl - pn', - type='eq',) - self.lub = Constraint(name='lub', info='Line limits upper bound', - e_str='PTDF @ (pn - pl) - rate_a', - type='uq',) - self.llb = Constraint(name='llb', info='Line limits lower bound', - e_str='- PTDF @ (pn - pl) - rate_a', - type='uq',) + self.aref = Constraint(name='aref', type='eq', + info='reference bus angle', + e_str='Cs@aBus',) + + self.pnb = Constraint(name='pnb', type='eq', + info='nodal power injection', + e_str='PTDF@(Cgi@pg - Cli@pd) - plf',) + + # self.pinj = Constraint(name='pinj', + # info='nodal power injection', + # e_str='CftT@plf - pl - pn', + # type='eq',) # --- objective --- self.obj = Objective(name='tc', info='total cost', unit='$', From 31c9592e327de7777d531e7986431d2e3ecd5421 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 22 Nov 2023 09:43:57 -0500 Subject: [PATCH 55/77] Clean up DCOPF --- ams/routines/dcopf.py | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/ams/routines/dcopf.py b/ams/routines/dcopf.py index 334d4bbe..d8fb0823 100644 --- a/ams/routines/dcopf.py +++ b/ams/routines/dcopf.py @@ -5,7 +5,7 @@ import numpy as np from ams.core.param import RParam -from ams.core.service import NumOp, VarSelect, NumOpDual +from ams.core.service import NumOp from ams.routines.routine import RoutineModel @@ -33,6 +33,10 @@ def __init__(self, system, config): name='ctrl', tex_name=r'c_{trl}', model='StaticGen', src='ctrl', no_parse=True) + self.nctrl = NumOp(u=self.ctrl, fun=np.logical_not, + name='nctrl', tex_name=r'-c_{trl}', + info='gen uncontrollability', + no_parse=True,) self.c2 = RParam(info='Gen cost coefficient 2', name='c2', tex_name=r'c_{2}', unit=r'$/(p.u.^2)', model='GCost', @@ -59,10 +63,6 @@ def __init__(self, system, config): self.pg0 = RParam(info='Gen initial active power', name='p0', tex_name=r'p_{g,0}', unit='p.u.', model='StaticGen',) - self.idxg = RParam(info='Gen bus', - name='idxg', tex_name=r'idx_{g}', - model='StaticGen', src='bus', - no_parse=True,) # --- load --- self.pd = RParam(info='nodal active demand', @@ -92,6 +92,15 @@ def __init__(self, system, config): name='Cl', tex_name=r'C_{l}', model='mats', src='Cl', no_parse=True,) + self.Cft = RParam(info='Line connection matrix', + name='Cft', tex_name=r'C_{ft}', + model='mats', src='Cft', + no_parse=True,) + self.PTDF = RParam(info='Power Transfer Distribution Factor', + name='PTDF', tex_name=r'PTDF', + model='mats', src='PTDF', + no_parse=True,) + self.Cgi = NumOp(u=self.Cg, fun=np.linalg.pinv, name='Cgi', tex_name=r'C_{g}^{-1}', info='inverse of Cg', @@ -101,14 +110,6 @@ def __init__(self, system, config): info='inverse of Cl', no_parse=True,) - self.Cft = RParam(info='Line connection matrix', - name='Cft', tex_name=r'C_{ft}', - model='mats', src='Cft', - no_parse=True,) - self.PTDF = RParam(info='Power Transfer Distribution Factor', - name='PTDF', tex_name=r'PTDF', - model='mats', src='PTDF', - no_parse=True,) def solve(self, **kwargs): """ @@ -206,10 +207,6 @@ def __init__(self, system, config): model='StaticGen', src='p', v0=self.pg0) # NOTE: `ug*pmin` results in unexpected error - self.nctrl = NumOp(u=self.ctrl, fun=np.logical_not, - name='nctrl', tex_name=r'-c_{trl}', - info='gen uncontrollability', - no_parse=True,) pglb = '-pg + mul(nctrl, pg0) + mul(ctrl, mul(ug, pmin))' self.pglb = Constraint(name='pglb', info='pg min', e_str=pglb, type='uq',) @@ -244,10 +241,6 @@ def __init__(self, system, config): info='nodal power injection', e_str='PTDF@(Cgi@pg - Cli@pd) - plf',) - # self.pinj = Constraint(name='pinj', - # info='nodal power injection', - # e_str='CftT@plf - pl - pn', - # type='eq',) # --- objective --- self.obj = Objective(name='tc', info='total cost', unit='$', From b34c9fa2e983e3d442ab07b2f0a65dfbf36fcfe3 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 23 Nov 2023 15:10:06 -0500 Subject: [PATCH 56/77] Improve OModel error info --- ams/opt/omodel.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index 4ca7ea03..5f121837 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -657,17 +657,31 @@ def setup(self, no_code=True, force_generate=False): rtn.syms.generate_symbols(force_generate=force_generate) # --- add RParams and Services as parameters --- for key, val in rtn.params.items(): - # logger.debug(f"Parsing param {key}") if not val.no_parse: - val.parse() + try: + val.parse() + except ValueError as e: + msg = f"Failed to parse Param <{key}>. " + msg += f"Original error: {e}" + raise ValueError(msg) setattr(self, key, val.optz) # --- add decision variables --- for key, val in rtn.vars.items(): - val.parse() + try: + val.parse() + except ValueError as e: + msg = f"Failed to parse Var <{key}>. " + msg += f"Original error: {e}" + raise ValueError(msg) setattr(self, key, val.optz) # --- add constraints --- for key, val in rtn.constrs.items(): - val.parse(no_code=no_code) + try: + val.parse(no_code=no_code) + except ValueError as e: + msg = f"Failed to parse Constr <{key}>. " + msg += f"Original error: {e}" + raise ValueError(msg) setattr(self, key, val.optz) # --- parse objective functions --- if rtn.type == 'PF': From 69ac5959c43f3b9ce6305d98c78acab7b47774ca Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 23 Nov 2023 15:10:25 -0500 Subject: [PATCH 57/77] Refactor DCOPF --- ams/routines/dcopf.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/ams/routines/dcopf.py b/ams/routines/dcopf.py index d8fb0823..23af5509 100644 --- a/ams/routines/dcopf.py +++ b/ams/routines/dcopf.py @@ -217,9 +217,6 @@ def __init__(self, system, config): self.aBus = Var(info='Bus voltage angle', name='aBus', tex_name=r'\theta_{n}', unit='rad', model='Bus',) - self.pn = Var(info='Bus active power injection', - name='pn', tex_name=r'p_{n}', - unit='p.u.', model='Bus',) self.plf = Var(info='Line active power', name='plf', tex_name=r'p_{lf}', From d393007b06b0c05d6949b57cbc0f99c17faaf74f Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 23 Nov 2023 15:10:39 -0500 Subject: [PATCH 58/77] Refactor RTED --- ams/routines/rted.py | 310 ++++++++++++++++++++++--------------------- 1 file changed, 161 insertions(+), 149 deletions(-) diff --git a/ams/routines/rted.py b/ams/routines/rted.py index 6442554c..7a1c929c 100644 --- a/ams/routines/rted.py +++ b/ams/routines/rted.py @@ -23,95 +23,70 @@ class RTEDBase(DCOPF): def __init__(self, system, config): DCOPF.__init__(self, system, config) + # --- region --- + self.zg = RParam(info='Gen zone', + name='zg', tex_name='z_{one,g}', + model='StaticGen', src='zone', + no_parse=True) + self.zd = RParam(info='Load zone', + name='zd', tex_name='z_{one,d}', + model='StaticLoad', src='zone', + no_parse=True) + self.gs = ZonalSum(u=self.zg, zone='Region', + name='gs', tex_name=r'S_{g}', + info='Sum Gen vars vector in shape of zone') + self.ds = ZonalSum(u=self.zd, zone='Region', + name='ds', tex_name=r'S_{d}', + info='Sum pd vector in shape of zone', + no_parse=True,) + self.pdz = NumOpDual(u=self.ds, u2=self.pd, + fun=np.multiply, + rfun=np.sum, rargs=dict(axis=1), + expand_dims=0, + name='pdz', tex_name=r'p_{d,z}', + unit='p.u.', info='zonal total load', + no_parse=True,) - def dc2ac(self, **kwargs): - """ - Convert the RTED results with ACOPF. - - Overload ``dc2ac`` method. - """ - if self.exec_time == 0 or self.exit_code != 0: - logger.warning('RTED is not executed successfully, quit conversion.') - return False - # set pru and prd into pmin and pmax - pr_idx = self.pru.get_idx() - pmin0 = self.system.StaticGen.get(src='pmin', attr='v', idx=pr_idx) - pmax0 = self.system.StaticGen.get(src='pmax', attr='v', idx=pr_idx) - p00 = self.system.StaticGen.get(src='p0', attr='v', idx=pr_idx) - - # solve ACOPF - ACOPF = self.system.ACOPF - pmin = pmin0 + self.prd.v - pmax = pmax0 - self.pru.v - self.system.StaticGen.set(src='pmin', attr='v', idx=pr_idx, value=pmin) - self.system.StaticGen.set(src='pmax', attr='v', idx=pr_idx, value=pmax) - self.system.StaticGen.set(src='p0', attr='v', idx=pr_idx, value=self.pg.v) - ACOPF.run() - self.pg.v = ACOPF.pg.v - - # NOTE: mock results to fit interface with ANDES - self.vBus = ACOPF.vBus - - # reset pmin, pmax, p0 - self.system.StaticGen.set(src='pmin', attr='v', idx=pr_idx, value=pmin0) - self.system.StaticGen.set(src='pmax', attr='v', idx=pr_idx, value=pmax0) - self.system.StaticGen.set(src='p0', attr='v', idx=pr_idx, value=p00) - self.system.recent = self - - self.is_ac = True - logger.warning('RTED is converted to AC.') - return True + # --- generator --- + self.R10 = RParam(info='10-min ramp rate', + name='R10', tex_name=r'R_{10}', + model='StaticGen', src='R10', + unit='p.u./h',) + self.gammape = RParam(info='Ratio of ESD1.pge w.r.t to that of static generator', + name='gammape', tex_name=r'\gamma_{p,e}', + model='ESD1', src='gammap', + no_parse=True,) - def run(self, no_code=True, **kwargs): - """ - Run the routine. - Parameters - ---------- - no_code : bool, optional - If True, print the generated CVXPY code. Defaults to False. +class SFRBase: + """ + Base class for SFR used in DCED. + """ - Other Parameters - ---------------- - solver: str, optional - The solver to use. For example, 'GUROBI', 'ECOS', 'SCS', or 'OSQP'. - verbose : bool, optional - Overrides the default of hiding solver output and prints logging - information describing CVXPY's compilation process. - gp : bool, optional - If True, parses the problem as a disciplined geometric program - instead of a disciplined convex program. - qcp : bool, optional - If True, parses the problem as a disciplined quasiconvex program - instead of a disciplined convex program. - requires_grad : bool, optional - Makes it possible to compute gradients of a solution with respect to Parameters - by calling problem.backward() after solving, or to compute perturbations to the variables - given perturbations to Parameters by calling problem.derivative(). - Gradients are only supported for DCP and DGP problems, not quasiconvex problems. - When computing gradients (i.e., when this argument is True), the problem must satisfy the DPP rules. - enforce_dpp : bool, optional - When True, a DPPError will be thrown when trying to solve a - non-DPP problem (instead of just a warning). - Only relevant for problems involving Parameters. Defaults to False. - ignore_dpp : bool, optional - When True, DPP problems will be treated as non-DPP, which may speed up compilation. Defaults to False. - method : function, optional - A custom solve method to use. - kwargs : keywords, optional - Additional solver specific arguments. See CVXPY documentation for details. + def __init__(self): - Notes - ----- - 1. remove ``vBus`` if has been converted with ``dc2ac`` - """ - if self.is_ac: - delattr(self, 'vBus') - self.is_ac = False - return super().run(**kwargs) + # 1. reserve + # --- reserve cost --- + self.cru = RParam(info='RegUp reserve coefficient', + name='cru', tex_name=r'c_{r,u}', + model='SFRCost', src='cru', + unit=r'$/(p.u.)',) + self.crd = RParam(info='RegDown reserve coefficient', + name='crd', tex_name=r'c_{r,d}', + model='SFRCost', src='crd', + unit=r'$/(p.u.)',) + # --- reserve requirement --- + self.du = RParam(info='RegUp reserve requirement in percentage', + name='du', tex_name=r'd_{u}', + model='SFR', src='du', + unit='%', no_parse=True,) + self.dd = RParam(info='RegDown reserve requirement in percentage', + name='dd', tex_name=r'd_{d}', + model='SFR', src='dd', + unit='%', no_parse=True,) -class RTED(RTEDBase): +class RTED(RTEDBase, SFRBase): """ DC-based real-time economic dispatch (RTED). RTED extends DCOPF with: @@ -143,6 +118,7 @@ class RTED(RTEDBase): def __init__(self, system, config): RTEDBase.__init__(self, system, config) + SFRBase.__init__(self) self.config.add(OrderedDict((('t', 5/60), ))) @@ -169,67 +145,15 @@ def __init__(self, system, config): self.info = 'Real-time economic dispatch' self.type = 'DCED' - # 1. reserve - # 1.1. reserve cost - self.cru = RParam(info='RegUp reserve coefficient', - name='cru', tex_name=r'c_{r,u}', - model='SFRCost', src='cru', - unit=r'$/(p.u.)',) - self.crd = RParam(info='RegDown reserve coefficient', - name='crd', tex_name=r'c_{r,d}', - model='SFRCost', src='crd', - unit=r'$/(p.u.)',) - # 1.2. reserve requirement - self.du = RParam(info='RegUp reserve requirement in percentage', - name='du', tex_name=r'd_{u}', - model='SFR', src='du', - unit='%', no_parse=True,) - self.dd = RParam(info='RegDown reserve requirement in percentage', - name='dd', tex_name=r'd_{d}', - model='SFR', src='dd', - unit='%', no_parse=True,) - self.zb = RParam(info='Bus zone', - name='zb', tex_name='z_{one,bus}', - model='Bus', src='zone', - no_parse=True) - self.zg = RParam(info='generator zone data', - name='zg', tex_name='z_{one,g}', - model='StaticGen', src='zone', - no_parse=True) - # 2. generator - self.R10 = RParam(info='10-min ramp rate (system base)', - name='R10', tex_name=r'R_{10}', - model='StaticGen', src='R10', - unit='p.u./h',) - self.gammape = RParam(info='Ratio of ESD1.pge w.r.t to that of static generator', - name='gammape', tex_name=r'\gamma_{p,e}', - model='ESD1', src='gammap', - no_parse=True,) - - # --- service --- - self.gs = ZonalSum(u=self.zg, zone='Region', - name='gs', tex_name=r'S_{g}', - info='Sum Gen vars vector in shape of zone') - # --- vars --- - self.pru = Var(info='RegUp reserve (system base)', + self.pru = Var(info='RegUp reserve', unit='p.u.', name='pru', tex_name=r'p_{r,u}', model='StaticGen', nonneg=True,) - self.prd = Var(info='RegDn reserve (system base)', + self.prd = Var(info='RegDn reserve', unit='p.u.', name='prd', tex_name=r'p_{r,d}', model='StaticGen', nonneg=True,) + # --- constraints --- - self.ds = ZonalSum(u=self.zb, zone='Region', - name='ds', tex_name=r'S_{d}', - info='Sum pl vector in shape of zone', - no_parse=True,) - self.pdz = NumOpDual(u=self.ds, u2=self.pl, - fun=np.multiply, - rfun=np.sum, rargs=dict(axis=1), - expand_dims=0, - name='pdz', tex_name=r'p_{d,z}', - unit='p.u.', info='zonal load', - no_parse=True,) self.dud = NumOpDual(u=self.pdz, u2=self.du, fun=np.multiply, rfun=np.reshape, rargs=dict(newshape=(-1,)), name='dud', tex_name=r'd_{u, d}', @@ -245,11 +169,11 @@ def __init__(self, system, config): info='RegDn reserve balance', e_str='gs @ mul(ug, prd) - ddd',) self.rru = Constraint(name='rru', type='uq', - info='RegUp reserve ramp', - e_str='mul(ug, pg + pru) - pmax',) + info='RegUp reserve source', + e_str='mul(ug, pg + pru) - mul(ug, pmax)',) self.rrd = Constraint(name='rrd', type='uq', - info='RegDn reserve ramp', - e_str='mul(ug, -pg + prd) - pmin',) + info='RegDn reserve source', + e_str='mul(ug, -pg + prd) - mul(ug, pmin)',) self.rgu = Constraint(name='rgu', type='uq', info='Gen ramping up', e_str='mul(ug, pg-pg0-R10)',) @@ -266,6 +190,92 @@ def __init__(self, system, config): cost += '+ sum(cru * pru + crd * prd)' # reserve cost self.obj.e_str = cost + def dc2ac(self, **kwargs): + """ + Convert the RTED results with ACOPF. + + Overload ``dc2ac`` method. + """ + if self.exec_time == 0 or self.exit_code != 0: + logger.warning('RTED is not executed successfully, quit conversion.') + return False + # set pru and prd into pmin and pmax + pr_idx = self.pru.get_idx() + pmin0 = self.system.StaticGen.get(src='pmin', attr='v', idx=pr_idx) + pmax0 = self.system.StaticGen.get(src='pmax', attr='v', idx=pr_idx) + p00 = self.system.StaticGen.get(src='p0', attr='v', idx=pr_idx) + + # solve ACOPF + ACOPF = self.system.ACOPF + pmin = pmin0 + self.prd.v + pmax = pmax0 - self.pru.v + self.system.StaticGen.set(src='pmin', attr='v', idx=pr_idx, value=pmin) + self.system.StaticGen.set(src='pmax', attr='v', idx=pr_idx, value=pmax) + self.system.StaticGen.set(src='p0', attr='v', idx=pr_idx, value=self.pg.v) + ACOPF.run() + self.pg.v = ACOPF.pg.v + + # NOTE: mock results to fit interface with ANDES + self.vBus = ACOPF.vBus + + # reset pmin, pmax, p0 + self.system.StaticGen.set(src='pmin', attr='v', idx=pr_idx, value=pmin0) + self.system.StaticGen.set(src='pmax', attr='v', idx=pr_idx, value=pmax0) + self.system.StaticGen.set(src='p0', attr='v', idx=pr_idx, value=p00) + self.system.recent = self + + self.is_ac = True + logger.warning('RTED is converted to AC.') + return True + + def run(self, no_code=True, **kwargs): + """ + Run the routine. + + Parameters + ---------- + no_code : bool, optional + If True, print the generated CVXPY code. Defaults to False. + + Other Parameters + ---------------- + solver: str, optional + The solver to use. For example, 'GUROBI', 'ECOS', 'SCS', or 'OSQP'. + verbose : bool, optional + Overrides the default of hiding solver output and prints logging + information describing CVXPY's compilation process. + gp : bool, optional + If True, parses the problem as a disciplined geometric program + instead of a disciplined convex program. + qcp : bool, optional + If True, parses the problem as a disciplined quasiconvex program + instead of a disciplined convex program. + requires_grad : bool, optional + Makes it possible to compute gradients of a solution with respect to Parameters + by calling problem.backward() after solving, or to compute perturbations to the variables + given perturbations to Parameters by calling problem.derivative(). + Gradients are only supported for DCP and DGP problems, not quasiconvex problems. + When computing gradients (i.e., when this argument is True), the problem must satisfy the DPP rules. + enforce_dpp : bool, optional + When True, a DPPError will be thrown when trying to solve a + non-DPP problem (instead of just a warning). + Only relevant for problems involving Parameters. Defaults to False. + ignore_dpp : bool, optional + When True, DPP problems will be treated as non-DPP, which may speed up compilation. Defaults to False. + method : function, optional + A custom solve method to use. + kwargs : keywords, optional + Additional solver specific arguments. See CVXPY documentation for details. + + Notes + ----- + 1. remove ``vBus`` if has been converted with ``dc2ac`` + """ + if self.is_ac: + delattr(self, 'vBus') + self.is_ac = False + return super().run(**kwargs) + class ESD1Base: """ @@ -327,11 +337,13 @@ def __init__(self): name='ce', tex_name=r'C_{E}', info='Select zue from pg', gamma='gammape', no_parse=True,) - self.pce = Var(info='ESD1 charging power (system base)', - unit='p.u.', name='pce', tex_name=r'p_{c,E}', + self.pce = Var(info='ESD1 charging power', + unit='p.u.', name='pce', + tex_name=r'p_{c,E}', model='ESD1', nonneg=True,) - self.pde = Var(info='ESD1 discharging power (system base)', - unit='p.u.', name='pde', tex_name=r'p_{d,E}', + self.pde = Var(info='ESD1 discharging power', + unit='p.u.', name='pde', + tex_name=r'p_{d,E}', model='ESD1', nonneg=True,) self.uce = Var(info='ESD1 charging decision', name='uce', tex_name=r'u_{c,E}', @@ -339,12 +351,12 @@ def __init__(self): self.ude = Var(info='ESD1 discharging decision', name='ude', tex_name=r'u_{d,E}', model='ESD1', boolean=True,) - self.zce = Var(info='Aux var for charging, :math:`z_{c,e}=u_{c,E}p_{c,E}`', - name='zce', tex_name=r'z_{c,E}', + self.zce = Var(name='zce', tex_name=r'z_{c,E}', model='ESD1', nonneg=True,) - self.zde = Var(info='Aux var for discharging, :math:`z_{d,e}=u_{d,E}*p_{d,E}`', - name='zde', tex_name=r'z_{d,E}', + self.zce.info = 'Aux var for charging, :math:`z_{c,e}=u_{c,E}*p_{c,E}`' + self.zde = Var(name='zde', tex_name=r'z_{d,E}', model='ESD1', nonneg=True,) + self.zde.info = 'Aux var for discharging, :math:`z_{d,e}=u_{d,E}*p_{d,E}`' # --- constraints --- self.ceb = Constraint(name='ceb', type='eq', From 6c1d1429ecfb8dd109ff7ae6b875f43073c6b0f2 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 23 Nov 2023 15:10:57 -0500 Subject: [PATCH 59/77] Refactor ED --- ams/core/service.py | 19 ++-- ams/models/timeslot.py | 21 +++- ams/routines/ed.py | 225 +++++++++++++++++++++++++---------------- 3 files changed, 164 insertions(+), 101 deletions(-) diff --git a/ams/core/service.py b/ams/core/service.py index cd94e29d..9f448d65 100644 --- a/ams/core/service.py +++ b/ams/core/service.py @@ -187,8 +187,6 @@ class LoadScale(ROperationService): nodal load. sd : Callable zonal load factor. - Cl: Callable - Connection matrix for Load and Bus. name : str, optional Instance name. tex_name : str, optional @@ -202,7 +200,6 @@ class LoadScale(ROperationService): def __init__(self, u: Callable, sd: Callable, - Cl: Callable, name: str = None, tex_name: str = None, unit: str = None, @@ -213,17 +210,17 @@ def __init__(self, super().__init__(name=name, tex_name=tex_name, unit=unit, info=info, u=u, no_parse=no_parse) self.sd = sd - self.Cl = Cl @property def v(self): - Region = self.rtn.system.Region - yloc_zl = np.array(Region.idx2uid(self.u.v)) - PQ = self.rtn.system.PQ - p0 = PQ.get(src='p0', attr='v', idx=PQ.idx.v) - p0s = np.multiply(self.sd.v[:, yloc_zl].transpose(), - p0[:, np.newaxis]) - return np.matmul(np.linalg.pinv(self.Cl.v), p0s) + sys = self.rtn.system + u_idx = self.u.get_idx() + u_bus = self.u.owner.get(src='bus', attr='v', idx=u_idx) + u_zone = sys.Bus.get(src='zone', attr='v', idx=u_bus) + u_yloc = np.array(sys.Region.idx2uid(u_zone)) + p0s = np.multiply(self.sd.v[:, u_yloc].transpose(), + self.u.v[:, np.newaxis]) + return p0s class NumOp(ROperationService): diff --git a/ams/models/timeslot.py b/ams/models/timeslot.py index c57b5290..a304e4af 100644 --- a/ams/models/timeslot.py +++ b/ams/models/timeslot.py @@ -30,22 +30,39 @@ def __init__(self, system=None, config=None): tex_name=r's_{d}', iconvert=str_list_iconv, oconvert=str_list_oconv, - vtype=float, - ) + vtype=float) class EDTSlot(TimeSlot): """ Time slot model for ED. + + `sd` is the zonal load scaling factor. + Cells in `sd` should have `nz` values seperated by comma, + where `nz` is the number of `Region` in the system. + + `ug` is the unit commitment decisions. + Cells in `ug` should have `ng` values seperated by comma, + where `ng` is the number of `StaticGen` in the system. """ def __init__(self, system=None, config=None): TimeSlot.__init__(self, system, config) + self.ug = NumParam(info='unit commitment decisions', + tex_name=r'u_{g}', + iconvert=str_list_iconv, + oconvert=str_list_oconv, + vtype=int) + class UCTSlot(TimeSlot): """ Time slot model for UC. + + `sd` is the zonal load scaling factor. + Cells in `sd` should have `nz` values seperated by comma, + where `nz` is the number of `Region` in the system. """ def __init__(self, system=None, config=None): diff --git a/ams/routines/ed.py b/ams/routines/ed.py index fdde635b..2904d822 100644 --- a/ams/routines/ed.py +++ b/ams/routines/ed.py @@ -6,16 +6,90 @@ import numpy as np from ams.core.param import RParam -from ams.core.service import (ZonalSum, NumOpDual, NumHstack, +from ams.core.service import (NumOpDual, NumHstack, RampSub, NumOp, LoadScale) from ams.routines.rted import RTED, ESD1Base -from ams.opt.omodel import Constraint +from ams.opt.omodel import Var, Constraint logger = logging.getLogger(__name__) +class SRBase: + """ + Base class for spinning reserve. + """ + + def __init__(self) -> None: + self.dsrp = RParam(info='spinning reserve requirement in percentage', + name='dsr', tex_name=r'd_{sr}', + model='SR', src='demand', + unit='%',) + self.csr = RParam(info='cost for spinning reserve', + name='csr', tex_name=r'c_{sr}', + model='SRCost', src='csr', + unit=r'$/(p.u.*h)', + indexer='gen', imodel='StaticGen',) + + self.prs = Var(name='prs', tex_name=r'p_{r,s}', + info='spinning reserve', unit='p.u.', + model='StaticGen', nonneg=True,) + + self.dsrpz = NumOpDual(u=self.pdz, u2=self.dsrp, fun=np.multiply, + name='dsrpz', tex_name=r'd_{s,r, p, z}', + info='zonal spinning reserve requirement in percentage',) + self.dsr = NumOpDual(u=self.dsrpz, u2=self.sd, fun=np.multiply, + rfun=np.transpose, + name='dsr', tex_name=r'd_{s,r,z}', + info='zonal spinning reserve requirement',) + + +class MPBase: + """ + Base class for multi-period dispatch. + """ + + def __init__(self) -> None: + # NOTE: Setting `ED.scale.owner` to `Horizon` will cause an error when calling `ED.scale.v`. + # This is because `Horizon` is a group that only contains the model `TimeSlot`. + # The `get` method of `Horizon` calls `andes.models.group.GroupBase.get` and results in an error. + self.sd = RParam(info='zonal load factor for ED', + name='sd', tex_name=r's_{d}', + src='sd', model='EDTSlot') + + self.timeslot = RParam(info='Time slot for multi-period ED', + name='timeslot', tex_name=r't_{s,idx}', + src='idx', model='EDTSlot', + no_parse=True) + + self.pdsz = NumOpDual(u=self.sd, u2=self.pdz, + fun=np.multiply, rfun=np.transpose, + name='pds', tex_name=r'p_{d,s,z}', + unit='p.u.', + info='Scaled zonal total load') + + self.tlv = NumOp(u=self.timeslot, fun=np.ones_like, + args=dict(dtype=float), + expand_dims=0, + info='time length vector', + no_parse=True) + + self.pds = LoadScale(u=self.pd, sd=self.sd, + name='pds', tex_name=r'p_{d,s}', + info='Scaled load',) + + self.R30 = RParam(info='30-min ramp rate', + name='R30', tex_name=r'R_{30}', + src='R30', unit='p.u./min', + model='StaticGen') + self.Mr = RampSub(u=self.pg, name='Mr', tex_name=r'M_{r}', + info='Subtraction matrix for ramping',) + self.RR30 = NumHstack(u=self.R30, ref=self.Mr, + name='RR30', tex_name=r'R_{30,R}', + info='Repeated ramp rate',) + + class ED(RTED): """ DC-based multi-period economic dispatch (ED). @@ -24,9 +98,11 @@ class ED(RTED): ED extends DCOPF as follows: - 1. Var ``pg`` is extended to 2D + 1. Vars `pg`, `pru`, `prd` are extended to 2D + + 2. 2D Vars `rgu` and `rgd` are introduced - 2. 2D Vars ``rgu`` and ``rgd`` are introduced + 3. Param `ug` is sourced from `EDTSlot`.`ug` as commitment decisions Notes ----- @@ -37,6 +113,8 @@ class ED(RTED): def __init__(self, system, config): RTED.__init__(self, system, config) + MPBase.__init__(self) + SRBase.__init__(self) self.config.add(OrderedDict((('t', 1), ))) @@ -47,140 +125,111 @@ def __init__(self, system, config): self.info = 'Economic dispatch' self.type = 'DCED' - self.ug.expand_dims = 1 + self.ug.info = 'unit commitment decisions' + self.ug.model = 'EDTSlot' + self.ug.src = 'ug' + self.ug.tex_name = r'u_{g}^{T}', + + self.ctrl.expand_dims = 1 self.c0.expand_dims = 1 self.pmax.expand_dims = 1 self.pmin.expand_dims = 1 + self.pg0.expand_dims = 1 self.rate_a.expand_dims = 1 self.dud.expand_dims = 1 self.ddd.expand_dims = 1 # --- params --- - # NOTE: Setting `ED.scale.owner` to `Horizon` will cause an error when calling `ED.scale.v`. - # This is because `Horizon` is a group that only contains the model `TimeSlot`. - # The `get` method of `Horizon` calls `andes.models.group.GroupBase.get` and results in an error. - self.sd = RParam(info='zonal load factor for ED', - name='sd', tex_name=r's_{d}', - src='sd', model='EDTSlot') - self.timeslot = RParam(info='Time slot for multi-period ED', - name='timeslot', tex_name=r't_{s,idx}', - src='idx', model='EDTSlot', - no_parse=True) - self.R30 = RParam(info='30-min ramp rate (system base)', - name='R30', tex_name=r'R_{30}', - src='R30', unit='p.u./min', - model='StaticGen') - self.dsrp = RParam(info='spinning reserve requirement in percentage', - name='dsr', tex_name=r'd_{sr}', - model='SR', src='demand', - unit='%',) - self.csr = RParam(info='cost for spinning reserve', - name='csr', tex_name=r'c_{sr}', - model='SRCost', src='csr', - unit=r'$/(p.u.*h)', - indexer='gen', imodel='StaticGen',) - self.Cl = RParam(info='connection matrix for Load and Bus', - name='Cl', tex_name=r'C_{l}', - model='mats', src='Cl', - no_parse=True,) - self.zl = RParam(info='zone of load', - name='zl', tex_name=r'z_{l}', - model='StaticLoad', src='zone', + self.ugt = NumOp(u=self.ug, fun=np.transpose, + name='ugt', tex_name=r'u_{g}', + info='input ug transpose', no_parse=True) # --- vars --- # NOTE: extend pg to 2D matrix, where row is gen and col is timeslot self.pg.horizon = self.timeslot - self.pg.info = '2D power generation (system base)' + self.pg.info = '2D Gen power' + pglb = '-pg + mul(mul(nctrl, pg0), tlv) ' + pglb += '+ mul(mul(ctrl, pmin), tlv)' + self.pglb.e_str = pglb + pgub = 'pg - mul(mul(nctrl, pg0), tlv) ' + pgub += '- mul(mul(ctrl, pmax), tlv)' + self.pgub.e_str = pgub - self.pn.horizon = self.timeslot - self.pn.info = '2D Bus power injection (system base)' + self.plf.horizon = self.timeslot + self.plf.info = '2D Line flow' self.pru.horizon = self.timeslot - self.pru.info = '2D RegUp power (system base)' + self.pru.info = '2D RegUp power' self.prd.horizon = self.timeslot - self.prd.info = '2D RegDn power (system base)' + self.prd.info = '2D RegDn power' + + self.prs.horizon = self.timeslot # --- constraints --- - # --- power balance --- - self.pds = NumOpDual(u=self.sd, u2=self.pdz, - fun=np.multiply, rfun=np.transpose, - name='pds', tex_name=r'p_{d,s,t}', - unit='p.u.', info='Scaled total load as row vector') # NOTE: Spg @ pg returns a row vector - self.pb.e_str = '- gs @ pg + pds' # power balance - - # spinning reserve - self.tlv = NumOp(u=self.timeslot, fun=np.ones_like, - args=dict(dtype=float), - expand_dims=0, - info='time length vector', - no_parse=True) - self.dsrpz = NumOpDual(u=self.pdz, u2=self.dsrp, fun=np.multiply, - name='dsrpz', tex_name=r'd_{sr, p, z}', - info='zonal spinning reserve requirement in percentage',) - self.dsr = NumOpDual(u=self.dsrpz, u2=self.sd, fun=np.multiply, - rfun=np.transpose, - name='dsr', tex_name=r'd_{sr}', - info='zonal spinning reserve requirement',) + self.pb.e_str = '- gs @ pg + pdsz' # power balance - self.sr = Constraint(name='sr', info='spinning reserve', type='uq', - e_str='-gs@mul(mul(pmax, tlv) - pg, mul(ug, tlv)) + dsr') + self.prsb = Constraint(info='spinning reserve balance', + name='prs', type='eq', + e_str='mul(ugt, mul(pmax, tlv) - pg) - prs') + self.rb = Constraint(info='spinning reserve requirement', + name='rb', type='uq', + e_str='-gs@mul(prs, ugt) + dsr') # --- bus power injection --- - self.Cli = NumOp(u=self.Cl, fun=np.linalg.pinv, - name='Cli', tex_name=r'C_{l}^{-1}', - info='inverse of Cl', - no_parse=True) - self.Rpd = LoadScale(u=self.zl, sd=self.sd, Cl=self.Cl, - name='Rpd', tex_name=r'p_{d,R}', - info='Scaled nodal load',) - self.pinj.e_str = 'Cg @ (pn - Rpd) - pg' # power injection + self.pnb.e_str = 'PTDF@(Cgi@pg - Cli@pds) - plf' # --- line limits --- - self.lub.e_str = 'PTDF @ (pn - Rpd) - mul(rate_a, tlv)' - self.llb.e_str = '-PTDF @ (pn - Rpd) - mul(rate_a, tlv)' + self.plflb.e_str = '-plf - mul(rate_a, tlv)' + self.plfub.e_str = 'plf - mul(rate_a, tlv)' # --- ramping --- - self.Mr = RampSub(u=self.pg, name='Mr', tex_name=r'M_{r}', - info='Subtraction matrix for ramping, (ng, ng-1)',) - self.RR30 = NumHstack(u=self.R30, ref=self.Mr, - name='RR30', tex_name=r'R_{30,R}', - info='Repeated ramp rate as 2D matrix, (ng, ng-1)',) - self.rbu.e_str = 'gs @ mul(mul(ug, tlv), pru) - mul(dud, tlv)' - self.rbd.e_str = 'gs @ mul(mul(ug, tlv), prd) - mul(ddd, tlv)' + self.rbu.e_str = 'gs@mul(ugt, pru) - mul(dud, tlv)' + self.rbd.e_str = 'gs@mul(ugt, prd) - mul(ddd, tlv)' - self.rru.e_str = 'mul(mul(ug, tlv), pg + pru) - mul(pmax, tlv)' - self.rrd.e_str = 'mul(mul(ug, tlv), -pg + prd) - mul(pmin, tlv)' + self.rru.e_str = 'mul(ugt, pg + pru) - mul(pmax, tlv)' + self.rrd.e_str = 'mul(ugt, -pg + prd) - mul(pmin, tlv)' self.rgu.e_str = 'pg @ Mr - t dot RR30' self.rgd.e_str = '-pg @ Mr - t dot RR30' self.rgu0 = Constraint(name='rgu0', info='Initial gen ramping up', - e_str='pg[:, 0] - pg0 - R30', + e_str='pg[:, 0] - pg0[:, 0] - R30', type='uq',) self.rgd0 = Constraint(name='rgd0', info='Initial gen ramping down', - e_str='- pg[:, 0] + pg0 - R30', + e_str='- pg[:, 0] + pg0[:, 0] - R30', type='uq',) # --- objective --- cost = 'sum(c2 @ (t dot pg)**2 + c1 @ (t dot pg))' - cost += '+ sum(mul(mul(ug, c0), tlv))' # constant cost - cost += ' + sum(csr @ (mul(mul(ug, pmax), tlv) - pg))' # spinning reserve cost + # constant cost + cost += '+ sum(mul(ugt, mul(c0, tlv)))' + # spinning reserve cost + cost += ' + sum(csr@prs)' self.obj.e_str = cost + def dc2ac(self, **kwargs): + """ + AC conversion ``dc2ac`` is not implemented yet for + multi-period dispatch. + """ + return NotImplementedError + def unpack(self, **kwargs): """ - ED will not unpack results from solver into devices - because the resutls are multi-time-period. + Multi-period dispatch will not unpack results from + solver into devices. + + # TODO: unpack first period results, and allow input + # to specify which period to unpack. """ return None - # TODO: add data check # if has model ``TimeSlot``, mandatory # if has model ``Region``, optional From 6a78b81299cf345f533ba3b7076a5da58e8c9fcd Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 23 Nov 2023 22:35:05 -0500 Subject: [PATCH 60/77] Improve logger msg --- ams/core/service.py | 21 ++++----------------- ams/opt/omodel.py | 26 ++++++++++++++++++-------- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/ams/core/service.py b/ams/core/service.py index 9f448d65..1b052710 100644 --- a/ams/core/service.py +++ b/ams/core/service.py @@ -79,22 +79,8 @@ def class_name(self): return self.__class__.__name__ def __repr__(self): - val_str = '' - - v = self.v - - if v is None: - return f'{self.class_name}: {self.owner.class_name}.{self.name}' - elif isinstance(v, np.ndarray): - if v.shape[0] == 1: - if len(self.v) <= 20: - val_str = f', v={self.v}' - else: - val_str = f', v in shape of {self.v.shape}' - else: - val_str = f', v in shape of {self.v.shape}' - - return f'{self.class_name}: {self.rtn.class_name}.{self.name}{val_str}' + if self.name is None: + return f'{self.class_name}: {self.rtn.class_name}' else: return f'{self.class_name}: {self.rtn.class_name}.{self.name}' @@ -466,7 +452,8 @@ def __init__(self, expand_dims=None, no_parse=no_parse) if self.u.horizon is None: - msg = f'{self.class_name} {self.name}.u {self.u.name} has no horizon, likely a modeling error.' + msg = f'{self.class_name} <{self.name}>.u: <{self.u.name}> ' + msg += 'has no horizon, likely a modeling error.' logger.error(msg) @property diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index 5f121837..c8af43d5 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -593,7 +593,11 @@ def parse(self, no_code=True): def __repr__(self): name_str = f"{self.name}=" if self.name is not None else "obj=" - value_str = f"{self.v:.4f}, " if self.v is not None else "" + try: + v = self.v + except Exception: + v = None + value_str = f"{v:.4f}, " if v is not None else "" return f"{name_str}{value_str}{self.e_str}" @@ -660,35 +664,41 @@ def setup(self, no_code=True, force_generate=False): if not val.no_parse: try: val.parse() - except ValueError as e: + except Exception as e: msg = f"Failed to parse Param <{key}>. " msg += f"Original error: {e}" - raise ValueError(msg) + raise Exception(msg) setattr(self, key, val.optz) # --- add decision variables --- for key, val in rtn.vars.items(): try: val.parse() - except ValueError as e: + except Exception as e: msg = f"Failed to parse Var <{key}>. " msg += f"Original error: {e}" - raise ValueError(msg) + raise Exception(msg) setattr(self, key, val.optz) # --- add constraints --- for key, val in rtn.constrs.items(): try: val.parse(no_code=no_code) - except ValueError as e: + except Exception as e: msg = f"Failed to parse Constr <{key}>. " msg += f"Original error: {e}" - raise ValueError(msg) + raise Exception(msg) setattr(self, key, val.optz) # --- parse objective functions --- if rtn.type == 'PF': # NOTE: power flow type has no objective function pass elif rtn.obj is not None: - rtn.obj.parse(no_code=no_code) + try: + rtn.obj.parse(no_code=no_code) + except Exception as e: + msg = f"Failed to parse Obj <{rtn.obj.name}>. " + msg += f"Original error: {e}" + raise Exception(msg) + # --- finalize the optimziation formulation --- code_mdl = "problem(self.obj, [constr for constr in self.constrs.values()])" for pattern, replacement in self.rtn.syms.sub_map.items(): From 736d8ef4eddad10af8b673c4c82a3876add55f61 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 23 Nov 2023 22:35:23 -0500 Subject: [PATCH 61/77] Improve logger msg --- ams/routines/routine.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ams/routines/routine.py b/ams/routines/routine.py index 744ec6ae..f891b133 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -154,8 +154,10 @@ def get_load(self, horizon: Union[int, str], horizon_all = mdl.idx.v try: row = horizon_all.index(horizon) - except ValueError: - raise ValueError(f"<{model}> does not have horizon with idx=<{horizon}>.") + except ValueError as e: + msg = f"<{model}> does not have horizon with idx=<{horizon}>. " + msg += f"Original error: {e}" + raise ValueError(msg) pq_factor = np.array(sdv[:, col][row, :]) pqv = np.multiply(pq0, pq_factor) return pqv From 5465fc400080df6c1e93253e07439bde141ad745 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 23 Nov 2023 22:35:45 -0500 Subject: [PATCH 62/77] Refactor UC --- ams/routines/ed.py | 28 ++++----- ams/routines/uc.py | 138 +++++++++++++++++++++++++++++++++------------ 2 files changed, 117 insertions(+), 49 deletions(-) diff --git a/ams/routines/ed.py b/ams/routines/ed.py index 2904d822..253fdedb 100644 --- a/ams/routines/ed.py +++ b/ams/routines/ed.py @@ -44,6 +44,12 @@ def __init__(self) -> None: name='dsr', tex_name=r'd_{s,r,z}', info='zonal spinning reserve requirement',) + # NOTE: define e_str in dispatch model + self.prsb = Constraint(info='spinning reserve balance', + name='prsb', type='eq',) + self.rsr = Constraint(info='spinning reserve requirement', + name='rsr', type='uq',) + class MPBase: """ @@ -72,6 +78,7 @@ def __init__(self) -> None: self.tlv = NumOp(u=self.timeslot, fun=np.ones_like, args=dict(dtype=float), expand_dims=0, + name='tlv', tex_name=r'v_{tl}', info='time length vector', no_parse=True) @@ -89,6 +96,12 @@ def __init__(self) -> None: name='RR30', tex_name=r'R_{30,R}', info='Repeated ramp rate',) + self.ctrl.expand_dims = 1 + self.c0.expand_dims = 1 + self.pmax.expand_dims = 1 + self.pmin.expand_dims = 1 + self.pg0.expand_dims = 1 + self.rate_a.expand_dims = 1 class ED(RTED): """ @@ -130,12 +143,6 @@ def __init__(self, system, config): self.ug.src = 'ug' self.ug.tex_name = r'u_{g}^{T}', - self.ctrl.expand_dims = 1 - self.c0.expand_dims = 1 - self.pmax.expand_dims = 1 - self.pmin.expand_dims = 1 - self.pg0.expand_dims = 1 - self.rate_a.expand_dims = 1 self.dud.expand_dims = 1 self.ddd.expand_dims = 1 @@ -171,12 +178,8 @@ def __init__(self, system, config): # NOTE: Spg @ pg returns a row vector self.pb.e_str = '- gs @ pg + pdsz' # power balance - self.prsb = Constraint(info='spinning reserve balance', - name='prs', type='eq', - e_str='mul(ugt, mul(pmax, tlv) - pg) - prs') - self.rb = Constraint(info='spinning reserve requirement', - name='rb', type='uq', - e_str='-gs@mul(prs, ugt) + dsr') + self.prsb.e_str = 'mul(ugt, mul(pmax, tlv) - pg) - prs' + self.rsr.e_str = '-gs@prs + dsr' # --- bus power injection --- self.pnb.e_str = 'PTDF@(Cgi@pg - Cli@pds) - plf' @@ -186,7 +189,6 @@ def __init__(self, system, config): self.plfub.e_str = 'plf - mul(rate_a, tlv)' # --- ramping --- - self.rbu.e_str = 'gs@mul(ugt, pru) - mul(dud, tlv)' self.rbd.e_str = 'gs@mul(ugt, prd) - mul(ddd, tlv)' diff --git a/ams/routines/uc.py b/ams/routines/uc.py index a99ec81a..93f79c1f 100644 --- a/ams/routines/uc.py +++ b/ams/routines/uc.py @@ -8,7 +8,8 @@ from ams.core.param import RParam from ams.core.service import (NumOp, NumOpDual, MinDur) -from ams.routines.ed import ED +from ams.routines.rted import RTEDBase +from ams.routines.ed import SRBase, MPBase from ams.routines.rted import ESD1Base from ams.opt.omodel import Var, Constraint @@ -16,7 +17,42 @@ logger = logging.getLogger(__name__) -class UC(ED): +class NSRBase: + """ + Base class for non-spinning reserve. + """ + + def __init__(self) -> None: + self.cnsr = RParam(info='cost for non-spinning reserve', + name='cnsr', tex_name=r'c_{nsr}', + model='NSRCost', src='cnsr', + unit=r'$/(p.u.*h)', + indexer='gen', imodel='StaticGen',) + self.dnsrp = RParam(info='non-spinning reserve requirement in percentage', + name='dnsr', tex_name=r'd_{nsr}', + model='NSR', src='demand', + unit='%',) + self.prns = Var(info='non-spinning reserve', + name='prns', tex_name=r'p_{r, ns}', + model='StaticGen', nonneg=True,) + + self.dnsrpz = NumOpDual(u=self.pdz, u2=self.dnsrp, fun=np.multiply, + name='dnsrpz', tex_name=r'd_{nsr, p, z}', + info='zonal non-spinning reserve requirement in percentage',) + self.dnsr = NumOpDual(u=self.dnsrpz, u2=self.sd, fun=np.multiply, + rfun=np.transpose, + name='dnsr', tex_name=r'd_{nsr}', + info='zonal non-spinning reserve requirement', + no_parse=True,) + + # NOTE: define e_str in dispatch model + self.prnsb = Constraint(info='non-spinning reserve balance', + name='prnsb', type='eq',) + self.rnsr = Constraint(info='non-spinning reserve requirement', + name='rnsr', type='uq',) + + +class UC(RTEDBase, MPBase, SRBase, NSRBase): """ DC-based unit commitment (UC). The bilinear term in the formulation is linearized with big-M method. @@ -49,11 +85,16 @@ class UC(ED): """ def __init__(self, system, config): - ED.__init__(self, system, config) + RTEDBase.__init__(self, system, config) + MPBase.__init__(self) + SRBase.__init__(self) + NSRBase.__init__(self) - self.config.add(OrderedDict((('cul', 1000), + self.config.add(OrderedDict((('t', 1), + ('cul', 1000), ))) self.config.add_extra("_help", + t="time interval in hours", cul="penalty for unserved load, $/p.u.", ) @@ -84,15 +125,27 @@ def __init__(self, system, config): self.timeslot.info = 'Time slot for multi-period UC' self.timeslot.model = 'UCTSlot' - self.cnsr = RParam(info='cost for non-spinning reserve', - name='cnsr', tex_name=r'c_{nsr}', - model='NSRCost', src='cnsr', - unit=r'$/(p.u.*h)', - indexer='gen', imodel='StaticGen',) - self.dnsrp = RParam(info='non-spinning reserve requirement in percentage', - name='dnsr', tex_name=r'd_{nsr}', - model='NSR', src='demand', - unit='%',) + self.ug.expand_dims = 1 + + # NOTE: extend pg to 2D matrix, where row is gen and col is timeslot + self.pg.horizon = self.timeslot + self.pg.info = '2D Gen power' + + self.plf.horizon = self.timeslot + self.plf.info = '2D Line flow' + + self.prs.horizon = self.timeslot + self.prs.info = '2D Spinning reserve' + + self.prns.horizon = self.timeslot + self.prns.info = '2D Non-spinning reserve' + + pglb = '-pg + mul(mul(nctrl, pg0), tlv) ' + pglb += '+ mul(mul(ctrl, pmin), tlv)' + self.pglb.e_str = pglb + pgub = 'pg - mul(mul(nctrl, pg0), tlv) ' + pgub += '- mul(mul(ctrl, pmax), tlv)' + self.pgub.e_str = pgub # --- vars --- self.ugd = Var(info='commitment decision', @@ -131,8 +184,22 @@ def __init__(self, system, config): e_str='-ugd[:, 0] + ug[:, 0] - wgd[:, 0]',) # --- constraints --- - self.pb.e_str = '- gs @ zug + pds' # power balance - self.pb.type = 'uq' + self.pb.e_str = 'gs @ zug - pdsz' # power balance + self.pb.type = 'uq' # soft constraint + + # spinning reserve + self.prsb.e_str = 'mul(ugd, mul(pmax, tlv)) - zug - prs' + # spinning reserve requirement + # TODO: rsr eqn is not correct, need to fix + self.rsr.e_str = '-gs@prs + dsr' + + # non-spinning reserve + self.prnsb.e_str = 'mul(1-ugd, mul(pmax, tlv)) - prns' + # non-spinning reserve requirement + self.rnsr.e_str = '-gs@prns + dnsr' + + # --- bus power injection --- + self.pnb.e_str = 'PTDF@(Cgi@pg - Cli@pds) - plf' # --- big M for ugd*pg --- self.Mzug = NumOp(info='10 times of max of pmax as big M for zug', @@ -147,18 +214,6 @@ def __init__(self, system, config): self.zugub2 = Constraint(name='zugub2', info='zug upper bound', type='uq', e_str='zug - Mzug dot ugd') - # --- reserve --- - # supplement non-spinning reserve - self.dnsrpz = NumOpDual(u=self.pdz, u2=self.dnsrp, fun=np.multiply, - name='dnsrpz', tex_name=r'd_{nsr, p, z}', - info='zonal non-spinning reserve requirement in percentage',) - self.dnsr = NumOpDual(u=self.dnsrpz, u2=self.sd, fun=np.multiply, - rfun=np.transpose, - name='dnsr', tex_name=r'd_{nsr}', - info='zonal non-spinning reserve requirement',) - self.nsr = Constraint(name='nsr', info='non-spinning reserve', type='uq', - e_str='-gs@(multiply((1 - ugd), mul(pmax, tlv))) + dnsr') - # --- minimum ON/OFF duration --- self.Con = MinDur(u=self.pg, u2=self.td1, name='Con', tex_name=r'T_{on}', @@ -173,19 +228,13 @@ def __init__(self, system, config): name='doff', type='uq', e_str='multiply(Coff, wgd) - (1 - ugd)') - # --- penalty for unserved load --- - self.Cgi = NumOp(u=self.Cg, fun=np.linalg.pinv, - name='Cgi', tex_name=r'C_{g}^{-1}', - info='inverse of Cg', - no_parse=True) - # --- objective --- gcost = 'sum(c2 @ (t dot zug)**2 + c1 @ (t dot zug))' gcost += '+ sum(mul(mul(ug, c0), tlv))' acost = ' + sum(csu * vgd + csd * wgd)' # action - srcost = ' + sum(csr @ (mul(mul(pmax, tlv), ugd) - zug))' # spinning reserve - nsrcost = ' + sum(cnsr @ mul((1 - ugd), mul(pmax, tlv)))' # non-spinning reserve - dcost = ' + sum(cul dot pos(gs @ pg - pds))' # unserved energy + srcost = ' + sum(csr @ prs)' # spinning reserve + nsrcost = ' + sum(cnsr @ prns)' # non-spinning reserve + dcost = ' + sum(cul dot pos(gs @ pg - pdsz))' # unserved load self.obj.e_str = gcost + acost + srcost + nsrcost + dcost def _initial_guess(self): @@ -231,6 +280,23 @@ def init(self, **kwargs): self._initial_guess() return super().init(**kwargs) + def dc2ac(self, **kwargs): + """ + AC conversion ``dc2ac`` is not implemented yet for + multi-period dispatch. + """ + return NotImplementedError + + def unpack(self, **kwargs): + """ + Multi-period dispatch will not unpack results from + solver into devices. + + # TODO: unpack first period results, and allow input + # to specify which period to unpack. + """ + return None + class UC2(UC, ESD1Base): """ From 893191845ce4c70cff1e7d17abc210b69a5402bb Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 23 Nov 2023 23:26:15 -0500 Subject: [PATCH 63/77] Refactor UC --- ams/routines/ed.py | 47 +++++++++++++++++++++++++++------------------- ams/routines/uc.py | 23 ++++++++++++----------- 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/ams/routines/ed.py b/ams/routines/ed.py index 253fdedb..40994b11 100644 --- a/ams/routines/ed.py +++ b/ams/routines/ed.py @@ -238,30 +238,14 @@ def unpack(self, **kwargs): # if ``Region``, if ``Bus`` has param ``zone``, optional, if none, auto fill -class ED2(ED, ESD1Base): +class ESD1MPBase(ESD1Base): """ - ED with energy storage :ref:`ESD1`. - The bilinear term in the formulation is linearized with big-M method. + Extended base class for energy storage in multi-period dispatch. """ - def __init__(self, system, config): - ED.__init__(self, system, config) + def __init__(self): ESD1Base.__init__(self) - self.config.t = 1 # dispatch interval in hour - - self.info = 'Economic dispatch with energy storage' - self.type = 'DCED' - - # NOTE: extend vars to 2D - self.SOC.horizon = self.timeslot - self.pce.horizon = self.timeslot - self.pde.horizon = self.timeslot - self.uce.horizon = self.timeslot - self.ude.horizon = self.timeslot - self.zce.horizon = self.timeslot - self.zde.horizon = self.timeslot - self.Mre = RampSub(u=self.SOC, name='Mre', tex_name=r'M_{r,E}', info='Subtraction matrix for SOC', no_parse=True) @@ -287,3 +271,28 @@ def __init__(self, system, config): self.SOCr = Constraint(name='SOCr', type='eq', info='SOC requirement', e_str='SOC[:, -1] - SOCinit',) + + +class ED2(ED, ESD1Base): + """ + ED with energy storage :ref:`ESD1`. + The bilinear term in the formulation is linearized with big-M method. + """ + + def __init__(self, system, config): + ED.__init__(self, system, config) + ESD1Base.__init__(self) + + self.config.t = 1 # dispatch interval in hour + + self.info = 'Economic dispatch with energy storage' + self.type = 'DCED' + + # NOTE: extend vars to 2D + self.SOC.horizon = self.timeslot + self.pce.horizon = self.timeslot + self.pde.horizon = self.timeslot + self.uce.horizon = self.timeslot + self.ude.horizon = self.timeslot + self.zce.horizon = self.timeslot + self.zde.horizon = self.timeslot diff --git a/ams/routines/uc.py b/ams/routines/uc.py index 93f79c1f..d9464aea 100644 --- a/ams/routines/uc.py +++ b/ams/routines/uc.py @@ -9,8 +9,7 @@ from ams.core.param import RParam from ams.core.service import (NumOp, NumOpDual, MinDur) from ams.routines.rted import RTEDBase -from ams.routines.ed import SRBase, MPBase -from ams.routines.rted import ESD1Base +from ams.routines.ed import SRBase, MPBase, ESD1MPBase from ams.opt.omodel import Var, Constraint @@ -91,7 +90,7 @@ def __init__(self, system, config): NSRBase.__init__(self) self.config.add(OrderedDict((('t', 1), - ('cul', 1000), + ('cul', 10000), ))) self.config.add_extra("_help", t="time interval in hours", @@ -234,7 +233,7 @@ def __init__(self, system, config): acost = ' + sum(csu * vgd + csd * wgd)' # action srcost = ' + sum(csr @ prs)' # spinning reserve nsrcost = ' + sum(cnsr @ prns)' # non-spinning reserve - dcost = ' + sum(cul dot pos(gs @ pg - pdsz))' # unserved load + dcost = ' + sum(cul dot pos(pdsz - gs @ pg))' # unserved load self.obj.e_str = gcost + acost + srcost + nsrcost + dcost def _initial_guess(self): @@ -298,20 +297,22 @@ def unpack(self, **kwargs): return None -class UC2(UC, ESD1Base): +class UC2(UC, ESD1MPBase): """ UC with energy storage :ref:`ESD1`. """ def __init__(self, system, config): UC.__init__(self, system, config) - ESD1Base.__init__(self) + ESD1MPBase.__init__(self) self.info = 'unit commitment with energy storage' self.type = 'DCUC' - # TODO: finish UC with energy storage formulation - # self.SOC.horizon = self.timeslot - # self.pge.horizon = self.timeslot - # self.ude.horizon = self.timeslot - # self.zue.horizon = self.timeslot + self.SOC.horizon = self.timeslot + self.pce.horizon = self.timeslot + self.pde.horizon = self.timeslot + self.uce.horizon = self.timeslot + self.ude.horizon = self.timeslot + self.zce.horizon = self.timeslot + self.zde.horizon = self.timeslot From 6b9e9a21a1491dc2450495f8099435963c8b4d67 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 23 Nov 2023 23:53:55 -0500 Subject: [PATCH 64/77] Refactor DOPF --- ams/routines/dcopf.py | 3 ++- ams/routines/dopf.py | 52 +++++++++++++++++-------------------------- 2 files changed, 22 insertions(+), 33 deletions(-) diff --git a/ams/routines/dcopf.py b/ams/routines/dcopf.py index 23af5509..da3dc9dc 100644 --- a/ams/routines/dcopf.py +++ b/ams/routines/dcopf.py @@ -65,7 +65,7 @@ def __init__(self, system, config): unit='p.u.', model='StaticGen',) # --- load --- - self.pd = RParam(info='nodal active demand', + self.pd = RParam(info='active demand', name='pd', tex_name=r'p_{d}', model='StaticLoad', src='p0', unit='p.u.',) @@ -230,6 +230,7 @@ def __init__(self, system, config): self.pb = Constraint(name='pb', info='power balance', e_str='sum(pd) - sum(pg)', type='eq',) + # TODO: add eqn to get aBus self.aref = Constraint(name='aref', type='eq', info='reference bus angle', e_str='Cs@aBus',) diff --git a/ams/routines/dopf.py b/ams/routines/dopf.py index cd2e2337..6a97a1c9 100644 --- a/ams/routines/dopf.py +++ b/ams/routines/dopf.py @@ -4,14 +4,13 @@ import numpy as np from ams.core.param import RParam -from ams.core.service import NumOp from ams.routines.dcopf import DCOPF from ams.opt.omodel import Var, Constraint, Objective -class LDOPF(DCOPF): +class DOPF(DCOPF): """ Linearzied distribution OPF, where power loss are ignored. @@ -28,9 +27,9 @@ def __init__(self, system, config): self.type = 'DED' # --- params --- - self.ql = RParam(info='reactive power demand connected to Bus (system base)', - name='ql', tex_name=r'q_{l}', unit='p.u.', - model='mats', src='ql',) + self.qd = RParam(info='reactive demand', + name='qd', tex_name=r'q_{d}', unit='p.u.', + model='StaticLoad', src='q0',) self.vmax = RParam(info="Bus voltage upper limit", name='vmax', tex_name=r'v_{max}', unit='p.u.', model='Bus', src='vmax', no_parse=True, @@ -41,29 +40,22 @@ def __init__(self, system, config): self.r = RParam(info='line resistance', name='r', tex_name='r', unit='p.u.', model='Line', src='r') - self.x = RParam(info='line reactance', - name='x', tex_name='x', unit='p.u.', - model='Line', src='x', ) - self.qmax = RParam(info='generator maximum reactive power (system base)', + self.qmax = RParam(info='generator maximum reactive power', name='qmax', tex_name=r'q_{max}', unit='p.u.', model='StaticGen', src='qmax',) - self.qmin = RParam(info='generator minimum reactive power (system base)', + self.qmin = RParam(info='generator minimum reactive power', name='qmin', tex_name=r'q_{min}', unit='p.u.', model='StaticGen', src='qmin',) # --- vars --- - self.qg = Var(info='Gen reactive power (system base)', + self.qg = Var(info='Gen reactive power', name='qg', tex_name=r'q_{g}', unit='p.u.', model='StaticGen', src='q',) self.qglb = Constraint(name='qglb', type='uq', - info='Gen reactive power lower limit', - e_str='-qg + qmin',) + info='qg min', + e_str='-qg + mul(ug, qmin)',) self.qgub = Constraint(name='qgub', type='uq', - info='Gen reactive power upper limit', - e_str='qg - qmax',) - # TODO: add qg limit, lb=self.qmin, ub=self.qmax, - self.qn = Var(info='Bus reactive power', - name='qn', tex_name=r'q_{n}', unit='p.u.', - model='Bus',) + info='qg max', + e_str='qg - mul(ug, qmax)',) self.vsq = Var(info='square of Bus voltage', name='vsq', tex_name=r'v^{2}', unit='p.u.', @@ -78,20 +70,15 @@ def __init__(self, system, config): type='uq',) self.qlf = Var(info='line reactive power', - name='qlf', tex_name=r'q_{lf}', unit='p.u.', - model='Line',) + name='qlf', tex_name=r'q_{lf}', + unit='p.u.', model='Line',) # --- constraints --- - self.pinj.e_str = 'CftT@plf - pl - pn' - self.qinj = Constraint(name='qinj', - info='node reactive power injection', - e_str='CftT@qlf - ql - qn', - type='eq',) + self.pnb.e_str = 'PTDF@(Cgi@pg - Cli@pd) - plf' self.qb = Constraint(name='qb', info='reactive power balance', - e_str='sum(ql) - sum(qg)', + e_str='sum(qd) - sum(qg)', type='eq',) - self.lvd = Constraint(name='lvd', info='line voltage drop', e_str='Cft@vsq - (r * plf + x * qlf)', @@ -106,12 +93,12 @@ def unpack(self, **kwargs): self.system.Bus.set(src='v', attr='v', value=vBus, idx=self.vsq.get_idx()) -class LDOPF2(LDOPF): +class DOPFVIS(DOPF): """ Linearzied distribution OPF with variables for virtual inertia and damping from from REGCV1, where power loss are ignored. - ERROR: the formulation is problematic, check later. + UNDER DEVELOPMENT! Reference: @@ -121,7 +108,7 @@ class LDOPF2(LDOPF): """ def __init__(self, system, config): - LDOPF.__init__(self, system, config) + DOPF.__init__(self, system, config) # --- params --- self.cm = RParam(info='Virtual inertia cost', @@ -141,7 +128,8 @@ def __init__(self, system, config): self.D = Var(info='Emulated damping coefficient from REGCV1', name='D', tex_name=r'D', unit='p.u.', model='REGCV1',) + obj = 'sum(c2 * pg**2 + c1 * pg + ug * c0 + cm * M + cd * D)' self.obj = Objective(name='tc', info='total cost', unit='$', - e_str='sum(c2 * pg**2 + c1 * pg + ug * c0 + cm * M + cd * D)', + e_str=obj, sense='min',) From 8a9e22820dc34821036e98b844166eade3dc0c9f Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 23 Nov 2023 23:54:29 -0500 Subject: [PATCH 65/77] Rename routines --- ams/routines/__init__.py | 8 ++++---- ams/routines/ed.py | 2 +- ams/routines/rted.py | 2 +- ams/routines/uc.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ams/routines/__init__.py b/ams/routines/__init__.py index 29b07a21..8e2cb649 100644 --- a/ams/routines/__init__.py +++ b/ams/routines/__init__.py @@ -12,10 +12,10 @@ ('cpf', ['CPF']), ('acopf', ['ACOPF']), ('dcopf', ['DCOPF']), - ('ed', ['ED', 'ED2']), - ('rted', ['RTED', 'RTED2']), - ('uc', ['UC', 'UC2']), - ('dopf', ['LDOPF', 'LDOPF2']), + ('ed', ['ED', 'EDES']), + ('rted', ['RTED', 'RTEDES']), + ('uc', ['UC', 'UCES']), + ('dopf', ['DOPF', 'DOPFVIS']), ]) class_names = list_flatten(list(all_routines.values())) diff --git a/ams/routines/ed.py b/ams/routines/ed.py index 40994b11..840bc582 100644 --- a/ams/routines/ed.py +++ b/ams/routines/ed.py @@ -273,7 +273,7 @@ def __init__(self): e_str='SOC[:, -1] - SOCinit',) -class ED2(ED, ESD1Base): +class EDES(ED, ESD1Base): """ ED with energy storage :ref:`ESD1`. The bilinear term in the formulation is linearized with big-M method. diff --git a/ams/routines/rted.py b/ams/routines/rted.py index 7a1c929c..a8db008e 100644 --- a/ams/routines/rted.py +++ b/ams/routines/rted.py @@ -387,7 +387,7 @@ def __init__(self): e_str=SOCb,) -class RTED2(RTED, ESD1Base): +class RTEDES(RTED, ESD1Base): """ RTED with energy storage :ref:`ESD1`. The bilinear term in the formulation is linearized with big-M method. diff --git a/ams/routines/uc.py b/ams/routines/uc.py index d9464aea..c2e39df7 100644 --- a/ams/routines/uc.py +++ b/ams/routines/uc.py @@ -297,7 +297,7 @@ def unpack(self, **kwargs): return None -class UC2(UC, ESD1MPBase): +class UCES(UC, ESD1MPBase): """ UC with energy storage :ref:`ESD1`. """ From 029c9210c94c20a175e552b13ba511c2c7f0fddd Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 24 Nov 2023 00:21:18 -0500 Subject: [PATCH 66/77] Typo --- ams/routines/dcopf.py | 2 +- ams/routines/rted.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ams/routines/dcopf.py b/ams/routines/dcopf.py index da3dc9dc..7127267c 100644 --- a/ams/routines/dcopf.py +++ b/ams/routines/dcopf.py @@ -41,7 +41,7 @@ def __init__(self, system, config): name='c2', tex_name=r'c_{2}', unit=r'$/(p.u.^2)', model='GCost', indexer='gen', imodel='StaticGen', - pos=True) + nonneg=True) self.c1 = RParam(info='Gen cost coefficient 1', name='c1', tex_name=r'c_{1}', unit=r'$/(p.u.)', model='GCost', diff --git a/ams/routines/rted.py b/ams/routines/rted.py index a8db008e..0640eedd 100644 --- a/ams/routines/rted.py +++ b/ams/routines/rted.py @@ -52,10 +52,6 @@ def __init__(self, system, config): name='R10', tex_name=r'R_{10}', model='StaticGen', src='R10', unit='p.u./h',) - self.gammape = RParam(info='Ratio of ESD1.pge w.r.t to that of static generator', - name='gammape', tex_name=r'\gamma_{p,e}', - model='ESD1', src='gammap', - no_parse=True,) class SFRBase: @@ -312,6 +308,10 @@ def __init__(self): name='gene', tex_name=r'g_{E}', model='ESD1', src='gen', no_parse=True,) + info = 'Ratio of ESD1.pge w.r.t to that of static generator', + self.gammape = RParam(name='gammape', tex_name=r'\gamma_{p,e}', + model='ESD1', src='gammap', + no_parse=True, info=info) # --- service --- self.REtaD = NumOp(name='REtaD', tex_name=r'\frac{1}{\eta_d}', From 397e78f7dbd783bf0f7db85a9e65868459aa68a4 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 27 Nov 2023 22:38:55 -0500 Subject: [PATCH 67/77] Fix ACOPF --- ams/routines/acopf.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/ams/routines/acopf.py b/ams/routines/acopf.py index 24444b36..bfa2a97d 100644 --- a/ams/routines/acopf.py +++ b/ams/routines/acopf.py @@ -104,6 +104,20 @@ def __init__(self, system, config): self.type = 'ACED' # --- params --- + self.c2 = RParam(info='Gen cost coefficient 2', + name='c2', tex_name=r'c_{2}', + unit=r'$/(p.u.^2)', model='GCost', + indexer='gen', imodel='StaticGen', + nonneg=True) + self.c1 = RParam(info='Gen cost coefficient 1', + name='c1', tex_name=r'c_{1}', + unit=r'$/(p.u.)', model='GCost', + indexer='gen', imodel='StaticGen',) + self.c0 = RParam(info='Gen cost coefficient 0', + name='c0', tex_name=r'c_{0}', + unit=r'$', model='GCost', + indexer='gen', imodel='StaticGen', + no_parse=True) self.ql = RParam(info='reactive power demand (system base)', name='ql', tex_name=r'q_{l}', model='mats', src='ql', From 6fee997aa33c6757e5c6990e4075a442bbbd240e Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 27 Nov 2023 22:39:11 -0500 Subject: [PATCH 68/77] Fix tests --- tests/test_case.py | 8 ++++---- tests/test_omodel.py | 4 ++-- tests/test_service.py | 45 ++++++++++++++++--------------------------- 3 files changed, 23 insertions(+), 34 deletions(-) diff --git a/tests/test_case.py b/tests/test_case.py index 625fdfd4..a9d8be54 100644 --- a/tests/test_case.py +++ b/tests/test_case.py @@ -237,8 +237,8 @@ def test_ieee39_esd1_init(self): default_config=True, no_output=True, ) - ss.ED2.init() - ss.UC2.init() + ss.EDES.init() + ss.UCES.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.") + self.assertEqual(ss.EDES.exit_code, 0, "Exit code is not 0.") + self.assertEqual(ss.UCES.exit_code, 0, "Exit code is not 0.") diff --git a/tests/test_omodel.py b/tests/test_omodel.py index 6823dcec..5ead7cb5 100644 --- a/tests/test_omodel.py +++ b/tests/test_omodel.py @@ -52,13 +52,13 @@ def test_constr_access_brfore_solve(self): Test `Constr` access before solve. """ self.ss.DCOPF.init(force=True) - np.testing.assert_equal(self.ss.DCOPF.lub.v, None) + np.testing.assert_equal(self.ss.DCOPF.plflb.v, None) def test_constr_access_after_solve(self): """ Test `Constr` access after solve. """ self.ss.DCOPF.run() - self.assertIsInstance(self.ss.DCOPF.lub.v, np.ndarray) + self.assertIsInstance(self.ss.DCOPF.plflb.v, np.ndarray) # NOTE: add Var, Constr add functions diff --git a/tests/test_service.py b/tests/test_service.py index 1e53fd75..44f37e04 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -65,14 +65,13 @@ class TestService(unittest.TestCase): """ def setUp(self) -> None: - self.ss = ams.load( - ams.get_case("ieee39/ieee39_uced_esd1.xlsx"), - default_config=True, - no_output=True, - ) + self.ss = ams.load(ams.get_case("ieee39/ieee39_uced_esd1.xlsx"), + default_config=True, + no_output=True,) self.nR = self.ss.Region.n self.nB = self.ss.Bus.n - self.nl = self.ss.Line.n + self.nL = self.ss.Line.n + self.nD = self.ss.StaticLoad.n # number of static loads def test_NumOp_norfun(self): """ @@ -92,20 +91,14 @@ def test_NumOp_ArrayOut(self): """ Test `NumOp` non-array output. """ - M = NumOp( - u=self.ss.PV.pmax, - fun=np.max, - rfun=np.dot, - rargs=dict(b=10), - array_out=True, - ) - M2 = NumOp( - u=self.ss.PV.pmax, - fun=np.max, - rfun=np.dot, - rargs=dict(b=10), - array_out=False, - ) + M = NumOp(u=self.ss.PV.pmax, + fun=np.max, + rfun=np.dot, rargs=dict(b=10), + array_out=True,) + M2 = NumOp(u=self.ss.PV.pmax, + fun=np.max, + rfun=np.dot, rargs=dict(b=10), + array_out=False,) self.assertIsInstance(M.v, np.ndarray) self.assertIsInstance(M2.v, (int, float)) @@ -123,15 +116,11 @@ def test_ZonalSum(self): """ Test `ZonalSum`. """ - ds = ZonalSum( - u=self.ss.RTED.zb, - zone="Region", - name="ds", - tex_name=r"S_{d}", - info="Sum pl vector in shape of zone", - ) + ds = ZonalSum(u=self.ss.RTED.zd, zone="Region", + name="ds", tex_name=r"S_{d}", + info="Sum pl vector in shape of zone",) ds.rtn = self.ss.RTED # check if the shape is correct - np.testing.assert_array_equal(ds.v.shape, (self.nR, self.nB)) + np.testing.assert_array_equal(ds.v.shape, (self.nR, self.nD)) # check if the values are correct self.assertTrue(np.all(ds.v.sum(axis=1) <= np.array([self.nB, self.nB]))) From d26ea039c97aa0000d9ece99494f6b5e51d4d4c5 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Tue, 28 Nov 2023 11:00:38 -0500 Subject: [PATCH 69/77] [WIP] Clean up cases --- ams/cases/5bus/pjm5bus_uced.xlsx | Bin 26854 -> 26988 bytes ams/cases/ieee123/ieee123_regcv1.xlsx | Bin 41617 -> 41637 bytes ams/cases/ieee14/ieee14_uced.xlsx | Bin 31601 -> 31712 bytes ams/cases/ieee39/ieee39_esd1.xlsx | Bin 34170 -> 0 bytes ams/cases/ieee39/ieee39_uced.xlsx | Bin 38691 -> 38787 bytes ams/cases/ieee39/ieee39_uced_esd1.xlsx | Bin 39132 -> 39261 bytes ams/cases/npcc/npcc_uced.xlsx | Bin 82064 -> 82198 bytes ams/cases/wecc/wecc_uced.xlsx | Bin 89525 -> 89635 bytes 8 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 ams/cases/ieee39/ieee39_esd1.xlsx diff --git a/ams/cases/5bus/pjm5bus_uced.xlsx b/ams/cases/5bus/pjm5bus_uced.xlsx index ccc2bb56b53f61f84d5fce5dfe015392b9a4f305..2a9446cafaa4843c0c12d0880ffd8136f8ba2cea 100644 GIT binary patch delta 4490 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 delta 4391 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 diff --git a/ams/cases/ieee14/ieee14_uced.xlsx b/ams/cases/ieee14/ieee14_uced.xlsx index 1986b6a59c6efe19dc14fda31c2943b7adb898b7..dc7a723be702b9391f14759b1e72bedcfc4fc23e 100644 GIT binary patch delta 8186 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 V2NMV&L0E(-1u&zUc>RU(e*o8?`-uPm delta 8233 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 diff --git a/ams/cases/ieee39/ieee39_esd1.xlsx b/ams/cases/ieee39/ieee39_esd1.xlsx deleted file mode 100644 index c324dd451d47268f994ceac3880bd134ee314e32..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34170 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 EKUYCZTL1t6 diff --git a/ams/cases/ieee39/ieee39_uced_esd1.xlsx b/ams/cases/ieee39/ieee39_uced_esd1.xlsx index 6555130c9f2d91005815406bcea03b4fbc925cb7..2ee3608150b24e6a6d72806c87b0ebf8a352eff7 100644 GIT binary patch delta 6213 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 v5;t<6-(x&d$vnue(QuDsddkQ56$u2gd`S5b{Xt delta 6136 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 F{|`c7qfY<; diff --git a/ams/cases/npcc/npcc_uced.xlsx b/ams/cases/npcc/npcc_uced.xlsx index c879f42fc14c03ebb18cc3d900e3f1d1afa75ad8..58d63a6b917c412b09b6e63ea5a59045a0bfe42e 100644 GIT binary patch delta 9126 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 delta 8917 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 diff --git a/ams/cases/wecc/wecc_uced.xlsx b/ams/cases/wecc/wecc_uced.xlsx index 49069e3b293f508d2ddea05f57bad264944736bf..cf3e6a29372fd295098645b24e79112134fa86b8 100644 GIT binary patch delta 8637 zcmZ8{1yGz#vo(vm%i;tH4!gMfLU4Bt5FiA1UmOw^3GNa+1Sb$Yc(4Egf)g|l+@0_* zy!U?Z_us9lQ`3FAPxn^M&NDU7er`o6Y(uFj!@$ttWs*ikMnGUss>Psy7778|&0qfl zT<3;{j3cMSC2DxFf7TULT$rmELSa58b@`e8eA;d~P$Ef++90-$Iyrq-LPFy2-|QIf zRx#e?G8S@fneZ_O>v8&3R(8E-eg?h2@yUxBAxb=E_f0XJWYyf+ofJ|U z$lRDSX?Z<7BSha%dfnrz6)=zsd;#=xRD%wNni#xc+s$jbXjyM4y0o)>q2nf} z@{dC6SNl2;#obuonwewqu}SwkVV818vaOWkKIHOyyd%~+1JT}%fDklxupT+kYAC9b zZ567O^2_a-)_V5uD*@~R-2o+p{#(X{{ORBM!!b<5+v^e@ICXQ2?@fH!WaZ_Rd{cN7 zC-HM+)13bd1V>?OIzA^Ewjf=Qg}wKji#%-yX|*uwJ}1Nfh9izZJ?0t^5&jiuoA@sD zg~(<^xR6-(uD(^(@7yY=u-q4L;e(!v#tV*UZXxytMvQODzUmwEWpbqSy9MDr#t$_g zf*qC;Eno4!)ih!g=S)mH7ktJP@hp=umGpB_9{mG@li~;1%MkvHcbA>5sen~_JIhlu zOr#)l1;Ld(VP7O?X_cZV>khpGm%JD`ln|8*2_@3&UB8w9cMNxgB z!xbDTmoGpt^E!uIBtT0##Ib;`CPHLLNaXA-BCQ~>=Fvzt9ukTE;`rgiPY9ZN58mIkx5%*?)0 z+H-}CWMM}VEkLP-OUTw|&yKzcFcbaaMbq5F{qD-wPSN|3t6UP&r3U7b^qs+}^y6o$ z3-y;W1$atoI0gV6(b8^1Wc6rMjx9)8wQ2Mou!W-DFcvb0t`87}Otvoc?Z2JPs^~k_ z4q)>M5PjQUx;{`pcXik|J!6BlD~V$tQoYLcM$Fv2=p$0J+25oIB$E0CoTiVMfdP{d z_9zGl`IJc$?G#WDmye@EcG6(nIvsxGA;YLB-2raxx=<}iX^QS-)5Is+&|xqp5HCTs z(D#y6RJkcxc`6YMEIX{`kxWWU(%V4zsi?ihw|CAOVw5iPb0PchT`g{Zve4LWD7&_- z+StnW@Y!kU;KXcB1%jBg%)XnE?=NeZux7~bDqD&&*J3E2o_l>ll zQAzWsKM@IYw!jjO;^1Nef1J;qQXDJ?V5t6J|B{`NSlHy^!G$Uv%n85tz0!?4TCrTr zo~JS8ups8HL@!}|Db4UQAf(p^|$YBzy6#edg(Av0lrvd)venHymVRVHl-Nnq;XLU442FzEq2nY`k2#=7Rz7#hP!k0Hk*#ux-$OP+> zLdyrQr12s16^@_t?|pCQk7s>4il%$LqpwlNx-f*lw6>d7dyD)UD?PZs+}gP>29NGs zY~6XCR_A(;?wlW(H;nEKJB+1mng8-=H?4C1J35NDklmtecTMhRmKj)oa`@}6m6rK* zs!~#I>8AUA5XIcLUXZFmMt-^w@i3LQKQzOYw{Z4?z{r0HX3wr6t1g9;AT>UanSF?m z{0_Tsr1nl^l$6x{b;6ox)4mly;6?{P;YU~&G796z@I%&sL|URsy#%MBP6Ru#OUbGu zMzB}K^8@DBJS)Cwj3BUZ{Qf~lp#in!>QIK0eG9xl8Ce{9^Z(_b8V{58K)y4^aZZz# z+UL?Q8BqUA^TvFvFTa2}w?qy)3@5c48n#x(OqqfSAm zo~YG{p97i55wSpG@^|D2eOq=Ea!B?9Bt=%*G7+WLV~{~ie=yWeruSKX#JLHLe1viF z3q_Ssp|ACIg1k1`FNRqR!=B4gA(|MBxhkPANN#pb+5MPJJ_kVFR=D|3B=bF6OW0T{ zVBdVLvtVqcEWb^7L#WThjhXbAWTsOh7W=MAK2znjD(dIA6`|ge$M@u}-T24M0Palf zT=_oZb+wY@Qs|$&EOP$&LWyIAZ`>&GUREF>Lz87bZ;09MHvP9Hm5BA3FpZL*B+tqo zCHsq1%C%5U-&G`%y;35g#|3cj=se0X=#=oWg(>yv4#ubzV-Q3IlU1^*7p_$vvuVFe zk*;6+xB>b6BRpF6tEh01fu(+9e^r0U-5nIt{>ZdHF)@BAr4xJAl@k@UtfwE$U3E{o zORzKXUD16es!#aXmOomRolp&_;eN1D%eY8NVu3{OEgBNjgb1nq3Hb5zYI-(V{q=bD z7q&~X`^U}(?;RiWKHgUzv$LxeKQcae+xq_{NYP1ES8Q%5mh;#{=RZ9JrIcQCAKtk7 z{3S~oV|-IT&@O<2*%nKQ_?HX;;V3F8wu(H7t_KY|=(GTW@0chzxX6bBkU=nEmzzc} zIz>ZN{vWrJ^7oMR7^NS^?>hV=-P(yefX^7;QQ&r7N6D%kG|4CNU*%_4ve4Qj2d7l& zd1+42{-Q-|N)Bgn;B2VZn4m`Xlf1urS?3x=ZWR0?!9o_Y0BlXHr85T6W1FIp+EQ%A zMqfd9$v8Y{*#4R-vzC0lI4;lt1{nX+>~1oeed6x7Cxniu#s~2Cl?`!cspfK z<$+uxN7M}HTGMccVP?s4Gl3D$C4zqf0otTIANj=!jUiiH^0q$$peXd26o+PA7Zg(Z zNH-r^dhdSu;?2q02835YFIkJgL$(U{TkEo1@%ItF7uH;eVnX-I)3uOVAMhKyUZsJg zV=o`$4a2U}to04SrfY2biIThVdD^kML0n?*QNV+syyEF0Jdkt9KXOf|(b&Gd(>sGxv1T#fN9y%k9GAZT{{-&`X!w{n5?qrL5tjP7J?F zq^SMLjyOkL&yCAcPLK23Nu(kE9d#$bb)zVaxzn!OFV>dN7-hGu~ zC!jrk&XVupHPn^Ad|iLqqt*~tT4KGhjY#Ux9>UbGEpGX=h5O4r z91!B)q8}!o?UK!8o!^EiB1VkwGAG>mid3X-r|A)RgU++;MX2vh4qx$-e4B^y3h5R@Mvb}CB$eVMcN z`3!o4#`J5%rmz_4e52!~8E;$=sExSYMoL=X5&#|H#g*V)-@lpVT_*6+Ul9+vM2Twz zCWnyP`thm<3pD%{YoiDw!C{{E=Nk`S`=tNI!?TM!G`BRScE1?OMQpNfCy?Jf7$?It zKt3slQ;*$!*P{M6=_ttzD;JsIX@Trki$EkpWW2yMh5VN|7ND{p)5wd^2tEV7MAV*l zNKw!#2261UVv#tOGs?HJodgj<44C)w#J929;nWJiG;I(CK24m1F(NNecm0nW_^P~W ze!}C6L1MJ0h+({hTGiK&rGi#2E%g&0-H;Z)+9nmFB=CYt=hmzuvYh3Vv0bV^ZsG@? z**BPEHm}?8(Za?ta6ita+=>C1uY&lR0BcYlV8*aq)0=|8`@qv3n>*K7V@UVz5B=YH zWCxhZM6NE2`i-h}Wg+a7bqHV`%eF10e*oRFs+JQWI&ocjFew*g^dIf~G zO@UVIfcSjav*f5#*hrp0)FBY|MHk3M3HdqG3m!J6buAtqCM#_o5vFx*9vLR94RQt; zLzokgVGQWo|BY8Z7kmPHZwnK%3Xf{GkW*1mwReJPCV}mF9227&zKh0m>~EJf_W z@|^u;5Pf{O0W*SW#(4)zm9k%akYYKb~`7A&u3P70eH&84un8%ZvAyDR?vVmkj{V z$qOZ{rfgw1FwH#h4zGM*f7uxD{0|g$*xR6`Phgv70YQuXMWk`?2_gZeVv%H$+>Ji{ zWy-g9hh66@RLzNLT-H5{yP4L+Gg=Lmd-FrFlyf<+=JsD*z9fYiOisDO)#qD{W8R>G z%lyx$oJZon5P#kJsdr9?h>maX#4NR1T%}tyM6=*T<`#8L5n=b|T7WK*!@8i%KoyBD zi?a+|6^s6tq`X|wmeCoTD7x5u^=))9t2U`*9^mZ}Kf(a;*~m#KH)_72*-;;eZy2is z2($6+B9r~W7>~uiF4GCjvhjU62rPi(Ft8MkqrgfyjsffCeFsYCj+*$nSMD+E8R&GOBrkjf`cL@@qLC`mw@9o1(ok;^F=j&V#&9(w`Ct@x*;~ ze2%W2!!p^$95`sHKRu?Xj9k|q zJtir!rhz>{L=IhTaI8dEejBp8-xr-EKIZ6Fsl?yJ;17yFvKjk%71^_yw68J zE4rrw2N_>~hZ3%*GmaWs5)&1JqZL6jOpzeP%mp@7I2yA%jl<{ceyfb5oTRd8EU_3( z$nK_=MF~aCT;Su>N4$PQ09i2@CC#ErS*3^Rb(Qks`$WDOrjqq1+<3yx4}8ym6OMSk zU3iuFyQNp0PZ2KEn^x2eMji_?g=cJ@^{wkZ7laij#&ZX*=EHawU z^0z~hRcWZ~QJ;zN4x0KGa(&2f&^Rk5 z=0qnaJD=Wvdklk*hNnn)@YC38X5GSSac7rn(P2tM2K*N#HB!6I+c-0#(%ma2=Ec28C$gF?8 zzRTFA43;_8&u)E3A9)UFq+a1lZf^u>?eC#bfAJYMtOHxinC(hdefx^avc?v-*`M99 z5@NDpoVJ{cs?B=ptYy<_I+YRHj%g7`436B!nLFb9sR8QB$FH+kPij3W8bj@{4`q@x zd^I9dX^9{duB>nWwHB!I{b^#MMCWz`6;8*jHD?N7jE|c{`(~juW z5cU2ooX=Hjn4LNM8o%AF3S$x*(9ZuOT>B=u_|FTuq;gg0tXzPaD_$Vmf%qG3+lcY! zh;CR_%0Zi!cQTZhl;x^a*c?|})2`iFaZ}g5GVX(2q3*(Yq`Afgbg5?U#gNs+)+SfB z169hHy6ckmc-qqaZL8hrQhc|p@jS>7n|-a+z&x^KOzP$>2ix;Jh5nwm?YrtX4Fb9H zHPvjgSvB?0RL75NM;2BMTjA{u-MYs4_8SqP7J$cnof5V>gg z@ySacixV%svz^&fxpu?)Q0&Bcl+yBfylG=b^m`&AbP-A%P_>6Hx2Zb0m>^0YKgD^q z;eoZeJ(_o1K;d2>R*&ZD=1goU3w~f;G_N;2qN)xIH;X1#tG{J#M#^o*VfJ2qVLTdY zYuXcHB%~y&5wjOT9T#y5HqSTOa~U~z*wxzLcSwnbyk^#O6>CDOaKUY?>OFx^gwDck zzvx2e?1`|zRN&@{y$h_`^u6?6XHNIBPcn)Nzjb^lPI870l-;-P8V6<4B{8CujY-)k z4Kph=9BoF}E;R&3i;pSM7KUUjWj>A9pbS;=h7;2>^406HCVm+mgj*ncEWRN6lvfMy zn%xNqBYUytkMC)MynX)uZL4mrS^7g#UTxuXFKUp!-gPV&w3TkL4enaGAi~~J z205T&L}I9cwt2d02pEBS4kd+lp>fPg zPOE_xrx$lMl4d#i!Cn=yF0rR~wYl0t`s?N9%kA zEB=3ouKlcHVE@3pb4^Js<CSd5o&f{0P*MH^`z z_GRlQ$cyr`|q=Rr<+)#D~OJ=9mhsBfTd)}65| zVzip$PI50%=`rVHSw^Jfq+1Ch$fH^81Zxo{RYBW=UD-57&WXQ~CxVjrN~1}$i-Z*$ zR*Oi!6IKer50Z8R@H3^Hvf7Vqrg^u--yDO1u+5i?OJ25y#Fs8Loklfg;xuF5+ZyV} z1^!t(M7aqZ#qdi6dB+!It#{3F>ts4L-mBS*^&w-$ z;J!t(vcWT0k8*0g^`5DLo)f3=`z}pwmz9d#=kC=^q^VXmI_&W4YFAGrs{hgVJd;(u z{^(Sm$F^zhk>$tJV)o<2jmNL9GpIu6{@mYf@4h3QdC5?rqL3S>WQG{3+Il){wRBch zMQu}Db$m#iLG`<+iuwvUsRuU2uV=?AJfdJKs#0SrI#NBC4z{wRv+B0DNz!tDNdqL; z=jFd`#|0w~gl=du;Q6 zN$5!9;r8Hy;bSj03sI+gu8*xWI&TlIKeiM6oiW4K;-5nQQ{nFKGA#9fy46ra9s7^i zg`x0nk|DhHeR$+;WA>S%gdAQLRYis)OWB|Ej^$E_@WTW2!GEy${(jW^0r2>m1Su)% z2CvDE5b#qP#hghfUA0M82fzz}7EKov>`T2;$3+n&{w3qqin36|IN|uP+^}T-Dl}ej zYcbxPYjEqfB(CGD$SaQ!I~fgRBVhSiiPY{ieTVe>Qxjz~N*olCVA<1?6M8aEqWm)l#u!qsxKM~lzX&#t9DATgRWzRtyR0jPw23AwGktO`IhoMb zWd%&khm0OKgQae9k;tuyMYL)QdK7&}4`}tUbV?XUE!`+f_1P17GQlGLhi-TChg!IJ zqu^1foRa?{#$S(O~?cwjWkRM zNGkY3lJ?#Nz>UOC`j0Vd0^mZ@Ab&K7)9}dwC~2uC0Q@u_Qvd}ZDoxcCz(D!5g5c}Q Z1`z>);9phvtROAM6u^keqz`8i{vR>&4EfBH`4>$H%uzygS)Q_}(Kz zOt$uW?7L3A?HJ!WzLJ3Si7lCKz(gOT0t3NG3eGTY#s?GG_6N_}tyM?ZRVDz*mY=WT z%^%Uw8W$zPB%>oLh>Dr@@M|eXcNmJ5h@?V_BkMm3P|A5v4Jlo_RIKdA(2%O1?D^uj zC!vKkjQD)a1nXdf`a!mO#@0|yx>b%PJvuedSJmdAWE$iWTgF_k7KJ9-GGJUlhu9kJ z@>&<^{FvaBD)-bJn_2EMnaSXb9NB{H>8t8kZgJkuT*r_>NP4sUm{*~{ZW+NJ7Oke2 zX@2zG=UlaBXchF@qTIh&W_|1Udnp&o^^`vij=k)2x_CvfQ8L44p46}RoiR3i^QHS$ zb~Z|LA>HvJBN#Mf-V$FOCF>z5fx+-5A zv}Lk$B|AAj-@Q3u}fqu<)MAH;EZ@r2P6xkJF(5MlIkJ?_jEQ(2yc1tK28|K$kp2rnvc1!J-MO9^ff7i^a=;(-w)&L=2bpHI1eyp4Z5U|6h=Ii8zG(|Sy z=y;s0xposJ z(MBneJ=zQ50-Ka^j6ukWf~r>d%S25K3rZ0q;{ub%5s7Q(u^UP;@FH0J?%`X*SJLF_ z9dZaar?;a+R`NgEXaM>yjK0r*D)i6R9J0`>zpPF2o0MKJiJ)U^ zt_EvITl7wg@;cv++-{T;3k7@pm?XnSl~&9lDNH!y%v)-A3}$5-IcZ)Vm`oVQ6uhpv zytNJF@jPHdP)ks6folH}V6{-y%xR)XN3ny5vc^s=tQ9SSe(Em2Ce0^~3GWWI<+S%@ z%0DLyXH{aQ6ZqL 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 From fa52ec35d6f28d07c52c5e52995079e01e63ba3b Mon Sep 17 00:00:00 2001 From: jinningwang Date: Tue, 28 Nov 2023 11:33:02 -0500 Subject: [PATCH 70/77] Typo --- tests/test_routine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_routine.py b/tests/test_routine.py index a834f775..7d317bc1 100644 --- a/tests/test_routine.py +++ b/tests/test_routine.py @@ -90,7 +90,7 @@ def test_Init(self): f"{rtn.class_name} initialization failed!") -@unittest.skipUnless(HAVE_IGRAPH, "igaph not available") +@unittest.skipUnless(HAVE_IGRAPH, "igraph not available") class TestRoutineGraph(unittest.TestCase): """ Test routine graph. From 246e6e34064ed120d427cb3c815cc0882a919fd6 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Tue, 28 Nov 2023 12:50:40 -0500 Subject: [PATCH 71/77] Doc --- ams/core/documenter.py | 20 +++++++++++++------- ams/core/symprocessor.py | 10 +++++++--- ams/routines/dcopf.py | 6 +++--- ams/routines/dopf.py | 6 ++++-- ams/routines/ed.py | 8 ++++---- ams/routines/rted.py | 10 +++++----- docs/source/conf.py | 2 +- 7 files changed, 37 insertions(+), 25 deletions(-) diff --git a/ams/core/documenter.py b/ams/core/documenter.py index 261067ee..c8f4b2e2 100644 --- a/ams/core/documenter.py +++ b/ams/core/documenter.py @@ -498,18 +498,24 @@ def _tex_pre(docm, p, tex_map): """ # NOTE: in the future, there might occur special math symbols - map_before = { - 'sum': 'SUM', - r'\sum': 'SUM', - r'\eta': 'ETA', - r'\gamma': 'GAMMA', - r'\frac': 'FRAC', - } + map_before = OrderedDict([ + ('sum', 'SUM'), + (r'\sum', 'SUM'), + (r'\eta', 'ETA'), + (r'\gamma', 'GAMMA'), + (r'\theta', 'THETA'), + (r'\frac', 'FRAC'), + (r'\overline', 'OVERLINE'), + (r'\underline', 'UNDERLINE'), + ]) map_post = OrderedDict([ ('SUM', r'\sum'), + ('THETA', r'\theta'), ('ETA', r'\eta'), ('GAMMA', r'\gamma'), ('FRAC', r'\frac'), + ('OVERLINE', r'\overline'), + ('UNDERLINE', r'\underline'), ]) expr = p.e_str diff --git a/ams/core/symprocessor.py b/ams/core/symprocessor.py index 14d2685f..c3e7d27b 100644 --- a/ams/core/symprocessor.py +++ b/ams/core/symprocessor.py @@ -4,10 +4,10 @@ This module is revised from ``andes.core.symprocessor``. """ -import logging # NOQA -from collections import OrderedDict # NOQA +import logging +from collections import OrderedDict -import sympy as sp # NOQA +import sympy as sp logger = logging.getLogger(__name__) @@ -87,9 +87,13 @@ def __init__(self, parent): (r'\b(\w+)\s*\*\s*(\w+)\b', r'\1 \2'), (r'\@', r' '), (r'dot', r' '), + (r'sum_squares\((.*?)\)', r"SUM((\1))^2"), (r'multiply\(([^,]+), ([^)]+)\)', r'\1 \2'), (r'\bnp.linalg.pinv(\d+)', r'\1^{\-1}'), (r'\bpos\b', 'F^{+}'), + (r'mul\((.*?),\s*(.*?)\)', r'\1 \2'), + (r'\bmul\b\((.*?),\s*(.*?)\)', r'\1 \2'), + (r'\bsum\b', 'SUM'), ]) self.status = { diff --git a/ams/routines/dcopf.py b/ams/routines/dcopf.py index 7127267c..b4fe5495 100644 --- a/ams/routines/dcopf.py +++ b/ams/routines/dcopf.py @@ -53,11 +53,11 @@ def __init__(self, system, config): no_parse=True) # --- generator --- self.pmax = RParam(info='Gen maximum active power', - name='pmax', tex_name=r'p_{max}', + name='pmax', tex_name=r'p_{g, max}', unit='p.u.', model='StaticGen', no_parse=False,) self.pmin = RParam(info='Gen minimum active power', - name='pmin', tex_name=r'p_{min}', + name='pmin', tex_name=r'p_{g, min}', unit='p.u.', model='StaticGen', no_parse=False,) self.pg0 = RParam(info='Gen initial active power', @@ -97,7 +97,7 @@ def __init__(self, system, config): model='mats', src='Cft', no_parse=True,) self.PTDF = RParam(info='Power Transfer Distribution Factor', - name='PTDF', tex_name=r'PTDF', + name='PTDF', tex_name=r'P_{TDF}', model='mats', src='PTDF', no_parse=True,) diff --git a/ams/routines/dopf.py b/ams/routines/dopf.py index 6a97a1c9..298e453c 100644 --- a/ams/routines/dopf.py +++ b/ams/routines/dopf.py @@ -31,11 +31,13 @@ def __init__(self, system, config): name='qd', tex_name=r'q_{d}', unit='p.u.', model='StaticLoad', src='q0',) self.vmax = RParam(info="Bus voltage upper limit", - name='vmax', tex_name=r'v_{max}', unit='p.u.', + name='vmax', tex_name=r'\overline{v}', + unit='p.u.', model='Bus', src='vmax', no_parse=True, ) self.vmin = RParam(info="Bus voltage lower limit", - name='vmin', tex_name=r'v_{min}', unit='p.u.', + name='vmin', tex_name=r'\underline{v}', + unit='p.u.', model='Bus', src='vmin', no_parse=True,) self.r = RParam(info='line resistance', name='r', tex_name='r', unit='p.u.', diff --git a/ams/routines/ed.py b/ams/routines/ed.py index 840bc582..af647a79 100644 --- a/ams/routines/ed.py +++ b/ams/routines/ed.py @@ -111,11 +111,11 @@ class ED(RTED): ED extends DCOPF as follows: - 1. Vars `pg`, `pru`, `prd` are extended to 2D + 1. Vars ``pg``, ``pru``, ``prd`` are extended to 2D - 2. 2D Vars `rgu` and `rgd` are introduced + 2. 2D Vars ``rgu`` and ``rgd`` are introduced - 3. Param `ug` is sourced from `EDTSlot`.`ug` as commitment decisions + 3. Param ``ug`` is sourced from ``EDTSlot.ug`` as commitment decisions Notes ----- @@ -141,7 +141,7 @@ def __init__(self, system, config): self.ug.info = 'unit commitment decisions' self.ug.model = 'EDTSlot' self.ug.src = 'ug' - self.ug.tex_name = r'u_{g}^{T}', + # self.ug.tex_name = r'u_{g}', self.dud.expand_dims = 1 self.ddd.expand_dims = 1 diff --git a/ams/routines/rted.py b/ams/routines/rted.py index 0640eedd..57c1445c 100644 --- a/ams/routines/rted.py +++ b/ams/routines/rted.py @@ -284,13 +284,13 @@ def __init__(self): name='En', src='En', tex_name='E_n', unit='MWh', model='ESD1', no_parse=True,) - self.SOCmin = RParam(info='Minimum required value for SOC in limiter', - name='SOCmin', src='SOCmin', - tex_name='SOC_{min}', unit='%', - model='ESD1',) self.SOCmax = RParam(info='Maximum allowed value for SOC in limiter', name='SOCmax', src='SOCmax', - tex_name='SOC_{max}', unit='%', + tex_name=r'SOC_{max}', unit='%', + model='ESD1',) + self.SOCmin = RParam(info='Minimum required value for SOC in limiter', + name='SOCmin', src='SOCmin', + tex_name=r'SOC_{min}', unit='%', model='ESD1',) self.SOCinit = RParam(info='Initial state of charge', name='SOCinit', src='SOCinit', diff --git a/docs/source/conf.py b/docs/source/conf.py index d6a17b89..7027c190 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -186,7 +186,7 @@ # import and execute model reference generation script shutil.rmtree("_examples", ignore_errors=True) shutil.copytree("../../examples", "_examples", ) -# shutil.rmtree("_examples/demonstration") +shutil.rmtree("_examples/demonstration") jupyter_execute_notebooks = "off" From 55fea4815a167c2761dffba5e1697f5c9286a4c9 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Tue, 28 Nov 2023 20:05:36 -0500 Subject: [PATCH 72/77] Fix pglb and pgub --- ams/routines/dcopf.py | 18 +++++++++++++----- ams/routines/ed.py | 12 +++++++----- ams/routines/uc.py | 16 ++++++++++------ 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/ams/routines/dcopf.py b/ams/routines/dcopf.py index b4fe5495..e311630d 100644 --- a/ams/routines/dcopf.py +++ b/ams/routines/dcopf.py @@ -5,7 +5,7 @@ import numpy as np from ams.core.param import RParam -from ams.core.service import NumOp +from ams.core.service import NumOp, NumOpDual from ams.routines.routine import RoutineModel @@ -33,10 +33,18 @@ def __init__(self, system, config): name='ctrl', tex_name=r'c_{trl}', model='StaticGen', src='ctrl', no_parse=True) + self.ctrle = NumOpDual(info='Effective Gen controllability', + name='ctrle', tex_name=r'c_{trl, e}', + u=self.ctrl, u2=self.ug, + fun=np.multiply, no_parse=True) self.nctrl = NumOp(u=self.ctrl, fun=np.logical_not, - name='nctrl', tex_name=r'-c_{trl}', - info='gen uncontrollability', + name='nctrl', tex_name=r'c_{trl,n}', + info='Effective Gen uncontrollability', no_parse=True,) + self.nctrle = NumOpDual(info='Effective Gen uncontrollability', + name='nctrle', tex_name=r'c_{trl,n,e}', + u=self.nctrl, u2=self.ug, + fun=np.multiply, no_parse=True) self.c2 = RParam(info='Gen cost coefficient 2', name='c2', tex_name=r'c_{2}', unit=r'$/(p.u.^2)', model='GCost', @@ -207,10 +215,10 @@ def __init__(self, system, config): model='StaticGen', src='p', v0=self.pg0) # NOTE: `ug*pmin` results in unexpected error - pglb = '-pg + mul(nctrl, pg0) + mul(ctrl, mul(ug, pmin))' + pglb = '-pg + mul(nctrle, pg0) + mul(ctrle, pmin)' self.pglb = Constraint(name='pglb', info='pg min', e_str=pglb, type='uq',) - pgub = 'pg - mul(nctrl, pg0) - mul(ctrl, mul(ug, pmax))' + pgub = 'pg - mul(nctrle, pg0) - mul(ctrle, pmax)' self.pgub = Constraint(name='pgub', info='pg max', e_str=pgub, type='uq',) diff --git a/ams/routines/ed.py b/ams/routines/ed.py index af647a79..88b4e8f2 100644 --- a/ams/routines/ed.py +++ b/ams/routines/ed.py @@ -78,7 +78,7 @@ def __init__(self) -> None: self.tlv = NumOp(u=self.timeslot, fun=np.ones_like, args=dict(dtype=float), expand_dims=0, - name='tlv', tex_name=r'v_{tl}', + name='tlv', tex_name=r'1_{tl}', info='time length vector', no_parse=True) @@ -156,11 +156,13 @@ def __init__(self, system, config): # NOTE: extend pg to 2D matrix, where row is gen and col is timeslot self.pg.horizon = self.timeslot self.pg.info = '2D Gen power' - pglb = '-pg + mul(mul(nctrl, pg0), tlv) ' - pglb += '+ mul(mul(ctrl, pmin), tlv)' + self.ctrle.u2 = self.ugt + self.nctrle.u2 = self.ugt + pglb = '-pg + mul(mul(nctrle, pg0), tlv) ' + pglb += '+ mul(mul(ctrle, tlv), pmin)' self.pglb.e_str = pglb - pgub = 'pg - mul(mul(nctrl, pg0), tlv) ' - pgub += '- mul(mul(ctrl, pmax), tlv)' + pgub = 'pg - mul(mul(nctrle, pg0), tlv) ' + pgub += '- mul(mul(ctrle, tlv), pmax)' self.pgub.e_str = pgub self.plf.horizon = self.timeslot diff --git a/ams/routines/uc.py b/ams/routines/uc.py index c2e39df7..0b2d4435 100644 --- a/ams/routines/uc.py +++ b/ams/routines/uc.py @@ -139,11 +139,16 @@ def __init__(self, system, config): self.prns.horizon = self.timeslot self.prns.info = '2D Non-spinning reserve' - pglb = '-pg + mul(mul(nctrl, pg0), tlv) ' - pglb += '+ mul(mul(ctrl, pmin), tlv)' + # TODO: havn't test non-controllability? + self.ctrle.u2 = self.tlv + self.ctrle.info = 'Reshaped controllability' + self.nctrle.u2 = self.tlv + self.nctrle.info = 'Reshaped non-controllability' + pglb = '-pg + mul(mul(nctrl, pg0), ugd)' + pglb += '+ mul(mul(ctrl, pmin), ugd)' self.pglb.e_str = pglb - pgub = 'pg - mul(mul(nctrl, pg0), tlv) ' - pgub += '- mul(mul(ctrl, pmax), tlv)' + pgub = 'pg - mul(mul(nctrl, pg0), ugd)' + pgub += '- mul(mul(ctrl, pmax), ugd)' self.pgub.e_str = pgub # --- vars --- @@ -189,7 +194,6 @@ def __init__(self, system, config): # spinning reserve self.prsb.e_str = 'mul(ugd, mul(pmax, tlv)) - zug - prs' # spinning reserve requirement - # TODO: rsr eqn is not correct, need to fix self.rsr.e_str = '-gs@prs + dsr' # non-spinning reserve @@ -272,7 +276,7 @@ def _initial_guess(self): g_idx = priority[0] ug0 = 0 self.system.StaticGen.set(src='u', attr='v', idx=g_idx, value=ug0) - logger.warning(f'Turn off StaticGen {g_idx} as initial guess for commitment.') + logger.warning(f'Turn off StaticGen {g_idx} as initial commitment guess.') return True def init(self, **kwargs): From 9104f5b9c50b4c1a7811bd809ac7cbe9e21674c2 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Tue, 28 Nov 2023 21:39:50 -0500 Subject: [PATCH 73/77] Typo --- ams/routines/rted.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ams/routines/rted.py b/ams/routines/rted.py index 57c1445c..b463830a 100644 --- a/ams/routines/rted.py +++ b/ams/routines/rted.py @@ -292,7 +292,7 @@ def __init__(self): name='SOCmin', src='SOCmin', tex_name=r'SOC_{min}', unit='%', model='ESD1',) - self.SOCinit = RParam(info='Initial state of charge', + self.SOCinit = RParam(info='Initial SOC', name='SOCinit', src='SOCinit', tex_name=r'SOC_{init}', unit='%', model='ESD1',) @@ -323,7 +323,7 @@ def __init__(self): array_out=False,) # --- vars --- - self.SOC = Var(info='ESD1 SOC', unit='%', + self.SOC = Var(info='ESD1 State of Charge', unit='%', name='SOC', tex_name=r'SOC', model='ESD1', pos=True, v0=self.SOCinit,) From 47c1ae33bf79f4f033c069f4cbe4ecf0d1ea793c Mon Sep 17 00:00:00 2001 From: jinningwang Date: Tue, 28 Nov 2023 21:40:06 -0500 Subject: [PATCH 74/77] Fix cases --- ams/cases/5bus/pjm5bus_uced.json | 72 ++++++++++++++++--------- ams/cases/5bus/pjm5bus_uced_esd1.xlsx | Bin 0 -> 26711 bytes ams/cases/ieee39/ieee39_uced_esd1.xlsx | Bin 39261 -> 39265 bytes 3 files changed, 48 insertions(+), 24 deletions(-) create mode 100644 ams/cases/5bus/pjm5bus_uced_esd1.xlsx diff --git a/ams/cases/5bus/pjm5bus_uced.json b/ams/cases/5bus/pjm5bus_uced.json index 6b7baeb4..2f997f79 100644 --- a/ams/cases/5bus/pjm5bus_uced.json +++ b/ams/cases/5bus/pjm5bus_uced.json @@ -722,145 +722,169 @@ "idx": "EDT1", "u": 1.0, "name": "EDT1", - "sd": "0.793,0.0" + "sd": "0.793,0.0", + "ug": "1,1,1,1" }, { "idx": "EDT2", "u": 1.0, "name": "EDT2", - "sd": "0.756,0.0" + "sd": "0.756,0.0", + "ug": "1,1,1,1" }, { "idx": "EDT3", "u": 1.0, "name": "EDT3", - "sd": "0.723,0.0" + "sd": "0.723,0.0", + "ug": "1,1,1,1" }, { "idx": "EDT4", "u": 1.0, "name": "EDT4", - "sd": "0.708,0.0" + "sd": "0.708,0.0", + "ug": "1,1,1,1" }, { "idx": "EDT5", "u": 1.0, "name": "EDT5", - "sd": "0.7,0.0" + "sd": "0.7,0.0", + "ug": "1,1,1,1" }, { "idx": "EDT6", "u": 1.0, "name": "EDT6", - "sd": "0.706,0.0" + "sd": "0.706,0.0", + "ug": "1,1,1,1" }, { "idx": "EDT7", "u": 1.0, "name": "EDT7", - "sd": "0.75,0.0" + "sd": "0.75,0.0", + "ug": "1,1,1,1" }, { "idx": "EDT8", "u": 1.0, "name": "EDT8", - "sd": "0.802,0.0" + "sd": "0.802,0.0", + "ug": "1,1,1,1" }, { "idx": "EDT9", "u": 1.0, "name": "EDT9", - "sd": "0.828,0.0" + "sd": "0.828,0.0", + "ug": "1,1,1,1" }, { "idx": "EDT10", "u": 1.0, "name": "EDT10", - "sd": "0.851,0.0" + "sd": "0.851,0.0", + "ug": "1,1,1,1" }, { "idx": "EDT11", "u": 1.0, "name": "EDT11", - "sd": "0.874,0.0" + "sd": "0.874,0.0", + "ug": "1,1,1,1" }, { "idx": "EDT12", "u": 1.0, "name": "EDT12", - "sd": "0.898,0.0" + "sd": "0.898,0.0", + "ug": "1,1,1,1" }, { "idx": "EDT13", "u": 1.0, "name": "EDT13", - "sd": "0.919,0.0" + "sd": "0.919,0.0", + "ug": "1,1,1,1" }, { "idx": "EDT14", "u": 1.0, "name": "EDT14", - "sd": "0.947,0.0" + "sd": "0.947,0.0", + "ug": "1,1,1,1" }, { "idx": "EDT15", "u": 1.0, "name": "EDT15", - "sd": "0.97,0.0" + "sd": "0.97,0.0", + "ug": "1,1,1,1" }, { "idx": "EDT16", "u": 1.0, "name": "EDT16", - "sd": "0.987,0.0" + "sd": "0.987,0.0", + "ug": "1,1,1,1" }, { "idx": "EDT17", "u": 1.0, "name": "EDT17", - "sd": "1.0,0.0" + "sd": "1.0,0.0", + "ug": "1,1,1,1" }, { "idx": "EDT18", "u": 1.0, "name": "EDT18", - "sd": "1.0,0.0" + "sd": "1.0,0.0", + "ug": "1,1,1,1" }, { "idx": "EDT19", "u": 1.0, "name": "EDT19", - "sd": "0.991,0.0" + "sd": "0.991,0.0", + "ug": "1,1,1,1" }, { "idx": "EDT20", "u": 1.0, "name": "EDT20", - "sd": "0.956,0.0" + "sd": "0.956,0.0", + "ug": "1,1,1,1" }, { "idx": "EDT21", "u": 1.0, "name": "EDT21", - "sd": "0.93,0.0" + "sd": "0.93,0.0", + "ug": "1,1,1,1" }, { "idx": "EDT22", "u": 1.0, "name": "EDT22", - "sd": "0.905,0.0" + "sd": "0.905,0.0", + "ug": "1,1,1,1" }, { "idx": "EDT23", "u": 1.0, "name": "EDT23", - "sd": "0.849,0.0" + "sd": "0.849,0.0", + "ug": "1,1,1,1" }, { "idx": "EDT24", "u": 1.0, "name": "EDT24", - "sd": "0.784,0.0" + "sd": "0.784,0.0", + "ug": "1,1,1,1" } ], "UCTSlot": [ diff --git a/ams/cases/5bus/pjm5bus_uced_esd1.xlsx b/ams/cases/5bus/pjm5bus_uced_esd1.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..b0a1926dc8eec065c8e83be631fca0f35abe1df8 GIT binary patch literal 26711 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°f^SgC zZ`<;3Dt^l^ZJ6f`?Vd|mP_XgB>jTKdVHksR!xei6$KV3sX=DHSD1A#ag{*J^1w=u> z8o)v>2r)4i5JdlNjUW&L^xt8RVwx5Y{6Qj2lp$gvS5=tkN5q2hKt9nBqN~Xic8Nmh zzn2vRLjHFvQ0qZcK8SsmqyQ;1WMcdQsv9$Lkc0)F1iu#VQqE8SD+4YWy5M=BC_@v>56orgL5hNzlj@lYkm1ljm=*?Q yu6!TC%lY?MZzGsCS|%181Z-tW2>&TPb1(?xcJNa0{q-FX2~f_$u%grci2nn^4)1>e delta 2664 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 Date: Tue, 28 Nov 2023 22:10:48 -0500 Subject: [PATCH 75/77] Update examples --- docs/source/release-notes.rst | 8 + examples/.placeholder | 0 examples/demonstration/1. ESD1.ipynb | 820 +++++++++---------- examples/ex1.ipynb | 497 ++++-------- examples/ex2.ipynb | 1129 +++++++++++++++++++------- requirements.txt | 3 +- 6 files changed, 1390 insertions(+), 1067 deletions(-) delete mode 100644 examples/.placeholder diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index c6456808..ddf55835 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -9,6 +9,14 @@ The APIs before v3.0.0 are in beta and may change without prior notice. Pre-v1.0.0 ========== +v0.7.4 (2023-11-29) +------------------- + +- Refactor routins and optimization models to improve performance +- Fix routines modeling +- Add examples +- Fix built-in cases + v0.7.3 (2023-11-3) ------------------- diff --git a/examples/.placeholder b/examples/.placeholder deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/demonstration/1. ESD1.ipynb b/examples/demonstration/1. ESD1.ipynb index 7441f09e..7e86af06 100644 --- a/examples/demonstration/1. ESD1.ipynb +++ b/examples/demonstration/1. ESD1.ipynb @@ -8,19 +8,27 @@ "# Dispatch with Energy Storage" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this case, we will show the usage of energy storage included dispatch.\n", + "\n", + "In AMS, ``ESD1`` is an dispatch model for energy storage, which has a corresponding\n", + "dynamic model ``ESD1`` in ANDES." + ] + }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ - "import ams\n", + "import pandas as pd\n", "\n", - "import datetime\n", - "\n", - "import numpy as np\n", + "import ams\n", "\n", - "import cvxpy as cp" + "import datetime" ] }, { @@ -32,8 +40,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "Last run time: 2023-11-10 00:12:54\n", - "ams:0.7.3.post42.dev0+gd7a5b95\n" + "Last run time: 2023-11-28 22:03:14\n", + "ams:0.7.3.post74.dev0+g47c1ae3\n" ] } ], @@ -49,7 +57,14 @@ "metadata": {}, "outputs": [], "source": [ - "ams.config_logger(stream_level=10)" + "ams.config_logger(stream_level=20)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A small-size PJM 5-bus case with ESD1 is used in this example." ] }, { @@ -61,56 +76,113 @@ "name": "stderr", "output_type": "stream", "text": [ - "Input format guessed as xlsx.\n", - "Parsing input file \"/Users/jinningwang/Documents/work/ams/ams/cases/ieee39/ieee39_uced_esd1_t2.xlsx\"...\n", - "Input file parsed in 0.1132 seconds.\n", - "System set up in 0.0038 seconds.\n" + "Parsing input file \"/Users/jinningwang/Documents/work/ams/ams/cases/5bus/pjm5bus_uced_esd1.xlsx\"...\n", + "Input file parsed in 0.1205 seconds.\n", + "System set up in 0.0031 seconds.\n" ] } ], "source": [ - "sp = ams.load(ams.get_case('ieee39/ieee39_uced_esd1_t2.xlsx'),\n", + "sp = ams.load(ams.get_case('5bus/pjm5bus_uced_esd1.xlsx'),\n", " setup=True,)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The model information can be inspected as follow." + ] + }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "RTED2 data check passed.\n", - "- Generating symbols for RTED2\n", - "Set constrs pb: sum(pl) - sum(pg) == 0\n", - "Set constrs pinj: CftT@plf - pl - pn == 0\n", - "Set constrs lub: PTDF @ (pn - pl) - rate_a <= 0\n", - "Set constrs llb: - PTDF @ (pn - pl) - rate_a <= 0\n", - "Set constrs rbu: gs @ mul(ug, pru) - dud == 0\n", - "Set constrs rbd: gs @ mul(ug, prd) - ddd == 0\n", - "Set constrs rru: mul(ug, pg + pru) - pmax <= 0\n", - "Set constrs rrd: mul(ug, -pg + prd) - pmin <= 0\n", - "Set constrs rgu: mul(ug, pg-pg0-R10) <= 0\n", - "Set constrs rgd: mul(ug, -pg+pg0-R10) <= 0\n", - "Set constrs ceb: uce + ude - 1 == 0\n", - "Set constrs cpe: ce @ pg - zce - zde == 0\n", - "Set constrs zce1: -zce + pce <= 0\n", - "Set constrs zce2: zce - pce - Mb dot (1-uce) <= 0\n", - "Set constrs zce3: zce - Mb dot uce <= 0\n", - "Set constrs zde1: -zde + pde <= 0\n", - "Set constrs zde2: zde - pde - Mb dot (1-ude) <= 0\n", - "Set constrs zde3: zde - Mb dot ude <= 0\n", - "Set constrs SOCb: mul(En, (SOC - SOCinit)) - t dot mul(EtaC, zce)+ t dot mul(REtaD, zde) == 0\n", - "Set obj tc: min. sum(c2 @ (t dot pg)**2) + sum(c1 @ (t dot pg)) + ug * c0 + sum(cru * pru + crd * prd)\n", - "Routine initialized in 0.0559 seconds.\n" - ] - }, { "data": { + "text/html": [ + "

\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idxunamebusgenSngammapgammaqSOCminSOCmaxSOCinitEnEtaCEtaD
uid
0ESD1_11.0ESD1_10PV_1100.01.01.00.01.00.2100.01.01.0
\n", + "
" + ], "text/plain": [ - "True" + " idx u name bus gen Sn gammap gammaq SOCmin SOCmax \\\n", + "uid \n", + "0 ESD1_1 1.0 ESD1_1 0 PV_1 100.0 1.0 1.0 0.0 1.0 \n", + "\n", + " SOCinit En EtaC EtaD \n", + "uid \n", + "0 0.2 100.0 1.0 1.0 " ] }, "execution_count": 5, @@ -119,7 +191,20 @@ } ], "source": [ - "sp.RTED2.init()" + "sp.ESD1.as_df()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`RTEDES` is the RTED model extended to include energy storage.\n", + "\n", + "Note that mixed integer linear programming (MILP) requires\n", + "capable solvers such as Gurobi or CPLEX.\n", + "They might require license and installation.\n", + "\n", + "More details can be found at [CVXPY - Choosing a solver](https://www.cvxpy.org/tutorial/advanced/index.html#choosing-a-solver)." ] }, { @@ -131,7 +216,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "RTED2 has already been initialized.\n" + "Routine initialized in 0.0140 seconds.\n" ] }, { @@ -146,7 +231,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "RTED2 solved as optimal in 0.0793 seconds, converged after 33 iterations using solver GUROBI.\n" + "RTEDES solved as optimal in 0.0341 seconds, converged after 0 iteration using solver GUROBI.\n" ] }, { @@ -161,228 +246,137 @@ } ], "source": [ - "sp.RTED2.run(solver='GUROBI')" + "sp.RTEDES.run(solver='GUROBI', reoptimize=True)" ] }, { - "cell_type": "code", - "execution_count": 7, + "cell_type": "markdown", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "pg=[6.01822025 5.99427957 5.97043409 5.08 5.98234496 5.8\n", - " 5.64 6.00623798 6.03022646 6.04225669]\n", - "uce=[0. 0.]\n", - "pce=[0. 0.]\n", - "zce=[0. 0.]\n", - "ude=[1. 1.]\n", - "pde=[3.00911012 3.00911012]\n", - "zde=[3.00911012 3.00911012]\n", - "SOC=[0.49749241 0.49749241]\n", - "obj=592.8203580808986\n" - ] - } - ], "source": [ - "print(f\"pg={sp.RTED2.pg.v}\")\n", - "\n", - "print(f\"uce={sp.RTED2.uce.v}\")\n", - "print(f\"pce={sp.RTED2.pce.v}\")\n", - "print(f\"zce={sp.RTED2.zce.v}\")\n", - "\n", - "print(f\"ude={sp.RTED2.ude.v}\")\n", - "print(f\"pde={sp.RTED2.pde.v}\")\n", - "print(f\"zde={sp.RTED2.zde.v}\")\n", - "\n", - "print(f\"SOC={sp.RTED2.SOC.v}\")\n", - "print(f\"obj={sp.RTED2.obj.v}\")" + "Note that, in RTED, the time interval is 5/60 [H] by default, and the\n", + "dispatch model has been adjusted accordingly." ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 7, "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "RTED2 data check passed.\n", - "Set constrs pb: sum(pl) - sum(pg) == 0\n", - "Set constrs pinj: CftT@plf - pl - pn == 0\n", - "Set constrs lub: PTDF @ (pn - pl) - rate_a <= 0\n", - "Set constrs llb: - PTDF @ (pn - pl) - rate_a <= 0\n", - "Set constrs rbu: gs @ mul(ug, pru) - dud == 0\n", - "Set constrs rbd: gs @ mul(ug, prd) - ddd == 0\n", - "Set constrs rru: mul(ug, pg + pru) - pmax <= 0\n", - "Set constrs rrd: mul(ug, -pg + prd) - pmin <= 0\n", - "Set constrs rgu: mul(ug, pg-pg0-R10) <= 0\n", - "Set constrs rgd: mul(ug, -pg+pg0-R10) <= 0\n", - "Set constrs ceb: uce + ude - 1 == 0\n", - "Set constrs cpe: ce @ pg - zce - zde == 0\n", - "Set constrs zce1: -zce + pce <= 0\n", - "Set constrs zce2: zce - pce - Mb dot (1-uce) <= 0\n", - "Set constrs zce3: zce - Mb dot uce <= 0\n", - "Set constrs zde1: -zde + pde <= 0\n", - "Set constrs zde2: zde - pde - Mb dot (1-ude) <= 0\n", - "Set constrs zde3: zde - Mb dot ude <= 0\n", - "Set constrs SOCb: mul(En, (SOC - SOCinit)) - t dot mul(EtaC, zce)+ t dot mul(REtaD, zde) == 0\n", - "Set obj tc: min. sum(c2 @ (t dot pg)**2) + sum(c1 @ (t dot pg)) + ug * c0 + sum(cru * pru + crd * prd)\n", - "Routine initialized in 0.0663 seconds.\n", - "RTED2 solved as optimal in 0.0754 seconds, converged after 33 iterations using solver GUROBI.\n" - ] - }, { "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
VarValueinfo
0uce[0.0]ESD1 charging decision
1ude[1.0]ESD1 discharging decision
2pce[0.0]ESD1 charging power
3pde[2.1]ESD1 discharging power
4SOC[0.1982]ESD1 State of Charge
5SOCinit[0.2]Initial SOC
\n", + "
" + ], "text/plain": [ - "True" + " Var Value info\n", + "0 uce [0.0] ESD1 charging decision\n", + "1 ude [1.0] ESD1 discharging decision\n", + "2 pce [0.0] ESD1 charging power\n", + "3 pde [2.1] ESD1 discharging power\n", + "4 SOC [0.1982] ESD1 State of Charge\n", + "5 SOCinit [0.2] Initial SOC" ] }, - "execution_count": 8, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "sp.RTED2.set(src='c1', attr='v', idx='GCost_1', value=999)\n", - "sp.RTED2.run(force_init=True, solver='GUROBI')" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "pg=[0. 7.13724518 6.52 5.08 6.87 5.8\n", - " 5.64 7.15034313 7.17661754 7.18979416]\n", - "uce=[0. 0.]\n", - "pce=[0. 0.]\n", - "zce=[0. 0.]\n", - "ude=[1. 1.]\n", - "pde=[0. 0.]\n", - "zde=[0. 0.]\n", - "SOC=[0.5 0.5]\n", - "obj=622.4342380817811\n" - ] - } - ], - "source": [ - "print(f\"pg={sp.RTED2.pg.v}\")\n", + "RTEDESres = pd.DataFrame()\n", "\n", - "print(f\"uce={sp.RTED2.uce.v}\")\n", - "print(f\"pce={sp.RTED2.pce.v}\")\n", - "print(f\"zce={sp.RTED2.zce.v}\")\n", + "items = [sp.RTEDES.uce, sp.RTEDES.ude,\n", + " sp.RTEDES.pce, sp.RTEDES.pde,\n", + " sp.RTEDES.SOC, sp.RTEDES.SOCinit]\n", "\n", - "print(f\"ude={sp.RTED2.ude.v}\")\n", - "print(f\"pde={sp.RTED2.pde.v}\")\n", - "print(f\"zde={sp.RTED2.zde.v}\")\n", + "RTEDESres['Var'] = [item.name for item in items]\n", + "RTEDESres['Value'] = [item.v.round(4) for item in items]\n", + "RTEDESres['info'] = [item.info for item in items]\n", "\n", - "print(f\"SOC={sp.RTED2.SOC.v}\")\n", - "print(f\"obj={sp.RTED2.obj.v}\")" + "RTEDESres" ] }, { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Input format guessed as xlsx.\n", - "Parsing input file \"/Users/jinningwang/Documents/work/ams/ams/cases/ieee39/ieee39_uced_esd1_t2.xlsx\"...\n", - "Input file parsed in 0.0892 seconds.\n", - "System set up in 0.0100 seconds.\n" - ] - } - ], - "source": [ - "sp = ams.load(ams.get_case('ieee39/ieee39_uced_esd1_t2.xlsx'),\n", - " setup=True,)" - ] - }, - { - "cell_type": "code", - "execution_count": 11, + "cell_type": "markdown", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "NumOp: ED2.REtaD, v in shape of (2,)" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ - "sp.ED2.REtaD" + "Similarly, multi-period dispatch ``EDES`` and ``UCES`` are also available.\n", + "They have 1 [H] time interval by default." ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "ED2 data check passed.\n", - "- Generating symbols for ED2\n", - "Set constrs pb: - gs @ pg + pds == 0\n", - "Code Constr: om.constrs[\"pb\"]=- self.om.rtn.gs.v @ self.om.pg + self.om.rtn.pds.v == 0\n", - "Set constrs pinj: Cg @ (pn - Rpd) - pg == 0\n", - "Code Constr: om.constrs[\"pinj\"]=self.om.rtn.Cg.v @ (self.om.pn - self.om.rtn.Rpd.v) - self.om.pg == 0\n", - "Set constrs lub: PTDF @ (pn - Rpd) - RRA <= 0\n", - "Code Constr: om.constrs[\"lub\"]=self.om.rtn.PTDF.v @ (self.om.pn - self.om.rtn.Rpd.v) - self.om.rtn.RRA.v <= 0\n", - "Set constrs llb: -PTDF @ (pn - Rpd) - RRA <= 0\n", - "Code Constr: om.constrs[\"llb\"]=-self.om.rtn.PTDF.v @ (self.om.pn - self.om.rtn.Rpd.v) - self.om.rtn.RRA.v <= 0\n", - "Set constrs sr: -gs@multiply(Rpmax - pg, Rug) + dsr <= 0\n", - "Code Constr: om.constrs[\"sr\"]=-self.om.rtn.gs.v@cp.multiply(self.om.rtn.Rpmax.v - self.om.pg, self.om.rtn.Rug.v) + self.om.rtn.dsr.v <= 0\n", - "Set constrs rgu: pg @ Mr - t dot RR30 <= 0\n", - "Code Constr: om.constrs[\"rgu\"]=self.om.pg @ self.om.rtn.Mr.v - self.rtn.config.t * self.om.rtn.RR30.v <= 0\n", - "Set constrs rgd: -pg @ Mr - t dot RR30 <= 0\n", - "Code Constr: om.constrs[\"rgd\"]=-self.om.pg @ self.om.rtn.Mr.v - self.rtn.config.t * self.om.rtn.RR30.v <= 0\n", - "Set constrs rgu0: pg[:, 0] - pg0 - R30 <= 0\n", - "Code Constr: om.constrs[\"rgu0\"]=self.om.pg[:, 0] - self.om.rtn.pg0.v - self.om.rtn.R30.v <= 0\n", - "Set constrs rgd0: - pg[:, 0] + pg0 - R30 <= 0\n", - "Code Constr: om.constrs[\"rgd0\"]=- self.om.pg[:, 0] + self.om.rtn.pg0.v - self.om.rtn.R30.v <= 0\n", - "Set constrs ceb: uce + ude - 1 == 0\n", - "Code Constr: om.constrs[\"ceb\"]=self.om.uce + self.om.ude - 1 == 0\n", - "Set constrs cpe: ce @ pg - zce - zde == 0\n", - "Code Constr: om.constrs[\"cpe\"]=self.om.rtn.ce.v @ self.om.pg - self.om.zce - self.om.zde == 0\n", - "Set constrs zce1: -zce + pce <= 0\n", - "Code Constr: om.constrs[\"zce1\"]=-self.om.zce + self.om.pce <= 0\n", - "Set constrs zce2: zce - pce - Mb dot (1-uce) <= 0\n", - "Code Constr: om.constrs[\"zce2\"]=self.om.zce - self.om.pce - self.om.rtn.Mb.v * (1-self.om.uce) <= 0\n", - "Set constrs zce3: zce - Mb dot uce <= 0\n", - "Code Constr: om.constrs[\"zce3\"]=self.om.zce - self.om.rtn.Mb.v * self.om.uce <= 0\n", - "Set constrs zde1: -zde + pde <= 0\n", - "Code Constr: om.constrs[\"zde1\"]=-self.om.zde + self.om.pde <= 0\n", - "Set constrs zde2: zde - pde - Mb dot (1-ude) <= 0\n", - "Code Constr: om.constrs[\"zde2\"]=self.om.zde - self.om.pde - self.om.rtn.Mb.v * (1-self.om.ude) <= 0\n", - "Set constrs zde3: zde - Mb dot ude <= 0\n", - "Code Constr: om.constrs[\"zde3\"]=self.om.zde - self.om.rtn.Mb.v * self.om.ude <= 0\n", - "Set constrs SOCb: mul(EnR, SOC @ Mre) - t dot mul(EtaCR, zce[:, 1:]) + t dot mul(REtaDR, zde[:, 1:]) == 0\n", - "Code Constr: om.constrs[\"SOCb\"]=cp.multiply(self.om.rtn.EnR.v, self.om.SOC @ self.om.rtn.Mre.v) - self.rtn.config.t * cp.multiply(self.om.rtn.EtaCR.v, self.om.zce[:, 1:]) + self.rtn.config.t * cp.multiply(self.om.rtn.REtaDR.v, self.om.zde[:, 1:]) == 0\n", - "Set constrs SOCb: mul(En, SOC[:, 0] - SOCinit) - t dot mul(EtaC, zce[:, 0]) + t dot mul(REtaD, zde[:, 0]) == 0\n", - "Code Constr: om.constrs[\"SOCb\"]=cp.multiply(self.om.rtn.En.v, self.om.SOC[:, 0] - self.om.rtn.SOCinit.v) - self.rtn.config.t * cp.multiply(self.om.rtn.EtaC.v, self.om.zce[:, 0]) + self.rtn.config.t * cp.multiply(self.om.rtn.REtaD.v, self.om.zde[:, 0]) == 0\n", - "Set constrs SOCr: SOC[:, -1] - SOCinit == 0\n", - "Code Constr: om.constrs[\"SOCr\"]=self.om.SOC[:, -1] - self.om.rtn.SOCinit.v == 0\n", - "Set obj tc: min. sum(c2 @ (t dot pg)**2 + c1 @ (t dot pg) + ug * c0) + sum(csr * ug * (Rpmax - pg))\n", - "Code Obj: om.obj=cp.Minimize(cp.sum(self.om.rtn.c2.v @ (self.rtn.config.t * self.om.pg)**2 + self.om.rtn.c1.v @ (self.rtn.config.t * self.om.pg) + self.om.rtn.ug.v @ self.om.rtn.c0.v) + cp.sum(self.om.rtn.csr.v @ self.om.rtn.ug.v * (self.om.rtn.Rpmax.v - self.om.pg)))\n", - "Routine initialized in 0.1141 seconds.\n" + "Routine initialized in 0.0238 seconds.\n", + "EDES solved as optimal in 0.0349 seconds, converged after 0 iteration using solver GUROBI.\n" ] }, { @@ -391,249 +385,245 @@ "True" ] }, - "execution_count": 12, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "sp.ED2.init(no_code=False)" + "sp.EDES.run(solver='GUROBI', reoptimize=True)" ] }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 9, "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "ED2 has already been initialized.\n", - "ED2 solved as optimal in 0.0905 seconds, converged after 68 iterations using solver GUROBI.\n" - ] - }, { "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
VarValueinfo
0uce[[0.0, 0.0, 0.0, 0.0]]ESD1 charging decision
1ude[[1.0, 1.0, 1.0, 1.0]]ESD1 discharging decision
2pce[[0.0, 0.0, 0.0, 0.0]]ESD1 charging power
3pde[[2.1, 2.1, 2.1, 2.1]]ESD1 discharging power
4SOC[[0.179, 0.179, 0.179, 0.179]]ESD1 State of Charge
5SOCinit[0.2]Initial SOC
\n", + "
" + ], "text/plain": [ - "True" + " Var Value info\n", + "0 uce [[0.0, 0.0, 0.0, 0.0]] ESD1 charging decision\n", + "1 ude [[1.0, 1.0, 1.0, 1.0]] ESD1 discharging decision\n", + "2 pce [[0.0, 0.0, 0.0, 0.0]] ESD1 charging power\n", + "3 pde [[2.1, 2.1, 2.1, 2.1]] ESD1 discharging power\n", + "4 SOC [[0.179, 0.179, 0.179, 0.179]] ESD1 State of Charge\n", + "5 SOCinit [0.2] Initial SOC" ] }, - "execution_count": 13, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "sp.ED2.run(solver=\"GUROBI\")" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "pg=[3.76453119 3.72343444 3.65885384]\n", - "uce=[[0. 0. 1.]\n", - " [0. 0. 1.]]\n", - "pce=[[0. 0. 1.829]\n", - " [0. 0. 1.829]]\n", - "zce=[[0. 0. 1.829]\n", - " [0. 0. 1.829]]\n", - "ude=[[1. 1. 0.]\n", - " [1. 1. 0.]]\n", - "pde=[[1.882 1.862 0. ]\n", - " [1.882 1.862 0. ]]\n", - "zde=[[1.882 1.862 0. ]\n", - " [1.882 1.862 0. ]]\n", - "SOC=[[0.481 0.1 0.5 ]\n", - " [0.481 0.1 0.5 ]]\n", - "obj=45508.58507547713\n" - ] - } - ], - "source": [ - "print(f\"pg={sp.ED2.pg.v[0, :]}\")\n", - "\n", - "nr = 3\n", + "ESESres = pd.DataFrame()\n", "\n", - "print(f\"uce={sp.ED2.uce.v}\")\n", - "print(f\"pce={sp.ED2.pce.v.round(nr)}\")\n", - "print(f\"zce={sp.ED2.zce.v.round(nr)}\")\n", + "items = [sp.EDES.uce, sp.EDES.ude,\n", + " sp.EDES.pce, sp.EDES.pde,\n", + " sp.EDES.SOC, sp.EDES.SOCinit]\n", "\n", - "print(f\"ude={sp.ED2.ude.v}\")\n", - "print(f\"pde={sp.ED2.pde.v.round(nr)}\")\n", - "print(f\"zde={sp.ED2.zde.v.round(nr)}\")\n", + "ESESres['Var'] = [item.name for item in items]\n", + "ESESres['Value'] = [item.v.round(4) for item in items]\n", + "ESESres['info'] = [item.info for item in items]\n", "\n", - "print(f\"SOC={sp.ED2.SOC.v.round(nr)}\")\n", - "print(f\"obj={sp.ED2.obj.v}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'mul(EnR, SOC @ Mre) - t dot mul(EtaCR, zce[:, 1:]) + t dot mul(REtaDR, zde[:, 1:])'" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "sp.ED2.SOCb.e_str" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "array([[-38.11773441, 40. ],\n", - " [-38.11773441, 40. ]])" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "np.multiply(sp.ED2.EnR.v, np.matmul(sp.ED2.SOC.v, sp.ED2.Mre.v))" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "array([[-0. , -1.82942692],\n", - " [-0. , -1.82942692]])" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "-sp.ED2.config.t * np.multiply(sp.ED2.EtaCR.v, sp.ED2.zce.v[:, 1:])" + "ESESres" ] }, { "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "array([[1.86171722, 0. ],\n", - " [1.86171722, 0. ]])" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "sp.ED2.config.t * np.multiply(sp.ED2.REtaDR.v, sp.ED2.zde.v[:, 1:])" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'mul(En, SOC[:, 0] - SOCinit) - t dot mul(EtaC, zce[:, 0]) + t dot mul(REtaD, zde[:, 0])'" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "sp.ED2.SOCb0.e_str" - ] - }, - { - "cell_type": "code", - "execution_count": 22, + "execution_count": 10, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "array([-1.88226559, -1.88226559])" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "np.multiply(sp.ED2.En.v, sp.ED2.SOC.v[:, 0]-sp.ED2.SOCinit.v)" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "metadata": {}, - "outputs": [ + "name": "stderr", + "output_type": "stream", + "text": [ + "All generators are online at initial, make initial guess for commitment.\n", + "Turn off StaticGen PV_1 as initial commitment guess.\n", + "Routine initialized in 0.0272 seconds.\n", + "UCES solved as optimal in 0.0369 seconds, converged after 0 iteration using solver GUROBI.\n" + ] + }, { "data": { "text/plain": [ - "array([-0., -0.])" + "True" ] }, - "execution_count": 25, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "-sp.ED2.config.t * np.multiply(sp.ED2.EtaC.v, sp.ED2.zce.v[:, 0])" + "sp.UCES.run(solver='GUROBI', reoptimize=True)" ] }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 11, "metadata": {}, "outputs": [ { "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
VarValueinfo
0uce[[0.0, 0.0, 0.0, 0.0]]ESD1 charging decision
1ude[[1.0, 1.0, 1.0, 1.0]]ESD1 discharging decision
2pce[[0.0, 0.0, 0.0, 0.0]]ESD1 charging power
3pde[[0.0, 0.0, 0.0, 0.0]]ESD1 discharging power
4SOC[[0.2, 0.2, 0.2, 0.2]]ESD1 State of Charge
5SOCinit[0.2]Initial SOC
\n", + "
" + ], "text/plain": [ - "array([1.88226559, 1.88226559])" + " Var Value info\n", + "0 uce [[0.0, 0.0, 0.0, 0.0]] ESD1 charging decision\n", + "1 ude [[1.0, 1.0, 1.0, 1.0]] ESD1 discharging decision\n", + "2 pce [[0.0, 0.0, 0.0, 0.0]] ESD1 charging power\n", + "3 pde [[0.0, 0.0, 0.0, 0.0]] ESD1 discharging power\n", + "4 SOC [[0.2, 0.2, 0.2, 0.2]] ESD1 State of Charge\n", + "5 SOCinit [0.2] Initial SOC" ] }, - "execution_count": 26, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "sp.ED2.config.t * np.multiply(sp.ED2.REtaD.v, sp.ED2.zde.v[:, 0])" + "UCESres = pd.DataFrame()\n", + "\n", + "items = [sp.UCES.uce, sp.UCES.ude,\n", + " sp.UCES.pce, sp.UCES.pde,\n", + " sp.UCES.SOC, sp.UCES.SOCinit]\n", + "\n", + "UCESres['Var'] = [item.name for item in items]\n", + "UCESres['Value'] = [item.v.round(4) for item in items]\n", + "UCESres['info'] = [item.info for item in items]\n", + "\n", + "UCESres" ] } ], diff --git a/examples/ex1.ipynb b/examples/ex1.ipynb index 2fd38dc9..c72d136a 100644 --- a/examples/ex1.ipynb +++ b/examples/ex1.ipynb @@ -51,8 +51,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "Last run time: 2023-11-07 22:30:50\n", - "ams:0.7.3.post29.dev0+g23b6565\n" + "Last run time: 2023-11-28 22:06:12\n", + "ams:0.7.3.post74.dev0+g47c1ae3\n" ] } ], @@ -126,16 +126,16 @@ "name": "stderr", "output_type": "stream", "text": [ - "Parsing input file \"/Users/jinningwang/Documents/work/ams/ams/cases/ieee39/ieee39_uced.xlsx\"...\n", - "Input file parsed in 0.1640 seconds.\n", - "System set up in 0.0041 seconds.\n" + "Parsing input file \"/Users/jinningwang/Documents/work/ams/ams/cases/5bus/pjm5bus_uced.xlsx\"...\n", + "Input file parsed in 0.0978 seconds.\n", + "System set up in 0.0032 seconds.\n" ] } ], "source": [ - "sp = ams.load(ams.get_case('ieee39/ieee39_uced.xlsx'),\n", - " default_config=True,\n", - " setup=True)" + "sp = ams.load(ams.get_case('5bus/pjm5bus_uced.xlsx'),\n", + " setup=True,\n", + " no_output=True,)" ] }, { @@ -162,28 +162,28 @@ { "data": { "text/plain": [ - "OrderedDict([('Summary', Summary (3 devices) at 0x1069a7670),\n", - " ('Bus', Bus (39 devices) at 0x161e03850),\n", - " ('PQ', PQ (19 devices) at 0x161e16250),\n", - " ('PV', PV (9 devices) at 0x161e16790),\n", - " ('Slack', Slack (1 device) at 0x161e25220),\n", - " ('Shunt', Shunt (0 devices) at 0x161e25ca0),\n", - " ('Line', Line (46 devices) at 0x161e2e190),\n", - " ('ESD1', ESD1 (0 devices) at 0x161e3c880),\n", - " ('REGCV1', REGCV1 (0 devices) at 0x161e3cfa0),\n", - " ('Area', Area (2 devices) at 0x161e4f730),\n", - " ('Region', Region (2 devices) at 0x161e4feb0),\n", - " ('SFR', SFR (2 devices) at 0x161e5a6a0),\n", - " ('SR', SR (2 devices) at 0x161e5ad00),\n", - " ('NSR', NSR (2 devices) at 0x161e64160),\n", - " ('GCost', GCost (10 devices) at 0x161e64580),\n", - " ('SFRCost', SFRCost (10 devices) at 0x161e64c10),\n", - " ('SRCost', SRCost (10 devices) at 0x161e731f0),\n", - " ('NSRCost', NSRCost (10 devices) at 0x161e73610),\n", - " ('REGCV1Cost', REGCV1Cost (0 devices) at 0x161e73a30),\n", - " ('TimeSlot', TimeSlot (0 devices) at 0x161e73d30),\n", - " ('EDTSlot', EDTSlot (24 devices) at 0x161e7c9a0),\n", - " ('UCTSlot', UCTSlot (24 devices) at 0x161e7cd90)])" + "OrderedDict([('Summary', Summary (3 devices) at 0x153c7bd00),\n", + " ('Bus', Bus (5 devices) at 0x10774b160),\n", + " ('PQ', PQ (3 devices) at 0x153c8e640),\n", + " ('PV', PV (3 devices) at 0x153c8eb80),\n", + " ('Slack', Slack (1 device) at 0x153c9e610),\n", + " ('Shunt', Shunt (0 devices) at 0x153caa0d0),\n", + " ('Line', Line (7 devices) at 0x153caa580),\n", + " ('ESD1', ESD1 (0 devices) at 0x153cb8c70),\n", + " ('REGCV1', REGCV1 (0 devices) at 0x153cc63d0),\n", + " ('Area', Area (3 devices) at 0x153cc6b20),\n", + " ('Region', Region (2 devices) at 0x153cd32e0),\n", + " ('SFR', SFR (2 devices) at 0x153cd3a90),\n", + " ('SR', SR (2 devices) at 0x153ce4130),\n", + " ('NSR', NSR (2 devices) at 0x153ce4550),\n", + " ('GCost', GCost (4 devices) at 0x153ce4970),\n", + " ('SFRCost', SFRCost (4 devices) at 0x153cf1040),\n", + " ('SRCost', SRCost (4 devices) at 0x153cf15e0),\n", + " ('NSRCost', NSRCost (4 devices) at 0x153cf1a00),\n", + " ('REGCV1Cost', REGCV1Cost (0 devices) at 0x153cf1e20),\n", + " ('TimeSlot', TimeSlot (0 devices) at 0x153cfb160),\n", + " ('EDTSlot', EDTSlot (24 devices) at 0x153cfbd30),\n", + " ('UCTSlot', UCTSlot (24 devices) at 0x153d0a190)])" ] }, "execution_count": 5, @@ -259,275 +259,51 @@ " 0\n", " PQ_1\n", " 1.0\n", - " PQ_1\n", - " 3\n", - " 345.0\n", - " 6.000\n", - " 2.500\n", - " 1.2\n", - " 0.8\n", + " PQ 1\n", " 1\n", + " 230.0\n", + " 3.0\n", + " 0.9861\n", + " 1.1\n", + " 0.9\n", + " None\n", " \n", " \n", " 1\n", " PQ_2\n", " 1.0\n", - " PQ_2\n", - " 4\n", - " 345.0\n", - " 4.500\n", - " 1.840\n", - " 1.2\n", - " 0.8\n", - " 1\n", + " PQ 2\n", + " 2\n", + " 230.0\n", + " 3.0\n", + " 0.9861\n", + " 1.1\n", + " 0.9\n", + " None\n", " \n", " \n", " 2\n", " PQ_3\n", " 1.0\n", - " PQ_3\n", - " 7\n", - " 345.0\n", - " 2.338\n", - " 0.840\n", - " 1.2\n", - " 0.8\n", - " 1\n", - " \n", - " \n", - " 3\n", - " PQ_4\n", - " 1.0\n", - " PQ_4\n", - " 8\n", - " 345.0\n", - " 5.220\n", - " 1.766\n", - " 1.2\n", - " 0.8\n", - " 1\n", - " \n", - " \n", - " 4\n", - " PQ_5\n", - " 1.0\n", - " PQ_5\n", - " 12\n", - " 138.0\n", - " 1.200\n", - " 0.300\n", - " 1.2\n", - " 0.8\n", - " 1\n", - " \n", - " \n", - " 5\n", - " PQ_6\n", - " 1.0\n", - " PQ_6\n", - " 15\n", - " 345.0\n", - " 3.200\n", - " 1.530\n", - " 1.2\n", - " 0.8\n", - " 1\n", - " \n", - " \n", - " 6\n", - " PQ_7\n", - " 1.0\n", - " PQ_7\n", - " 16\n", - " 345.0\n", - " 3.290\n", - " 0.323\n", - " 1.2\n", - " 0.8\n", - " 1\n", - " \n", - " \n", - " 7\n", - " PQ_8\n", - " 1.0\n", - " PQ_8\n", - " 18\n", - " 345.0\n", - " 1.580\n", - " 0.300\n", - " 1.2\n", - " 0.8\n", - " 1\n", - " \n", - " \n", - " 8\n", - " PQ_9\n", - " 1.0\n", - " PQ_9\n", - " 20\n", - " 138.0\n", - " 6.800\n", - " 1.030\n", - " 1.2\n", - " 0.8\n", - " 1\n", - " \n", - " \n", - " 9\n", - " PQ_10\n", - " 1.0\n", - " PQ_10\n", - " 21\n", - " 345.0\n", - " 2.740\n", - " 1.150\n", - " 1.2\n", - " 0.8\n", - " 1\n", - " \n", - " \n", - " 10\n", - " PQ_11\n", - " 1.0\n", - " PQ_11\n", - " 23\n", - " 345.0\n", - " 2.475\n", - " 0.846\n", - " 1.2\n", - " 0.8\n", - " 1\n", - " \n", - " \n", - " 11\n", - " PQ_12\n", - " 1.0\n", - " PQ_12\n", - " 24\n", - " 345.0\n", - " 3.086\n", - " -0.922\n", - " 1.2\n", - " 0.8\n", - " 1\n", - " \n", - " \n", - " 12\n", - " PQ_13\n", - " 1.0\n", - " PQ_13\n", - " 25\n", - " 345.0\n", - " 2.240\n", - " 0.472\n", - " 1.2\n", - " 0.8\n", - " 1\n", - " \n", - " \n", - " 13\n", - " PQ_14\n", - " 1.0\n", - " PQ_14\n", - " 26\n", - " 345.0\n", - " 1.390\n", - " 0.170\n", - " 1.2\n", - " 0.8\n", - " 1\n", - " \n", - " \n", - " 14\n", - " PQ_15\n", - " 1.0\n", - " PQ_15\n", - " 27\n", - " 345.0\n", - " 2.810\n", - " 0.755\n", - " 1.2\n", - " 0.8\n", - " 1\n", - " \n", - " \n", - " 15\n", - " PQ_16\n", - " 1.0\n", - " PQ_16\n", - " 28\n", - " 345.0\n", - " 2.060\n", - " 0.276\n", - " 1.2\n", - " 0.8\n", - " 1\n", - " \n", - " \n", - " 16\n", - " PQ_17\n", - " 1.0\n", - " PQ_17\n", - " 29\n", - " 345.0\n", - " 2.835\n", - " 1.269\n", - " 1.2\n", - " 0.8\n", - " 1\n", - " \n", - " \n", - " 17\n", - " PQ_18\n", - " 1.0\n", - " PQ_18\n", - " 31\n", - " 34.5\n", - " 0.800\n", - " 0.400\n", - " 1.2\n", - " 0.8\n", - " 1\n", - " \n", - " \n", - " 18\n", - " PQ_19\n", - " 1.0\n", - " PQ_19\n", - " 39\n", - " 345.0\n", - " 4.000\n", - " 2.500\n", - " 1.2\n", - " 0.8\n", - " 1\n", + " PQ 3\n", + " 3\n", + " 230.0\n", + " 4.0\n", + " 1.3147\n", + " 1.1\n", + " 0.9\n", + " None\n", " \n", " \n", "\n", "" ], "text/plain": [ - " idx u name bus Vn p0 q0 vmax vmin owner\n", - "uid \n", - "0 PQ_1 1.0 PQ_1 3 345.0 6.000 2.500 1.2 0.8 1\n", - "1 PQ_2 1.0 PQ_2 4 345.0 4.500 1.840 1.2 0.8 1\n", - "2 PQ_3 1.0 PQ_3 7 345.0 2.338 0.840 1.2 0.8 1\n", - "3 PQ_4 1.0 PQ_4 8 345.0 5.220 1.766 1.2 0.8 1\n", - "4 PQ_5 1.0 PQ_5 12 138.0 1.200 0.300 1.2 0.8 1\n", - "5 PQ_6 1.0 PQ_6 15 345.0 3.200 1.530 1.2 0.8 1\n", - "6 PQ_7 1.0 PQ_7 16 345.0 3.290 0.323 1.2 0.8 1\n", - "7 PQ_8 1.0 PQ_8 18 345.0 1.580 0.300 1.2 0.8 1\n", - "8 PQ_9 1.0 PQ_9 20 138.0 6.800 1.030 1.2 0.8 1\n", - "9 PQ_10 1.0 PQ_10 21 345.0 2.740 1.150 1.2 0.8 1\n", - "10 PQ_11 1.0 PQ_11 23 345.0 2.475 0.846 1.2 0.8 1\n", - "11 PQ_12 1.0 PQ_12 24 345.0 3.086 -0.922 1.2 0.8 1\n", - "12 PQ_13 1.0 PQ_13 25 345.0 2.240 0.472 1.2 0.8 1\n", - "13 PQ_14 1.0 PQ_14 26 345.0 1.390 0.170 1.2 0.8 1\n", - "14 PQ_15 1.0 PQ_15 27 345.0 2.810 0.755 1.2 0.8 1\n", - "15 PQ_16 1.0 PQ_16 28 345.0 2.060 0.276 1.2 0.8 1\n", - "16 PQ_17 1.0 PQ_17 29 345.0 2.835 1.269 1.2 0.8 1\n", - "17 PQ_18 1.0 PQ_18 31 34.5 0.800 0.400 1.2 0.8 1\n", - "18 PQ_19 1.0 PQ_19 39 345.0 4.000 2.500 1.2 0.8 1" + " idx u name bus Vn p0 q0 vmax vmin owner\n", + "uid \n", + "0 PQ_1 1.0 PQ 1 1 230.0 3.0 0.9861 1.1 0.9 None\n", + "1 PQ_2 1.0 PQ 2 2 230.0 3.0 0.9861 1.1 0.9 None\n", + "2 PQ_3 1.0 PQ 3 3 230.0 4.0 1.3147 1.1 0.9 None" ] }, "execution_count": 6, @@ -555,19 +331,19 @@ { "data": { "text/plain": [ - "OrderedDict([('DCPF', DCPF at 0x161e035e0),\n", - " ('PFlow', PFlow at 0x161e89a30),\n", - " ('CPF', CPF at 0x161e9a040),\n", - " ('ACOPF', ACOPF at 0x161e9a640),\n", - " ('DCOPF', DCOPF at 0x161eb0340),\n", - " ('ED', ED at 0x161eb08e0),\n", - " ('ED2', ED2 at 0x161ebfbb0),\n", - " ('RTED', RTED at 0x161ecec70),\n", - " ('RTED2', RTED2 at 0x162008670),\n", - " ('UC', UC at 0x165495a30),\n", - " ('UC2', UC2 at 0x1658735e0),\n", - " ('LDOPF', LDOPF at 0x16587ec40),\n", - " ('LDOPF2', LDOPF2 at 0x16588b730)])" + "OrderedDict([('DCPF', DCPF at 0x153c7bcd0),\n", + " ('PFlow', PFlow at 0x153d0adc0),\n", + " ('CPF', CPF at 0x153d193d0),\n", + " ('ACOPF', ACOPF at 0x153d19a30),\n", + " ('DCOPF', DCOPF at 0x153d31370),\n", + " ('ED', ED at 0x153d45040),\n", + " ('EDES', EDES at 0x153d6d100),\n", + " ('RTED', RTED at 0x153d93220),\n", + " ('RTEDES', RTEDES at 0x153da2430),\n", + " ('UC', UC at 0x153db6f10),\n", + " ('UCES', UCES at 0x16816ce20),\n", + " ('DOPF', DOPF at 0x16819e490),\n", + " ('DOPFVIS', DOPFVIS at 0x1681b35b0)])" ] }, "execution_count": 7, @@ -604,7 +380,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "Routine initialized in 0.0060 seconds.\n" + "Routine initialized in 0.0130 seconds.\n" ] }, { @@ -627,7 +403,7 @@ "metadata": {}, "source": [ "Then, one can solve it by calling ``run()``.\n", - "Here, argument `solver` can be passed to specify the solver to use, such as `solver='GUROBI'`.\n", + "Here, argument `solver` can be passed to specify the solver to use, such as `solver='ECOS'`.\n", "\n", "Installed solvers can be listed by ``ams.shared.INSTALLED_SOLVERS``,\n", "and more detailes of solver can be found at [CVXPY-Choosing a solver](https://www.cvxpy.org/tutorial/advanced/index.html#choosing-a-solver)." @@ -637,12 +413,41 @@ "cell_type": "code", "execution_count": 9, "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['CLARABEL',\n", + " 'CVXOPT',\n", + " 'ECOS',\n", + " 'ECOS_BB',\n", + " 'GLPK',\n", + " 'GLPK_MI',\n", + " 'GUROBI',\n", + " 'OSQP',\n", + " 'SCIPY',\n", + " 'SCS']" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ams.shared.INSTALLED_SOLVERS" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "RTED solved as optimal in 0.0118 seconds, converged after 50 iterations using solver OSQP.\n" + "RTED solved as optimal in 0.0193 seconds, converged after 9 iterations using solver ECOS.\n" ] }, { @@ -651,13 +456,13 @@ "True" ] }, - "execution_count": 9, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "sp.RTED.run()" + "sp.RTED.run(solver='ECOS')" ] }, { @@ -671,17 +476,16 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "array([6.01822025, 5.99427957, 5.97043409, 5.08 , 5.98234496,\n", - " 5.8 , 5.64 , 6.00623798, 6.03022646, 6.04225669])" + "array([2.1, 5.2, 0.7, 2. ])" ] }, - "execution_count": 10, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -699,25 +503,16 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "['PV_30',\n", - " 'PV_31',\n", - " 'PV_32',\n", - " 'PV_33',\n", - " 'PV_34',\n", - " 'PV_35',\n", - " 'PV_36',\n", - " 'PV_37',\n", - " 'PV_38',\n", - " 'Slack_39']" + "['PV_1', 'PV_3', 'PV_5', 'Slack_4']" ] }, - "execution_count": 11, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -735,47 +530,47 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "array([6.01822025, 5.99427957])" + "array([2.1, 5.2])" ] }, - "execution_count": 12, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "sp.RTED.get(src='pg', attr='v', idx=['PV_30', 'PV_31'])" + "sp.RTED.get(src='pg', attr='v', idx=['PV_1', 'PV_3'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "All variables are listed in an OrderedDict ``vars``." + "All Vars are listed in an OrderedDict ``vars``." ] }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "OrderedDict([('pg', Var: StaticGen.pg),\n", - " ('pn', Var: Bus.pn),\n", + " ('aBus', Var: Bus.aBus),\n", " ('plf', Var: Line.plf),\n", " ('pru', Var: StaticGen.pru),\n", " ('prd', Var: StaticGen.prd)])" ] }, - "execution_count": 13, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } @@ -788,21 +583,21 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The objective value can be accessed with ``obj.v``." + "The Objective value can be accessed with ``obj.v``." ] }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "592.8203580808987" + "2287.0833333698793" ] }, - "execution_count": 14, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } @@ -815,31 +610,34 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Similarly, the constraints are listed in an OrderedDict ``constrs``,\n", + "Similarly, all Constrs are listed in an OrderedDict ``constrs``,\n", "and the expression values can also be accessed." ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "OrderedDict([('pb', [ON]: sum(pl) - sum(pg) =0),\n", - " ('pinj', [ON]: CftT@plf - pl - pn =0),\n", - " ('lub', [ON]: PTDF @ (pn - pl) - rate_a <=0),\n", - " ('llb', [ON]: - PTDF @ (pn - pl) - rate_a <=0),\n", - " ('rbu', [ON]: gs @ multiply(ug, pru) - dud =0),\n", - " ('rbd', [ON]: gs @ multiply(ug, prd) - ddd =0),\n", - " ('rru', [ON]: multiply(ug, pg + pru) - pmax <=0),\n", - " ('rrd', [ON]: multiply(ug, -pg + prd) - pmin <=0),\n", - " ('rgu', [ON]: multiply(ug, pg-pg0-R10) <=0),\n", - " ('rgd', [ON]: multiply(ug, -pg+pg0-R10) <=0)])" + "OrderedDict([('pglb', [ON]: -pg + mul(nctrle, pg0) + mul(ctrle, pmin) <=0),\n", + " ('pgub', [ON]: pg - mul(nctrle, pg0) - mul(ctrle, pmax) <=0),\n", + " ('plflb', [ON]: -plf - rate_a <=0),\n", + " ('plfub', [ON]: plf - rate_a <=0),\n", + " ('pb', [ON]: sum(pd) - sum(pg) =0),\n", + " ('aref', [ON]: Cs@aBus =0),\n", + " ('pnb', [ON]: PTDF@(Cgi@pg - Cli@pd) - plf =0),\n", + " ('rbu', [ON]: gs @ mul(ug, pru) - dud =0),\n", + " ('rbd', [ON]: gs @ mul(ug, prd) - ddd =0),\n", + " ('rru', [ON]: mul(ug, pg + pru) - mul(ug, pmax) <=0),\n", + " ('rrd', [ON]: mul(ug, -pg + prd) - mul(ug, pmin) <=0),\n", + " ('rgu', [ON]: mul(ug, pg-pg0-R10) <=0),\n", + " ('rgd', [ON]: mul(ug, -pg+pg0-R10) <=0)])" ] }, - "execution_count": 15, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } @@ -848,20 +646,25 @@ "sp.RTED.constrs" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "One can also inspect the Constr values." + ] + }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "array([-99.48177975, -99.50572043, -99.52956591, -98.92 ,\n", - " -99.61765504, -98.8 , -98.76 , -99.49376202,\n", - " -99.46977354, -99.73645331])" + "array([ -997.9 , -997.0349, -1002.9651, -997. ])" ] }, - "execution_count": 16, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } @@ -887,7 +690,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.16" + "version": "3.9.18" }, "orig_nbformat": 4, "vscode": { diff --git a/examples/ex2.ipynb b/examples/ex2.ipynb index b1b40b99..839a1956 100644 --- a/examples/ex2.ipynb +++ b/examples/ex2.ipynb @@ -5,14 +5,15 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Manipulate the Model Data" + "# Manipulate the Dispatch Model" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "This example shows how to manipulate the model." + "This example shows how to manipulate the model,\n", + "such as trip a generator, change the load, etc." ] }, { @@ -35,8 +36,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "Last run time: 2023-11-07 22:31:12\n", - "ams:0.7.3.post29.dev0+g23b6565\n" + "Last run time: 2023-11-28 22:06:37\n", + "ams:0.7.3.post74.dev0+g47c1ae3\n" ] } ], @@ -80,16 +81,16 @@ "name": "stderr", "output_type": "stream", "text": [ - "Parsing input file \"/Users/jinningwang/Documents/work/ams/ams/cases/ieee39/ieee39_uced.xlsx\"...\n", - "Input file parsed in 0.1714 seconds.\n", - "System set up in 0.0043 seconds.\n" + "Parsing input file \"/Users/jinningwang/Documents/work/ams/ams/cases/5bus/pjm5bus_uced.xlsx\"...\n", + "Input file parsed in 0.1106 seconds.\n", + "System set up in 0.0034 seconds.\n" ] } ], "source": [ - "sp = ams.load(ams.get_case('ieee39/ieee39_uced.xlsx'),\n", - " default_config=True,\n", - " setup=True)" + "sp = ams.load(ams.get_case('5bus/pjm5bus_uced.xlsx'),\n", + " setup=True,\n", + " no_output=True,)" ] }, { @@ -155,275 +156,51 @@ " 0\n", " PQ_1\n", " 1.0\n", - " PQ_1\n", - " 3\n", - " 345.0\n", - " 6.000\n", - " 2.500\n", - " 1.2\n", - " 0.8\n", + " PQ 1\n", " 1\n", + " 230.0\n", + " 3.0\n", + " 0.9861\n", + " 1.1\n", + " 0.9\n", + " None\n", " \n", " \n", " 1\n", " PQ_2\n", " 1.0\n", - " PQ_2\n", - " 4\n", - " 345.0\n", - " 4.500\n", - " 1.840\n", - " 1.2\n", - " 0.8\n", - " 1\n", + " PQ 2\n", + " 2\n", + " 230.0\n", + " 3.0\n", + " 0.9861\n", + " 1.1\n", + " 0.9\n", + " None\n", " \n", " \n", " 2\n", " PQ_3\n", " 1.0\n", - " PQ_3\n", - " 7\n", - " 345.0\n", - " 2.338\n", - " 0.840\n", - " 1.2\n", - " 0.8\n", - " 1\n", - " \n", - " \n", - " 3\n", - " PQ_4\n", - " 1.0\n", - " PQ_4\n", - " 8\n", - " 345.0\n", - " 5.220\n", - " 1.766\n", - " 1.2\n", - " 0.8\n", - " 1\n", - " \n", - " \n", - " 4\n", - " PQ_5\n", - " 1.0\n", - " PQ_5\n", - " 12\n", - " 138.0\n", - " 1.200\n", - " 0.300\n", - " 1.2\n", - " 0.8\n", - " 1\n", - " \n", - " \n", - " 5\n", - " PQ_6\n", - " 1.0\n", - " PQ_6\n", - " 15\n", - " 345.0\n", - " 3.200\n", - " 1.530\n", - " 1.2\n", - " 0.8\n", - " 1\n", - " \n", - " \n", - " 6\n", - " PQ_7\n", - " 1.0\n", - " PQ_7\n", - " 16\n", - " 345.0\n", - " 3.290\n", - " 0.323\n", - " 1.2\n", - " 0.8\n", - " 1\n", - " \n", - " \n", - " 7\n", - " PQ_8\n", - " 1.0\n", - " PQ_8\n", - " 18\n", - " 345.0\n", - " 1.580\n", - " 0.300\n", - " 1.2\n", - " 0.8\n", - " 1\n", - " \n", - " \n", - " 8\n", - " PQ_9\n", - " 1.0\n", - " PQ_9\n", - " 20\n", - " 138.0\n", - " 6.800\n", - " 1.030\n", - " 1.2\n", - " 0.8\n", - " 1\n", - " \n", - " \n", - " 9\n", - " PQ_10\n", - " 1.0\n", - " PQ_10\n", - " 21\n", - " 345.0\n", - " 2.740\n", - " 1.150\n", - " 1.2\n", - " 0.8\n", - " 1\n", - " \n", - " \n", - " 10\n", - " PQ_11\n", - " 1.0\n", - " PQ_11\n", - " 23\n", - " 345.0\n", - " 2.475\n", - " 0.846\n", - " 1.2\n", - " 0.8\n", - " 1\n", - " \n", - " \n", - " 11\n", - " PQ_12\n", - " 1.0\n", - " PQ_12\n", - " 24\n", - " 345.0\n", - " 3.086\n", - " -0.922\n", - " 1.2\n", - " 0.8\n", - " 1\n", - " \n", - " \n", - " 12\n", - " PQ_13\n", - " 1.0\n", - " PQ_13\n", - " 25\n", - " 345.0\n", - " 2.240\n", - " 0.472\n", - " 1.2\n", - " 0.8\n", - " 1\n", - " \n", - " \n", - " 13\n", - " PQ_14\n", - " 1.0\n", - " PQ_14\n", - " 26\n", - " 345.0\n", - " 1.390\n", - " 0.170\n", - " 1.2\n", - " 0.8\n", - " 1\n", - " \n", - " \n", - " 14\n", - " PQ_15\n", - " 1.0\n", - " PQ_15\n", - " 27\n", - " 345.0\n", - " 2.810\n", - " 0.755\n", - " 1.2\n", - " 0.8\n", - " 1\n", - " \n", - " \n", - " 15\n", - " PQ_16\n", - " 1.0\n", - " PQ_16\n", - " 28\n", - " 345.0\n", - " 2.060\n", - " 0.276\n", - " 1.2\n", - " 0.8\n", - " 1\n", - " \n", - " \n", - " 16\n", - " PQ_17\n", - " 1.0\n", - " PQ_17\n", - " 29\n", - " 345.0\n", - " 2.835\n", - " 1.269\n", - " 1.2\n", - " 0.8\n", - " 1\n", - " \n", - " \n", - " 17\n", - " PQ_18\n", - " 1.0\n", - " PQ_18\n", - " 31\n", - " 34.5\n", - " 0.800\n", - " 0.400\n", - " 1.2\n", - " 0.8\n", - " 1\n", - " \n", - " \n", - " 18\n", - " PQ_19\n", - " 1.0\n", - " PQ_19\n", - " 39\n", - " 345.0\n", - " 4.000\n", - " 2.500\n", - " 1.2\n", - " 0.8\n", - " 1\n", + " PQ 3\n", + " 3\n", + " 230.0\n", + " 4.0\n", + " 1.3147\n", + " 1.1\n", + " 0.9\n", + " None\n", " \n", " \n", "\n", "" ], "text/plain": [ - " idx u name bus Vn p0 q0 vmax vmin owner\n", - "uid \n", - "0 PQ_1 1.0 PQ_1 3 345.0 6.000 2.500 1.2 0.8 1\n", - "1 PQ_2 1.0 PQ_2 4 345.0 4.500 1.840 1.2 0.8 1\n", - "2 PQ_3 1.0 PQ_3 7 345.0 2.338 0.840 1.2 0.8 1\n", - "3 PQ_4 1.0 PQ_4 8 345.0 5.220 1.766 1.2 0.8 1\n", - "4 PQ_5 1.0 PQ_5 12 138.0 1.200 0.300 1.2 0.8 1\n", - "5 PQ_6 1.0 PQ_6 15 345.0 3.200 1.530 1.2 0.8 1\n", - "6 PQ_7 1.0 PQ_7 16 345.0 3.290 0.323 1.2 0.8 1\n", - "7 PQ_8 1.0 PQ_8 18 345.0 1.580 0.300 1.2 0.8 1\n", - "8 PQ_9 1.0 PQ_9 20 138.0 6.800 1.030 1.2 0.8 1\n", - "9 PQ_10 1.0 PQ_10 21 345.0 2.740 1.150 1.2 0.8 1\n", - "10 PQ_11 1.0 PQ_11 23 345.0 2.475 0.846 1.2 0.8 1\n", - "11 PQ_12 1.0 PQ_12 24 345.0 3.086 -0.922 1.2 0.8 1\n", - "12 PQ_13 1.0 PQ_13 25 345.0 2.240 0.472 1.2 0.8 1\n", - "13 PQ_14 1.0 PQ_14 26 345.0 1.390 0.170 1.2 0.8 1\n", - "14 PQ_15 1.0 PQ_15 27 345.0 2.810 0.755 1.2 0.8 1\n", - "15 PQ_16 1.0 PQ_16 28 345.0 2.060 0.276 1.2 0.8 1\n", - "16 PQ_17 1.0 PQ_17 29 345.0 2.835 1.269 1.2 0.8 1\n", - "17 PQ_18 1.0 PQ_18 31 34.5 0.800 0.400 1.2 0.8 1\n", - "18 PQ_19 1.0 PQ_19 39 345.0 4.000 2.500 1.2 0.8 1" + " idx u name bus Vn p0 q0 vmax vmin owner\n", + "uid \n", + "0 PQ_1 1.0 PQ 1 1 230.0 3.0 0.9861 1.1 0.9 None\n", + "1 PQ_2 1.0 PQ 2 2 230.0 3.0 0.9861 1.1 0.9 None\n", + "2 PQ_3 1.0 PQ 3 3 230.0 4.0 1.3147 1.1 0.9 None" ] }, "execution_count": 5, @@ -439,7 +216,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "For simplicity, PQ is reorganized as nodal load `pl` in an routine." + "For simplicity, PQ load is typically marked as `pd` in RTED." ] }, { @@ -450,11 +227,7 @@ { "data": { "text/plain": [ - "array([0. , 0. , 6. , 4.5 , 0. , 0. , 2.338, 5.22 , 0. ,\n", - " 0. , 0. , 1.2 , 0. , 0. , 3.2 , 3.29 , 0. , 1.58 ,\n", - " 0. , 6.8 , 2.74 , 0. , 2.475, 3.086, 2.24 , 1.39 , 2.81 ,\n", - " 2.06 , 2.835, 0. , 0.8 , 0. , 0. , 0. , 0. , 0. ,\n", - " 0. , 0. , 4. ])" + "array([3., 3., 4.])" ] }, "execution_count": 6, @@ -463,7 +236,14 @@ } ], "source": [ - "sp.RTED.pl.v" + "sp.RTED.pd.v" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Run Simulation" ] }, { @@ -483,8 +263,8 @@ "name": "stderr", "output_type": "stream", "text": [ - "Routine initialized in 0.0083 seconds.\n", - "RTED solved as optimal in 0.0138 seconds, converged after 50 iterations using solver OSQP.\n" + "Routine initialized in 0.0088 seconds.\n", + "RTED solved as optimal in 0.0170 seconds, converged after 9 iterations using solver ECOS.\n" ] }, { @@ -499,7 +279,14 @@ } ], "source": [ - "sp.RTED.run()" + "sp.RTED.run(solver='ECOS')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Power generation (`pg`) and line flow (`plf`) can be accessed as follows." ] }, { @@ -510,8 +297,7 @@ { "data": { "text/plain": [ - "array([6.01822025, 5.99427957, 5.97043409, 5.08 , 5.98234496,\n", - " 5.8 , 5.64 , 6.00623798, 6.03022646, 6.04225669])" + "array([2.1, 5.2, 0.7, 2. ])" ] }, "execution_count": 8, @@ -523,13 +309,6 @@ "sp.RTED.pg.v" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The load values can be manipulated in the model `PQ`." - ] - }, { "cell_type": "code", "execution_count": 9, @@ -538,7 +317,8 @@ { "data": { "text/plain": [ - "True" + "array([ 0.70595331, 0.68616798, 0.00192539, -1.58809337, 0.61190663,\n", + " -0.70192539, 0.70595331])" ] }, "execution_count": 9, @@ -547,14 +327,21 @@ } ], "source": [ - "sp.PQ.set(src='p0', attr='v', idx=['PQ_1', 'PQ_3'], value=[6.5, 3])" + "sp.RTED.plf.v" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "The routine need to be re-initialized to make the changes effective." + "### Change Load" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The load values can be manipulated in the model `PQ`." ] }, { @@ -562,13 +349,6 @@ "execution_count": 10, "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Routine initialized in 0.0064 seconds.\n" - ] - }, { "data": { "text/plain": [ @@ -581,14 +361,16 @@ } ], "source": [ - "sp.RTED.init()" + "sp.PQ.set(src='p0', attr='v', idx=['PQ_1', 'PQ_2'], value=[3.2, 3.2])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We can see the `pl` is changed as expected." + "According parameters need to be updated to make the changes effective.\n", + "If not sure which parameters need to be updated, one can use\n", + "``update()`` to update all parameters." ] }, { @@ -599,11 +381,7 @@ { "data": { "text/plain": [ - "array([0. , 0. , 6.5 , 4.5 , 0. , 0. , 3. , 5.22 , 0. ,\n", - " 0. , 0. , 1.2 , 0. , 0. , 3.2 , 3.29 , 0. , 1.58 ,\n", - " 0. , 6.8 , 2.74 , 0. , 2.475, 3.086, 2.24 , 1.39 , 2.81 ,\n", - " 2.06 , 2.835, 0. , 0.8 , 0. , 0. , 0. , 0. , 0. ,\n", - " 0. , 0. , 4. ])" + "True" ] }, "execution_count": 11, @@ -612,7 +390,7 @@ } ], "source": [ - "sp.RTED.pl.v" + "sp.RTED.update('pd')" ] }, { @@ -631,7 +409,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "RTED solved as optimal in 0.0140 seconds, converged after 50 iterations using solver OSQP.\n" + "RTED solved as optimal in 0.0016 seconds, converged after 8 iterations using solver ECOS.\n" ] }, { @@ -646,7 +424,7 @@ } ], "source": [ - "sp.RTED.run()" + "sp.RTED.run(solver='ECOS')" ] }, { @@ -657,8 +435,7 @@ { "data": { "text/plain": [ - "array([6.18438538, 6.16011369, 6.13593852, 5.07999972, 6.14801408,\n", - " 5.79999973, 5.63999972, 6.17223744, 6.19655759, 6.20875415])" + "array([2.1 , 5.19999999, 1.10000002, 1.99999999])" ] }, "execution_count": 13, @@ -669,6 +446,750 @@ "source": [ "sp.RTED.pg.v" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Trip a Generator" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can see that there are three PV generators in the system." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idxunameSnVnbusbusrp0q0pmax...Qc2minQc2maxRagcR10R30Rqapfpg0td1td2
uid
0PV_11.0Alta100.0230.00None1.00000.02.1...0.00.0999.0999.0999.0999.00.00.00.50.0
1PV_31.0Solitude100.0230.02None3.23490.05.2...0.00.0999.0999.0999.0999.00.00.00.50.0
2PV_51.0Brighton100.0230.04None4.66510.06.0...0.00.0999.0999.0999.0999.00.00.00.50.0
\n", + "

3 rows × 33 columns

\n", + "
" + ], + "text/plain": [ + " idx u name Sn Vn bus busr p0 q0 pmax ... \\\n", + "uid ... \n", + "0 PV_1 1.0 Alta 100.0 230.0 0 None 1.0000 0.0 2.1 ... \n", + "1 PV_3 1.0 Solitude 100.0 230.0 2 None 3.2349 0.0 5.2 ... \n", + "2 PV_5 1.0 Brighton 100.0 230.0 4 None 4.6651 0.0 6.0 ... \n", + "\n", + " Qc2min Qc2max Ragc R10 R30 Rq apf pg0 td1 td2 \n", + "uid \n", + "0 0.0 0.0 999.0 999.0 999.0 999.0 0.0 0.0 0.5 0.0 \n", + "1 0.0 0.0 999.0 999.0 999.0 999.0 0.0 0.0 0.5 0.0 \n", + "2 0.0 0.0 999.0 999.0 999.0 999.0 0.0 0.0 0.5 0.0 \n", + "\n", + "[3 rows x 33 columns]" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sp.PV.as_df()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`PV_1` is tripped by setting its connection status `u` to 0." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sp.StaticGen.set(src='u', attr='v', idx='PV_1', value=0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In AMS, some parameters are defiend as constants in the numerical optimization model\n", + "to follow the CVXPY DCP and DPP rules.\n", + "Once non-parametric parameters are changed, the model needs to be resetup\n", + "to make the changes effective.\n", + "\n", + "More details can be found at [CVXPY - Disciplined Convex Programming](https://www.cvxpy.org/tutorial/dcp/index.html#disciplined-convex-programming)." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Resetup RTED OModel due to non-parametric change.\n" + ] + }, + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sp.RTED.update()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then we can resolve the model." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "RTED solved as optimal in 0.0171 seconds, converged after 8 iterations using solver ECOS.\n" + ] + }, + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sp.RTED.run(solver='ECOS')" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([0. , 5.2 , 3.20000001, 2. ])" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sp.RTED.pg.v" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([ 0.79973461, 0.56088965, -2.16035886, -1.60053079, 0.39946921,\n", + " -1.03964115, 0.79973461])" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sp.RTED.plf.v" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Trip a Line" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can inspect the `Line` model to check the system topology." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idxunamebus1bus2SnfnVn1Vn2r...tapphirate_arate_brate_cownerxcoordycoordaminamax
uid
001.0Line 1-201100.060.0230.0230.00.00281...1.00.0999.0999.0999.0NoneNoneNone-6.2831856.283185
111.0Line 1-403100.060.0230.0230.00.00304...1.00.0999.0999.0999.0NoneNoneNone-6.2831856.283185
221.0Line 1-504100.060.0230.0230.00.00064...1.00.0999.0999.0999.0NoneNoneNone-6.2831856.283185
331.0Line 2-312100.060.0230.0230.00.00108...1.00.0999.0999.0999.0NoneNoneNone-6.2831856.283185
441.0Line 3-423100.060.0230.0230.00.00297...1.00.0999.0999.0999.0NoneNoneNone-6.2831856.283185
551.0Line 4-534100.060.0230.0230.00.00297...1.00.0999.0999.0999.0NoneNoneNone-6.2831856.283185
661.0Line 1-2 (2)01100.060.0230.0230.00.00281...1.00.0999.0999.0999.0NoneNoneNone-6.2831856.283185
\n", + "

7 rows × 28 columns

\n", + "
" + ], + "text/plain": [ + " idx u name bus1 bus2 Sn fn Vn1 Vn2 r \\\n", + "uid \n", + "0 0 1.0 Line 1-2 0 1 100.0 60.0 230.0 230.0 0.00281 \n", + "1 1 1.0 Line 1-4 0 3 100.0 60.0 230.0 230.0 0.00304 \n", + "2 2 1.0 Line 1-5 0 4 100.0 60.0 230.0 230.0 0.00064 \n", + "3 3 1.0 Line 2-3 1 2 100.0 60.0 230.0 230.0 0.00108 \n", + "4 4 1.0 Line 3-4 2 3 100.0 60.0 230.0 230.0 0.00297 \n", + "5 5 1.0 Line 4-5 3 4 100.0 60.0 230.0 230.0 0.00297 \n", + "6 6 1.0 Line 1-2 (2) 0 1 100.0 60.0 230.0 230.0 0.00281 \n", + "\n", + " ... tap phi rate_a rate_b rate_c owner xcoord ycoord amin \\\n", + "uid ... \n", + "0 ... 1.0 0.0 999.0 999.0 999.0 None None None -6.283185 \n", + "1 ... 1.0 0.0 999.0 999.0 999.0 None None None -6.283185 \n", + "2 ... 1.0 0.0 999.0 999.0 999.0 None None None -6.283185 \n", + "3 ... 1.0 0.0 999.0 999.0 999.0 None None None -6.283185 \n", + "4 ... 1.0 0.0 999.0 999.0 999.0 None None None -6.283185 \n", + "5 ... 1.0 0.0 999.0 999.0 999.0 None None None -6.283185 \n", + "6 ... 1.0 0.0 999.0 999.0 999.0 None None None -6.283185 \n", + "\n", + " amax \n", + "uid \n", + "0 6.283185 \n", + "1 6.283185 \n", + "2 6.283185 \n", + "3 6.283185 \n", + "4 6.283185 \n", + "5 6.283185 \n", + "6 6.283185 \n", + "\n", + "[7 rows x 28 columns]" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sp.Line.as_df()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here line `1` is tripped by setting its connection status `u` to 0." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sp.Line.set(src='u', attr='v', idx=0, value=0)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Resetup RTED OModel due to non-parametric change.\n" + ] + }, + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sp.RTED.update()" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "RTED solved as optimal in 0.0190 seconds, converged after 8 iterations using solver ECOS.\n" + ] + }, + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sp.RTED.run(solver='ECOS')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here we can see the tripped line has no flow." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([-0. , 0.70423831, -2.03964421, -1.8645941 , 0.13540589,\n", + " -1.16035581, 1.3354059 ])" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sp.RTED.plf.v" + ] } ], "metadata": { @@ -687,7 +1208,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.16" + "version": "3.9.18" }, "orig_nbformat": 4, "vscode": { diff --git a/requirements.txt b/requirements.txt index 78f263c5..fad7a13c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,5 @@ matplotlib psutil openpyxl andes -cvxpy \ No newline at end of file +cvxpy +cvxopt \ No newline at end of file From 2613c0511d53171dbbad1e9bcdf83fae3ccee3ab Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 29 Nov 2023 08:08:37 -0500 Subject: [PATCH 76/77] Fix import Iterable --- ams/core/param.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ams/core/param.py b/ams/core/param.py index 52df6977..9b900577 100644 --- a/ams/core/param.py +++ b/ams/core/param.py @@ -5,8 +5,7 @@ import logging -from typing import Optional -from collections import Iterable +from typing import Optional, Iterable import numpy as np from scipy.sparse import issparse From 6a047836a45b5eaeb020a66f512cf94a53d64e1a Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 29 Nov 2023 08:54:52 -0500 Subject: [PATCH 77/77] Simplify API doc --- docs/source/api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index c3d483da..9fabf3cd 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -43,7 +43,7 @@ Routines :caption: Routines :template: autosummary/module_toctree.rst - ams.routines + ams.routines.routine Optimization