From fe959651a9e1b827ffbea0dffaae5b1ac1fbc9f9 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 2 Sep 2024 14:21:08 -0400 Subject: [PATCH 001/181] Add project toml --- docs/source/release-notes.rst | 3 +- pyproject.toml | 66 +++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 pyproject.toml diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 7c97f853..6f24fbb6 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -9,7 +9,7 @@ The APIs before v3.0.0 are in beta and may change without prior notice. Pre-v1.0.0 ========== -v0.9.9 (2024-xx-xx) +v0.9.9 (2024-09-02) ------------------- - In module `MatProcessor`, add two parameters `permc_spec` and `use_umfpack` in function `build_ptdf` @@ -18,6 +18,7 @@ v0.9.9 (2024-xx-xx) - Skip macOS tests in azure-pipelines due to failure in fixing its configuration - Prepare to support NumPy v2.0.0, but solvers have unexpected behavior - Improve the logic of setting `Optz` value +- Support NumPy v2.0.0 v0.9.8 (2024-06-18) ------------------- diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..d54e84bf --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,66 @@ +[build-system] +requires = ["setuptools", "wheel", "versioneer[toml]"] +build-backend = "setuptools.build_meta" + +[project] +name = "ltbams" +dynamic = ["version"] +description = "Python software for scheduling modeling and co-simulation with dynamics." +readme = "README.md" +authors = [ + {name = "Jinning Wang", email = "jinninggm@gmail.com"} +] + +license = {file = "LICENSE"} + +classifiers = [ + "Development Status :: 4 - Beta", + "Natural Language :: English", + "Programming Language :: Python :: 3", + "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", + "Environment :: Console" +] + +dependencies = [ + "kvxopt>=1.3.2.1", + "numpy", + "scipy", + "sympy>=1.6,!=1.10.0", + "pandas", + "matplotlib", + "psutil", + "openpyxl", + "andes>=1.8.7", + "pybind11", + "cvxpy" +] + +[project.optional-dependencies] +dev = [ + "pytest", + "pytest-cov", + "coverage", + "flake8", + "ipython", + "numpydoc", + "toml" +] +doc = [ + "sphinx", + "pydata-sphinx-theme", + "numpydoc", + "sphinx-copybutton", + "sphinx-panels", + "myst-parser", + "nbsphinx" +] + +[project.scripts] +ams = "ams.cli:main" + +[tool.versioneer] +VCS = "git" +style = "pep440-post" +versionfile_source = "ams/_version.py" +versionfile_build = "ams/_version.py" +tag_prefix = "v" \ No newline at end of file From ce546511e20e17177ab6edb3146044d1b7031393 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 2 Sep 2024 14:21:34 -0400 Subject: [PATCH 002/181] Minimize setup.py to follow toml --- setup.cfg | 6 ---- setup.py | 104 ++---------------------------------------------------- 2 files changed, 2 insertions(+), 108 deletions(-) delete mode 100644 setup.cfg diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index ca7d7376..00000000 --- a/setup.cfg +++ /dev/null @@ -1,6 +0,0 @@ -[versioneer] -VCS = git -style = pep440-post -versionfile_source = ams/_version.py -versionfile_build = ams/_version.py -tag_prefix = v diff --git a/setup.py b/setup.py index 71bf24c3..08f0e6aa 100644 --- a/setup.py +++ b/setup.py @@ -1,108 +1,8 @@ -import re -import sys -import os -from collections import defaultdict - -from setuptools import find_packages, setup +from setuptools import setup import versioneer -if sys.version_info < (3, 6): - error = """ -ams does not support Python <= {0}.{1}. -Python 3.6 and above is required. Check your Python version like so: - -python3 --version - -This may be due to an out-of-date pip. Make sure you have pip >= 9.0.1. -Upgrade pip like so: - -pip install --upgrade pip -""".format(3, 6) - sys.exit(error) - -here = os.path.abspath(os.path.dirname(__file__)) - -with open(os.path.join(here, 'README.md'), encoding='utf-8') as readme_file: - readme = readme_file.read() - - -def parse_requires(filename): - with open(os.path.join(here, filename)) as requirements_file: - reqs = [ - line for line in requirements_file.read().splitlines() - if not line.startswith('#') - ] - return reqs - - -def get_extra_requires(filename, add_all=True): - """ - Build ``extras_require`` from an invert requirements file. - - See: - https://hanxiao.io/2019/11/07/A-Better-Practice-for-Managing-extras-require-Dependencies-in-Python/ - """ - - with open(os.path.join(here, filename)) as fp: - extra_deps = defaultdict(set) - for k in fp: - if k.strip() and not k.startswith('#'): - tags = set() - if '#' in k: - if k.count("#") > 1: - raise ValueError("Invalid line: {}".format(k)) - - k, v = k.split('#') - tags.update(vv.strip() for vv in v.split(',')) - - tags.add(re.split('[<=>]', k)[0]) - for t in tags: - extra_deps[t].add(k) - - # add tag `all` at the end - if add_all: - extra_deps['all'] = set(vv for v in extra_deps.values() for vv in v) - - return extra_deps - - -extras_require = get_extra_requires("requirements-extra.txt") - -# --- update `extras_conda` to include packages only available in PyPI --- - setup( - name='ltbams', version=versioneer.get_version(), - cmdclass=versioneer.get_cmdclass(), - description="Python software for scheduling modeling and co-simulation with dynanics.", - long_description=readme, - long_description_content_type='text/markdown', - author="Jinning Wang", - author_email='jinninggm@gmail.com', - url='https://github.com/CURENT/ams', - packages=find_packages(exclude=[]), - entry_points={ - 'console_scripts': [ - 'ams = ams.cli:main', - ], - }, - include_package_data=True, - package_data={ - 'ltbams': [ - # When adding files here, remember to update MANIFEST.in as well, - # or else they will not be included in the distribution on PyPI! - # 'path/to/data_file', - ] - }, - install_requires=parse_requires('requirements.txt'), - extras_require=extras_require, - license="GNU Public License v3", - classifiers=[ - 'Development Status :: 4 - Beta', - 'Natural Language :: English', - 'Programming Language :: Python :: 3', - 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', - 'Environment :: Console', - ], + cmdclass=versioneer.get_cmdclass() ) From f61f8d4d04dab7c5d46aae0fcaedf6a5353b3c47 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 2 Sep 2024 14:21:57 -0400 Subject: [PATCH 003/181] Add script to auto generate requirements --- genreq.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 genreq.py diff --git a/genreq.py b/genreq.py new file mode 100644 index 00000000..48404820 --- /dev/null +++ b/genreq.py @@ -0,0 +1,44 @@ +""" +Generate requirements from pyproject.toml. +""" + +import toml +from datetime import datetime + + +def write_req(): + """ + Write requirements from pyproject.toml to requirements.txt and requirements-extra.txt. + """ + with open('pyproject.toml', 'r') as f: + pyproject = toml.load(f) + + dependencies = pyproject['project']['dependencies'] + dev_dependencies = pyproject['project']['optional-dependencies']['dev'] + doc_dependencies = pyproject['project']['optional-dependencies']['doc'] + + # Get the current date + current_date = datetime.now().strftime("%Y-%m-%d") + + comment = f"# Generated on {current_date}.\n" + + # Overwrite requirements.txt + with open('requirements.txt', 'w') as f: + f.write(comment) + for dep in dependencies: + f.write(dep + '\n') + + # Overwrite requirements-extra.txt + with open('requirements-extra.txt', 'w') as f: + f.write(comment) + max_len = max(len(dep) for dep in dev_dependencies + doc_dependencies) + for dep in dev_dependencies: + f.write(f"{dep.ljust(max_len)} # dev\n") + for dep in doc_dependencies: + f.write(f"{dep.ljust(max_len)} # doc\n") + + print("Requirements files generated successfully.") + + +if __name__ == "__main__": + write_req() From 3ac398e6fd69d16449cc8956769f9a2a4a21f64d Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 2 Sep 2024 14:22:17 -0400 Subject: [PATCH 004/181] Update requirements files --- requirements-extra.txt | 38 +++++++++++++++----------------------- requirements.txt | 3 ++- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/requirements-extra.txt b/requirements-extra.txt index c08df06e..b76bb171 100644 --- a/requirements-extra.txt +++ b/requirements-extra.txt @@ -1,23 +1,15 @@ -# FORMAT -# Put your extra requirements here in the following format -# -# package[version_required] # tag1, tag2, ... -# -# Allow at least one space between the package name/version and '#' -# -# Only one `#` is allowed per line. Lines starting with `#` are ignored. - -pytest==7.4 # dev -pytest-cov # dev -coverage # dev -flake8 # dev -pyscipopt # dev -ipython # dev, doc -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 # dev, doc -nbsphinx # dev, doc \ No newline at end of file +# Generated on 2024-09-02. +pytest # dev +pytest-cov # dev +coverage # dev +flake8 # dev +ipython # dev +numpydoc # dev +toml # dev +sphinx # doc +pydata-sphinx-theme # doc +numpydoc # doc +sphinx-copybutton # doc +sphinx-panels # doc +myst-parser # doc +nbsphinx # doc diff --git a/requirements.txt b/requirements.txt index 6466e12f..a47dbeec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +# Generated on 2024-09-02. kvxopt>=1.3.2.1 numpy scipy @@ -8,4 +9,4 @@ psutil openpyxl andes>=1.8.7 pybind11 -cvxpy \ No newline at end of file +cvxpy From d468f376fca3a5aadc73f534ec5a02a977260097 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 2 Sep 2024 14:22:41 -0400 Subject: [PATCH 005/181] [WIP] Fit toml --- .codacy.yml | 1 + .codecov.yml | 1 + .readthedocs.yml | 5 +---- azure-pipelines.yml | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.codacy.yml b/.codacy.yml index 37c328e7..d5659f21 100644 --- a/.codacy.yml +++ b/.codacy.yml @@ -3,6 +3,7 @@ engines: flake8: enabled: true exclude_paths: + - "pyproject.toml" - "setup.py" - "versioneer.py" - "ams/pypower/**" diff --git a/.codecov.yml b/.codecov.yml index 266fe31a..9ab4894c 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -15,6 +15,7 @@ cli: codecov: token: 5fc2d41f-bcba-455d-a843-f85beb76d28f ignore: + - "pyproject.toml" - "setup.py" - "versioneer.py" - "ams/pypower/**" diff --git a/.readthedocs.yml b/.readthedocs.yml index c732d0ac..d7fd60a4 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -14,10 +14,7 @@ sphinx: python: install: - - requirements: requirements.txt - method: pip path: . extra_requirements: - - doc - - method: setuptools - path: . \ No newline at end of file + - docs \ No newline at end of file diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 1c81cb90..23724202 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -88,6 +88,6 @@ jobs: conda install -c conda-forge kvxopt python -m pip install --upgrade pip pip install pytest pytest-azurepipelines - pip install .[dev,interop] + pip install .[dev] pytest displayName: pytest \ No newline at end of file From e7d51de78d28c9015091c45817843305a6c4c993 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 2 Sep 2024 14:24:00 -0400 Subject: [PATCH 006/181] Update release-notes --- docs/source/release-notes.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 6f24fbb6..d8088fd1 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -19,6 +19,7 @@ v0.9.9 (2024-09-02) - Prepare to support NumPy v2.0.0, but solvers have unexpected behavior - Improve the logic of setting `Optz` value - Support NumPy v2.0.0 +- Transition to pyproject.toml for build configuration v0.9.8 (2024-06-18) ------------------- From d08f2061e12ab5dbb3755e329556067469f659a1 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 2 Sep 2024 14:43:43 -0400 Subject: [PATCH 007/181] Minor fix --- .codacy.yml | 1 + .codecov.yml | 1 + .readthedocs.yml | 5 ++++- pyproject.toml | 5 +++-- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.codacy.yml b/.codacy.yml index d5659f21..dbd04e54 100644 --- a/.codacy.yml +++ b/.codacy.yml @@ -6,6 +6,7 @@ exclude_paths: - "pyproject.toml" - "setup.py" - "versioneer.py" + - "genreq.py" - "ams/pypower/**" - "ams/_version.py" - ".github/**" diff --git a/.codecov.yml b/.codecov.yml index 9ab4894c..fc844ef6 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -18,6 +18,7 @@ ignore: - "pyproject.toml" - "setup.py" - "versioneer.py" + - "genreq.py" - "ams/pypower/**" - "ams/_version.py" - ".github/**" diff --git a/.readthedocs.yml b/.readthedocs.yml index d7fd60a4..c732d0ac 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -14,7 +14,10 @@ sphinx: python: install: + - requirements: requirements.txt - method: pip path: . extra_requirements: - - docs \ No newline at end of file + - doc + - method: setuptools + path: . \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index d54e84bf..3af06c86 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,18 +41,19 @@ dev = [ "pytest-cov", "coverage", "flake8", - "ipython", "numpydoc", "toml" ] doc = [ + "ipython", "sphinx", "pydata-sphinx-theme", "numpydoc", "sphinx-copybutton", "sphinx-panels", "myst-parser", - "nbsphinx" + "nbsphinx", + "pyscipopt " ] [project.scripts] From fd2cade6d4bd8ae1bc879d3fb969bcb18a922a21 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 2 Sep 2024 14:58:24 -0400 Subject: [PATCH 008/181] Rename genreq to genconf, include versioneer cfg writing --- genreq.py => genconf.py | 31 +++++++++++++++++++++++++------ pyproject.toml | 5 +++-- requirements-extra.txt | 4 +++- setup.cfg | 7 +++++++ 4 files changed, 38 insertions(+), 9 deletions(-) rename genreq.py => genconf.py (62%) create mode 100644 setup.cfg diff --git a/genreq.py b/genconf.py similarity index 62% rename from genreq.py rename to genconf.py index 48404820..da9d74b1 100644 --- a/genreq.py +++ b/genconf.py @@ -1,11 +1,16 @@ """ -Generate requirements from pyproject.toml. +Generate configurations from pyproject.toml. """ import toml from datetime import datetime +# Get the current date +current_date = datetime.now().strftime("%Y-%m-%d") +comment = f"# Generated on {current_date}.\n" + + def write_req(): """ Write requirements from pyproject.toml to requirements.txt and requirements-extra.txt. @@ -17,11 +22,6 @@ def write_req(): dev_dependencies = pyproject['project']['optional-dependencies']['dev'] doc_dependencies = pyproject['project']['optional-dependencies']['doc'] - # Get the current date - current_date = datetime.now().strftime("%Y-%m-%d") - - comment = f"# Generated on {current_date}.\n" - # Overwrite requirements.txt with open('requirements.txt', 'w') as f: f.write(comment) @@ -40,5 +40,24 @@ def write_req(): print("Requirements files generated successfully.") +def write_cfg(): + """ + Write versioneer configuration from pyproject.toml to setup.cfg. + """ + with open('pyproject.toml', 'r') as f: + pyproject = toml.load(f) + + versioneer = pyproject['tool']['versioneer'] + + with open('setup.cfg', 'w') as f: + f.write(comment) + f.write("[versioneer]\n") + for key, value in versioneer.items(): + f.write(f"{key} = {value}\n") + + print("Versioneer configuration generated successfully.") + + if __name__ == "__main__": write_req() + write_cfg() diff --git a/pyproject.toml b/pyproject.toml index 3af06c86..84cd36d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,8 @@ dev = [ "coverage", "flake8", "numpydoc", - "toml" + "toml", + "pyscipopt" ] doc = [ "ipython", @@ -53,7 +54,7 @@ doc = [ "sphinx-panels", "myst-parser", "nbsphinx", - "pyscipopt " + "pyscipopt" ] [project.scripts] diff --git a/requirements-extra.txt b/requirements-extra.txt index b76bb171..7f77c81a 100644 --- a/requirements-extra.txt +++ b/requirements-extra.txt @@ -3,9 +3,10 @@ pytest # dev pytest-cov # dev coverage # dev flake8 # dev -ipython # dev numpydoc # dev toml # dev +pyscipopt # dev +ipython # doc sphinx # doc pydata-sphinx-theme # doc numpydoc # doc @@ -13,3 +14,4 @@ sphinx-copybutton # doc sphinx-panels # doc myst-parser # doc nbsphinx # doc +pyscipopt # doc diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..a8c004bd --- /dev/null +++ b/setup.cfg @@ -0,0 +1,7 @@ +# Generated on 2024-09-02. +[versioneer] +VCS = git +style = pep440-post +versionfile_source = ams/_version.py +versionfile_build = ams/_version.py +tag_prefix = v From a715f30dd70d99e43a88614b57f74ab2dc31653d Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 2 Sep 2024 15:06:41 -0400 Subject: [PATCH 009/181] Typo --- .codacy.yml | 2 +- .codecov.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.codacy.yml b/.codacy.yml index dbd04e54..0773411c 100644 --- a/.codacy.yml +++ b/.codacy.yml @@ -6,7 +6,7 @@ exclude_paths: - "pyproject.toml" - "setup.py" - "versioneer.py" - - "genreq.py" + - "genconf.py" - "ams/pypower/**" - "ams/_version.py" - ".github/**" diff --git a/.codecov.yml b/.codecov.yml index fc844ef6..ed292896 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -17,8 +17,8 @@ codecov: ignore: - "pyproject.toml" - "setup.py" + - "genconf.py" - "versioneer.py" - - "genreq.py" - "ams/pypower/**" - "ams/_version.py" - ".github/**" From 54681d279ae94b96078966517db7054f82d33420 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Tue, 3 Sep 2024 23:11:57 -0400 Subject: [PATCH 010/181] Update release notes --- docs/source/release-notes.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index e90f6c22..69168276 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -9,6 +9,11 @@ The APIs before v3.0.0 are in beta and may change without prior notice. Pre-v1.0.0 ========== +v0.9.11 (2024-xx-xx) +-------------------- + +- [WIP] Fix circular import issue in `ams.io` + v0.9.10 (2024-09-03) -------------------- From 05e10a284098fdd361b4be811a690ef23fc84c86 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 4 Sep 2024 06:45:45 -0400 Subject: [PATCH 011/181] Update release notes --- docs/source/release-notes.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 69168276..8d44a2d6 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -12,14 +12,13 @@ Pre-v1.0.0 v0.9.11 (2024-xx-xx) -------------------- -- [WIP] Fix circular import issue in `ams.io` +- Add pyproject.toml for PEP 517 and PEP 518 compliance v0.9.10 (2024-09-03) -------------------- Hotfix of import issue in ``v0.9.9``. -Features developed in ``v0.9.9``: - In module `MatProcessor`, add two parameters `permc_spec` and `use_umfpack` in function `build_ptdf` - Follow RTD's deprecation of Sphinx context injection at build time - In MATPOWER conversion, set devices name as None From 8ff7bcf827dc4a52d52935af9f03ed242e283038 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 5 Sep 2024 14:19:57 -0400 Subject: [PATCH 012/181] Fix typo in doc --- docs/source/modeling/example.rst | 2 +- docs/source/modeling/routine.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/modeling/example.rst b/docs/source/modeling/example.rst index a774da7c..c804d801 100644 --- a/docs/source/modeling/example.rst +++ b/docs/source/modeling/example.rst @@ -104,7 +104,7 @@ Model Section # --- power balance --- pb = 'Bbus@aBus + Pbusinj + Cl@pd + Csh@gsh - Cg@pg' self.pb = Constraint(name='pb', info='power balance', - e_str=pb, type='eq',) + e_str=pb, is_eq=True,) # --- line flow --- self.plf = Var(info='Line flow', unit='p.u.', diff --git a/docs/source/modeling/routine.rst b/docs/source/modeling/routine.rst index 7fcb736e..6cf8dbd4 100644 --- a/docs/source/modeling/routine.rst +++ b/docs/source/modeling/routine.rst @@ -24,7 +24,7 @@ A simplified code snippet for RTED is shown below as an example. info='Sum Gen vars vector in shape of zone', no_parse=True, sparse=True) ... ... - self.rbu = Constraint(name='rbu', type='eq', + self.rbu = Constraint(name='rbu', is_eq=True, info='RegUp reserve balance', e_str = 'gs @ mul(ug, pru) - dud') ... ... From 5158c99a42ffb878fa0aa5bf8959066d4b1025bd Mon Sep 17 00:00:00 2001 From: jinningwang Date: Tue, 10 Sep 2024 14:18:21 -0400 Subject: [PATCH 013/181] Rename test_mats.py to test_matp.py --- tests/{test_mats.py => test_matp.py} | 4 ++++ 1 file changed, 4 insertions(+) rename tests/{test_mats.py => test_matp.py} (99%) diff --git a/tests/test_mats.py b/tests/test_matp.py similarity index 99% rename from tests/test_mats.py rename to tests/test_matp.py index c2008185..93ae31a3 100644 --- a/tests/test_mats.py +++ b/tests/test_matp.py @@ -1,3 +1,7 @@ +""" +Test module MatProcessor. +""" + import unittest import os From ed385370d8e2249ac0b6c0ec4ab35f3cd9c4d8f4 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Tue, 10 Sep 2024 14:39:14 -0400 Subject: [PATCH 014/181] Rename test_andes.py to test_interop.py for clarification --- tests/test_interop.py | 181 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 tests/test_interop.py diff --git a/tests/test_interop.py b/tests/test_interop.py new file mode 100644 index 00000000..791e3905 --- /dev/null +++ b/tests/test_interop.py @@ -0,0 +1,181 @@ +""" +Test interface. +""" + +import unittest +import numpy as np +import pkg_resources +from pkg_resources import parse_version + +import andes +import ams + +from ams.interop.andes import (build_group_table, make_link_table, to_andes, + parse_addfile, verify_pf) + + +class TestAndesConversion(unittest.TestCase): + """ + Tests conversion from AMS to ANDES. + """ + ad_cases = [ + 'ieee14/ieee14_full.xlsx', + 'ieee39/ieee39_full.xlsx', + 'npcc/npcc.xlsx', + ] + am_cases = [ + 'ieee14/ieee14.json', + 'ieee39/ieee39.xlsx', + 'npcc/npcc_uced.xlsx', + ] + + def setUp(self) -> None: + """ + Test setup. + """ + + def test_basic_functions(self): + """ + Test basic functions defined in ANDES Interop. + """ + for ad_case in self.ad_cases: + sa = andes.load(andes.get_case(ad_case), + setup=True, no_output=True, default_config=True,) + # --- test build_group_table --- + ssa_stg = build_group_table(adsys=sa, + grp_name='StaticGen', + param_name=['u', 'name', 'idx', 'bus'], + mdl_name=[]) + self.assertEqual(ssa_stg.shape, + (sa.StaticGen.n, 4)) + + ssa_gov = build_group_table(adsys=sa, grp_name='TurbineGov', + param_name=['idx', 'syn'], + mdl_name=[]) + self.assertEqual(ssa_gov.shape, + (sa.TurbineGov.n, 2)) + # --- test make_link_table --- + link_table = make_link_table(adsys=sa) + stg_idx = [str(i) for i in sa.PV.idx.v + sa.Slack.idx.v] + self.assertSetEqual(set(stg_idx), + set(link_table['stg_idx'].values)) + bus_idx = [str(i) for i in sa.PV.bus.v + sa.Slack.bus.v] + self.assertSetEqual(set(bus_idx), + set(link_table['bus_idx'].values)) + + def test_convert(self): + """ + Test conversion from AMS case to ANDES case. + """ + for ad_case, am_case in zip(self.ad_cases, self.am_cases): + sp = ams.load(ams.get_case(am_case), + setup=True, no_output=True, default_config=True,) + # before addfile + sa = to_andes(sp, setup=False) + self.assertEqual(set(sp.PV.idx.v), set(sa.PV.idx.v)) + self.assertEqual(set(sp.Bus.idx.v), set(sa.Bus.idx.v)) + self.assertEqual(set(sp.Line.idx.v), set(sa.Line.idx.v)) + self.assertEqual(np.sum(sp.PQ.p0.v), np.sum(sa.PQ.p0.v)) + + # after addfile + sa = parse_addfile(adsys=sa, amsys=sp, + addfile=andes.get_case(ad_case)) + sa.setup() + set1 = set(sa.GENROU.gen.v) + set2 = set(sp.StaticGen.get_idx()) + # set2 includes set1, ensure GENROU.gen are all in StaticGen.idx + self.assertEqual(set1, set1 & set2) + + # ensure PFlow models consistency + pflow_mdls = list(sa.PFlow.models.keys()) + for mdl in pflow_mdls: + self.assertTrue(sp.models[mdl].as_df().equals(sa.PFlow.models[mdl].as_df())) + + def test_convert_after_update(self): + """ + Test conversion from AMS case to ANDES case after updating parameters. + """ + for am_case in self.am_cases: + sp = ams.load(ams.get_case(am_case), + setup=True, no_output=True, default_config=True,) + # record initial values + pq_idx = sp.PQ.idx.v + p0 = sp.PQ.p0.v.copy() + sa = to_andes(sp, setup=False, no_output=True, default_config=True) + # before update + np.testing.assert_array_equal(sp.PQ.p0.v, sa.PQ.p0.v) + # after update + sp.PQ.alter(src='p0', idx=pq_idx, value=0.9*p0) + sa = to_andes(sp, setup=False, no_output=True, default_config=True) + np.testing.assert_array_equal(sp.PQ.p0.v, sa.PQ.p0.v) + + def test_extra_dyn(self): + """ + Test conversion when extra dynamic models exist. + """ + sp = ams.load(ams.get_case('ieee14/ieee14_uced.xlsx'), + setup=True, no_output=True, default_config=True,) + sa = to_andes(sp, addfile=andes.get_case('ieee14/ieee14_full.xlsx'), + setup=True, no_output=True, default_config=True, + verify=False, tol=1e-3) + self.assertGreaterEqual(sa.PVD1.n, 0) + + def test_verify_pf(self): + """ + Test verification of power flow results. + """ + sp = ams.load(ams.get_case('matpower/case300.m'), + setup=True, no_output=True, default_config=True,) + sa = to_andes(sp, + setup=True, no_output=True, default_config=True, + verify=False, tol=1e-3) + # NOTE: it is known that there is 1e-7~1e-6 diff in case300.m + self.assertFalse(verify_pf(amsys=sp, adsys=sa, tol=1e-6)) + self.assertTrue(verify_pf(amsys=sp, adsys=sa, tol=1e-3)) + + +class TestDataExchange(unittest.TestCase): + """ + Tests for data exchange between AMS and ANDES. + """ + + def setUp(self) -> None: + """ + Test setup. This is executed before each test case. + """ + self.sp = ams.load(ams.get_case('ieee14/ieee14_uced.xlsx'), + setup=True, + no_output=True, + default_config=True,) + self.sp.RTED.run(solver='CLARABEL') + self.sp.RTED.dc2ac() + self.stg_idx = self.sp.RTED.pg.get_idx() + + def test_data_exchange(self): + """ + Test data exchange between AMS and ANDES. + """ + sa = to_andes(self.sp, setup=True, + addfile=andes.get_case('ieee14/ieee14_full.xlsx'), + no_output=True, + default_config=True,) + # alleviate limiter + sa.TGOV1.set(src='VMAX', attr='v', idx=sa.TGOV1.idx.v, value=100*np.ones(sa.TGOV1.n)) + sa.TGOV1.set(src='VMIN', attr='v', idx=sa.TGOV1.idx.v, value=np.zeros(sa.TGOV1.n)) + + # --- test before PFlow --- + self.sp.dyn.send(adsys=sa, routine='RTED') + p0 = sa.StaticGen.get(src='p0', attr='v', idx=self.stg_idx) + pg = self.sp.RTED.get(src='pg', attr='v', idx=self.stg_idx) + np.testing.assert_array_equal(p0, pg) + + # --- test after TDS --- + self.assertFalse(self.sp.dyn.is_tds) + sa.PFlow.run() + sa.TDS.init() + self.assertTrue(self.sp.dyn.is_tds) + + sa.TDS.config.tf = 1 + sa.TDS.run() + self.sp.dyn.send(adsys=sa, routine='RTED') + self.sp.dyn.receive(adsys=sa, routine='RTED', no_update=False) From 13ae6e17cdfb5e53999d643d8fb77320606ac871 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Tue, 10 Sep 2024 14:40:40 -0400 Subject: [PATCH 015/181] Format --- tests/test_interop.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_interop.py b/tests/test_interop.py index 791e3905..d1083e3e 100644 --- a/tests/test_interop.py +++ b/tests/test_interop.py @@ -4,8 +4,6 @@ import unittest import numpy as np -import pkg_resources -from pkg_resources import parse_version import andes import ams From 43b7dbe3ab01b4314a71ff46107bdbb0586302a8 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Tue, 10 Sep 2024 14:47:01 -0400 Subject: [PATCH 016/181] Rename part of test_andes.py to test_andes_mats.py --- tests/test_andes.py | 232 --------------------------------------- tests/test_andes_mats.py | 61 ++++++++++ 2 files changed, 61 insertions(+), 232 deletions(-) delete mode 100644 tests/test_andes.py create mode 100644 tests/test_andes_mats.py diff --git a/tests/test_andes.py b/tests/test_andes.py deleted file mode 100644 index d72602f0..00000000 --- a/tests/test_andes.py +++ /dev/null @@ -1,232 +0,0 @@ -""" -Test ANDES interface. -""" - -import unittest -import numpy as np -import pkg_resources -from pkg_resources import parse_version - -import andes -import ams - -from ams.interop.andes import (build_group_table, make_link_table, to_andes, - parse_addfile, verify_pf) - - -class TestMatrices(unittest.TestCase): - """ - Tests for system matrices consistency. - """ - - andes_version = pkg_resources.get_distribution("andes").version - if parse_version(andes_version) < parse_version('1.9.2'): - raise unittest.SkipTest("Requires ANDES version >= 1.9.2") - - sp = ams.load(ams.get_case('5bus/pjm5bus_demo.xlsx'), - setup=True, no_output=True, default_config=True,) - sa = sp.to_andes(setup=True, no_output=True, default_config=True,) - - def setUp(self) -> None: - """ - Test setup. - """ - - def test_build_y(self): - """ - Test build_y consistency. - """ - ysp = self.sp.Line.build_y() - ysa = self.sa.Line.build_y() - np.testing.assert_equal(np.array(ysp.V), np.array(ysa.V)) - - def test_build_Bp(self): - """ - Test build_Bp consistency. - """ - Bp_sp = self.sp.Line.build_Bp() - Bp_sa = self.sa.Line.build_Bp() - np.testing.assert_equal(np.array(Bp_sp.V), np.array(Bp_sa.V)) - - def test_build_Bpp(self): - """ - Test build_Bpp consistency. - """ - Bpp_sp = self.sp.Line.build_Bpp() - Bpp_sa = self.sa.Line.build_Bpp() - np.testing.assert_equal(np.array(Bpp_sp.V), np.array(Bpp_sa.V)) - - def test_build_Bdc(self): - """ - Test build_Bdc consistency. - """ - Bdc_sp = self.sp.Line.build_Bdc() - Bdc_sa = self.sa.Line.build_Bdc() - np.testing.assert_equal(np.array(Bdc_sp.V), np.array(Bdc_sa.V)) - - -class TestInteropBase(unittest.TestCase): - """ - Tests for basic function of ANDES interface. - """ - ad_cases = [ - 'ieee14/ieee14_full.xlsx', - 'ieee39/ieee39_full.xlsx', - 'npcc/npcc.xlsx', - ] - am_cases = [ - 'ieee14/ieee14.json', - 'ieee39/ieee39.xlsx', - 'npcc/npcc_uced.xlsx', - ] - - def setUp(self) -> None: - """ - Test setup. - """ - - def test_basic_functions(self): - """ - Test basic functions defined in ANDES Interop. - """ - for ad_case in self.ad_cases: - sa = andes.load(andes.get_case(ad_case), - setup=True, no_output=True, default_config=True,) - # --- test build_group_table --- - ssa_stg = build_group_table(adsys=sa, - grp_name='StaticGen', - param_name=['u', 'name', 'idx', 'bus'], - mdl_name=[]) - self.assertEqual(ssa_stg.shape, - (sa.StaticGen.n, 4)) - - ssa_gov = build_group_table(adsys=sa, grp_name='TurbineGov', - param_name=['idx', 'syn'], - mdl_name=[]) - self.assertEqual(ssa_gov.shape, - (sa.TurbineGov.n, 2)) - # --- test make_link_table --- - link_table = make_link_table(adsys=sa) - stg_idx = [str(i) for i in sa.PV.idx.v + sa.Slack.idx.v] - self.assertSetEqual(set(stg_idx), - set(link_table['stg_idx'].values)) - bus_idx = [str(i) for i in sa.PV.bus.v + sa.Slack.bus.v] - self.assertSetEqual(set(bus_idx), - set(link_table['bus_idx'].values)) - - def test_convert(self): - """ - Test conversion from AMS case to ANDES case. - """ - for ad_case, am_case in zip(self.ad_cases, self.am_cases): - sp = ams.load(ams.get_case(am_case), - setup=True, no_output=True, default_config=True,) - # before addfile - sa = to_andes(sp, setup=False) - self.assertEqual(set(sp.PV.idx.v), set(sa.PV.idx.v)) - self.assertEqual(set(sp.Bus.idx.v), set(sa.Bus.idx.v)) - self.assertEqual(set(sp.Line.idx.v), set(sa.Line.idx.v)) - self.assertEqual(np.sum(sp.PQ.p0.v), np.sum(sa.PQ.p0.v)) - - # after addfile - sa = parse_addfile(adsys=sa, amsys=sp, - addfile=andes.get_case(ad_case)) - sa.setup() - set1 = set(sa.GENROU.gen.v) - set2 = set(sp.StaticGen.get_idx()) - # set2 includes set1, ensure GENROU.gen are all in StaticGen.idx - self.assertEqual(set1, set1 & set2) - - # ensure PFlow models consistency - pflow_mdls = list(sa.PFlow.models.keys()) - for mdl in pflow_mdls: - self.assertTrue(sp.models[mdl].as_df().equals(sa.PFlow.models[mdl].as_df())) - - def test_convert_after_update(self): - """ - Test conversion from AMS case to ANDES case after updating parameters. - """ - for am_case in self.am_cases: - sp = ams.load(ams.get_case(am_case), - setup=True, no_output=True, default_config=True,) - # record initial values - pq_idx = sp.PQ.idx.v - p0 = sp.PQ.p0.v.copy() - sa = to_andes(sp, setup=False, no_output=True, default_config=True) - # before update - np.testing.assert_array_equal(sp.PQ.p0.v, sa.PQ.p0.v) - # after update - sp.PQ.alter(src='p0', idx=pq_idx, value=0.9*p0) - sa = to_andes(sp, setup=False, no_output=True, default_config=True) - np.testing.assert_array_equal(sp.PQ.p0.v, sa.PQ.p0.v) - - def test_extra_dyn(self): - """ - Test conversion when extra dynamic models exist. - """ - sp = ams.load(ams.get_case('ieee14/ieee14_uced.xlsx'), - setup=True, no_output=True, default_config=True,) - sa = to_andes(sp, addfile=andes.get_case('ieee14/ieee14_full.xlsx'), - setup=True, no_output=True, default_config=True, - verify=False, tol=1e-3) - self.assertGreaterEqual(sa.PVD1.n, 0) - - def test_verify_pf(self): - """ - Test verification of power flow results. - """ - sp = ams.load(ams.get_case('matpower/case300.m'), - setup=True, no_output=True, default_config=True,) - sa = to_andes(sp, - setup=True, no_output=True, default_config=True, - verify=False, tol=1e-3) - # NOTE: it is known that there is 1e-7~1e-6 diff in case300.m - self.assertFalse(verify_pf(amsys=sp, adsys=sa, tol=1e-6)) - self.assertTrue(verify_pf(amsys=sp, adsys=sa, tol=1e-3)) - - -class TestDataExchange(unittest.TestCase): - """ - Tests for data exchange between AMS and ANDES. - """ - - def setUp(self) -> None: - """ - Test setup. This is executed before each test case. - """ - self.sp = ams.load(ams.get_case('ieee14/ieee14_uced.xlsx'), - setup=True, - no_output=True, - default_config=True,) - self.sp.RTED.run(solver='CLARABEL') - self.sp.RTED.dc2ac() - self.stg_idx = self.sp.RTED.pg.get_idx() - - def test_data_exchange(self): - """ - Test data exchange between AMS and ANDES. - """ - sa = to_andes(self.sp, setup=True, - addfile=andes.get_case('ieee14/ieee14_full.xlsx'), - no_output=True, - default_config=True,) - # alleviate limiter - sa.TGOV1.set(src='VMAX', attr='v', idx=sa.TGOV1.idx.v, value=100*np.ones(sa.TGOV1.n)) - sa.TGOV1.set(src='VMIN', attr='v', idx=sa.TGOV1.idx.v, value=np.zeros(sa.TGOV1.n)) - - # --- test before PFlow --- - self.sp.dyn.send(adsys=sa, routine='RTED') - p0 = sa.StaticGen.get(src='p0', attr='v', idx=self.stg_idx) - pg = self.sp.RTED.get(src='pg', attr='v', idx=self.stg_idx) - np.testing.assert_array_equal(p0, pg) - - # --- test after TDS --- - self.assertFalse(self.sp.dyn.is_tds) - sa.PFlow.run() - sa.TDS.init() - self.assertTrue(self.sp.dyn.is_tds) - - sa.TDS.config.tf = 1 - sa.TDS.run() - self.sp.dyn.send(adsys=sa, routine='RTED') - self.sp.dyn.receive(adsys=sa, routine='RTED', no_update=False) diff --git a/tests/test_andes_mats.py b/tests/test_andes_mats.py new file mode 100644 index 00000000..bdf89eca --- /dev/null +++ b/tests/test_andes_mats.py @@ -0,0 +1,61 @@ +""" +Test ANDES matrices. +""" + +import unittest +import numpy as np +import pkg_resources +from pkg_resources import parse_version + +import ams + + +class TestMatrices(unittest.TestCase): + """ + Tests for system matrices consistency. + """ + + andes_version = pkg_resources.get_distribution("andes").version + if parse_version(andes_version) < parse_version('1.9.2'): + raise unittest.SkipTest("Requires ANDES version >= 1.9.2") + + sp = ams.load(ams.get_case('5bus/pjm5bus_demo.xlsx'), + setup=True, no_output=True, default_config=True,) + sa = sp.to_andes(setup=True, no_output=True, default_config=True,) + + def setUp(self) -> None: + """ + Test setup. + """ + + def test_build_y(self): + """ + Test build_y consistency. + """ + ysp = self.sp.Line.build_y() + ysa = self.sa.Line.build_y() + np.testing.assert_equal(np.array(ysp.V), np.array(ysa.V)) + + def test_build_Bp(self): + """ + Test build_Bp consistency. + """ + Bp_sp = self.sp.Line.build_Bp() + Bp_sa = self.sa.Line.build_Bp() + np.testing.assert_equal(np.array(Bp_sp.V), np.array(Bp_sa.V)) + + def test_build_Bpp(self): + """ + Test build_Bpp consistency. + """ + Bpp_sp = self.sp.Line.build_Bpp() + Bpp_sa = self.sa.Line.build_Bpp() + np.testing.assert_equal(np.array(Bpp_sp.V), np.array(Bpp_sa.V)) + + def test_build_Bdc(self): + """ + Test build_Bdc consistency. + """ + Bdc_sp = self.sp.Line.build_Bdc() + Bdc_sa = self.sa.Line.build_Bdc() + np.testing.assert_equal(np.array(Bdc_sp.V), np.array(Bdc_sa.V)) From 647d487a2af5b2f7debe8687255307f69255e90d Mon Sep 17 00:00:00 2001 From: jinningwang Date: Tue, 24 Sep 2024 15:24:48 -0400 Subject: [PATCH 017/181] Add group ACShort --- ams/models/group.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ams/models/group.py b/ams/models/group.py index 15fb217e..bbb41437 100644 --- a/ams/models/group.py +++ b/ams/models/group.py @@ -210,6 +210,12 @@ def __init__(self): self.common_params.extend(('bus1', 'bus2', 'r', 'x')) +class ACShort(GroupBase): + def __init__(self): + super(ACShort, self).__init__() + self.common_params.extend(('bus1', 'bus2')) + + class StaticLoad(GroupBase): """ Static load group. From bd4729b15c8669984d949b693cb9e6f7960dfbf6 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Tue, 24 Sep 2024 15:25:05 -0400 Subject: [PATCH 018/181] Add model Jumper --- ams/models/jumper.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 ams/models/jumper.py diff --git a/ams/models/jumper.py b/ams/models/jumper.py new file mode 100644 index 00000000..c60b12db --- /dev/null +++ b/ams/models/jumper.py @@ -0,0 +1,34 @@ +""" +Jumper model for connecting two buses without impedance. +""" + +from andes.models.line.jumper import JumperData + +from ams.core.model import Model + + +class Jumper(JumperData, Model): + """ + Jumper is a device to short two buses (merging two buses into one). + + Jumper can connect two buses satisfying one of the following conditions: + + - neither bus is voltage-controlled + - either bus is voltage-controlled + - both buses are voltage-controlled, and the voltages are the same. + + If the buses are controlled in different voltages, power flow will + not solve (as the power flow through the jumper will be infinite). + + In the solutions, the ``p`` and ``q`` are flowing out of bus1 + and flowing into bus2. + + Setting a Jumper's connectivity status ``u`` to zero will disconnect the two + buses. In the case of a system split, one will need to call + ``System.connectivity()`` immediately following the split to detect islands. + """ + + def __init__(self, system=None, config=None) -> None: + JumperData.__init__(self) + Model.__init__(self, system, config) + self.group = 'ACShort' From 660a2c117a73d39a2980b14e431d10ce70336b18 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Tue, 24 Sep 2024 15:42:09 -0400 Subject: [PATCH 019/181] Add model Jumper --- ams/models/__init__.py | 2 +- ams/models/jumper.py | 34 ---------------------------------- ams/models/line.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 29 insertions(+), 35 deletions(-) delete mode 100644 ams/models/jumper.py diff --git a/ams/models/__init__.py b/ams/models/__init__.py index 4d938067..354bb2da 100644 --- a/ams/models/__init__.py +++ b/ams/models/__init__.py @@ -10,7 +10,7 @@ ('bus', ['Bus']), ('static', ['PQ', 'Slack', 'PV']), ('shunt', ['Shunt']), - ('line', ['Line']), + ('line', ['Line', 'Jumper']), ('distributed', ['PVD1', 'ESD1', 'EV1', 'EV2']), ('renewable', ['REGCA1', 'REGCV1', 'REGCV2']), ('area', ['Area']), diff --git a/ams/models/jumper.py b/ams/models/jumper.py deleted file mode 100644 index c60b12db..00000000 --- a/ams/models/jumper.py +++ /dev/null @@ -1,34 +0,0 @@ -""" -Jumper model for connecting two buses without impedance. -""" - -from andes.models.line.jumper import JumperData - -from ams.core.model import Model - - -class Jumper(JumperData, Model): - """ - Jumper is a device to short two buses (merging two buses into one). - - Jumper can connect two buses satisfying one of the following conditions: - - - neither bus is voltage-controlled - - either bus is voltage-controlled - - both buses are voltage-controlled, and the voltages are the same. - - If the buses are controlled in different voltages, power flow will - not solve (as the power flow through the jumper will be infinite). - - In the solutions, the ``p`` and ``q`` are flowing out of bus1 - and flowing into bus2. - - Setting a Jumper's connectivity status ``u`` to zero will disconnect the two - buses. In the case of a system split, one will need to call - ``System.connectivity()`` immediately following the split to detect islands. - """ - - def __init__(self, system=None, config=None) -> None: - JumperData.__init__(self) - Model.__init__(self, system, config) - self.group = 'ACShort' diff --git a/ams/models/line.py b/ams/models/line.py index 9d364df4..c137e035 100644 --- a/ams/models/line.py +++ b/ams/models/line.py @@ -1,4 +1,5 @@ from andes.models.line.line import LineData +from andes.models.line.jumper import JumperData from andes.core.param import NumParam from andes.shared import deg2rad, np, spmatrix @@ -208,3 +209,30 @@ def build_Bdc(self): Bdc[item, item] = 1e-6 return Bdc + + +class Jumper(JumperData, Model): + """ + Jumper is a device to short two buses (merging two buses into one). + + Jumper can connect two buses satisfying one of the following conditions: + + - neither bus is voltage-controlled + - either bus is voltage-controlled + - both buses are voltage-controlled, and the voltages are the same. + + If the buses are controlled in different voltages, power flow will + not solve (as the power flow through the jumper will be infinite). + + In the solutions, the ``p`` and ``q`` are flowing out of bus1 + and flowing into bus2. + + Setting a Jumper's connectivity status ``u`` to zero will disconnect the two + buses. In the case of a system split, one will need to call + ``System.connectivity()`` immediately following the split to detect islands. + """ + + def __init__(self, system=None, config=None) -> None: + JumperData.__init__(self) + Model.__init__(self, system, config) + self.group = 'ACShort' From 14bbe5eabf6aec979ae0962f793eb9a62e9a0b4b Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 25 Sep 2024 09:33:08 -0400 Subject: [PATCH 020/181] Minor refactor on pflow_dict --- ams/interop/andes.py | 50 +++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/ams/interop/andes.py b/ams/interop/andes.py index aa6cb80a..74a3437f 100644 --- a/ams/interop/andes.py +++ b/ams/interop/andes.py @@ -17,33 +17,31 @@ # Models used in ANDES PFlow +def create_entry(*fields): + """ + Helper function to create a list of fields for a model entry. + """ + return list(('idx', 'u', 'name')) + list(fields) + + pflow_dict = OrderedDict([ - ('Bus', ['idx', 'u', 'name', - 'Vn', 'vmax', 'vmin', - 'v0', 'a0', 'xcoord', 'ycoord', - 'area', 'zone', 'owner']), - ('PQ', ['idx', 'u', 'name', - 'bus', 'Vn', 'p0', 'q0', - 'vmax', 'vmin', 'owner']), - ('PV', ['idx', 'u', 'name', 'Sn', - 'Vn', 'bus', 'busr', 'p0', 'q0', - 'pmax', 'pmin', 'qmax', 'qmin', - 'v0', 'vmax', 'vmin', 'ra', 'xs']), - ('Slack', ['idx', 'u', 'name', 'Sn', - 'Vn', 'bus', 'busr', 'p0', 'q0', - 'pmax', 'pmin', 'qmax', 'qmin', - 'v0', 'vmax', 'vmin', 'ra', 'xs', - 'a0']), - ('Shunt', ['idx', 'u', 'name', 'Sn', - 'Vn', 'bus', 'g', 'b', 'fn']), - ('Line', ['idx', 'u', 'name', - 'bus1', 'bus2', 'Sn', - 'fn', 'Vn1', 'Vn2', - 'r', 'x', 'b', 'g', 'b1', 'g1', 'b2', 'g2', - 'trans', 'tap', 'phi', - 'rate_a', 'rate_b', 'rate_c', - 'owner', 'xcoord', 'ycoord']), - ('Area', ['idx', 'u', 'name']), + ('Bus', create_entry('Vn', 'vmax', 'vmin', 'v0', 'a0', + 'xcoord', 'ycoord', 'area', 'zone', + 'owner')), + ('PQ', create_entry('bus', 'Vn', 'p0', 'q0', 'vmax', + 'vmin', 'owner')), + ('PV', create_entry('Sn', 'Vn', 'bus', 'busr', 'p0', 'q0', + 'pmax', 'pmin', 'qmax', 'qmin', + 'v0', 'vmax', 'vmin', 'ra', 'xs')), + ('Slack', create_entry('Sn', 'Vn', 'bus', 'busr', 'p0', 'q0', + 'pmax', 'pmin', 'qmax', 'qmin', + 'v0', 'vmax', 'vmin', 'ra', 'xs', 'a0')), + ('Shunt', create_entry('Sn', 'Vn', 'bus', 'g', 'b', 'fn')), + ('Line', create_entry('bus1', 'bus2', 'Sn', 'fn', 'Vn1', 'Vn2', + 'r', 'x', 'b', 'g', 'b1', 'g1', 'b2', 'g2', + 'trans', 'tap', 'phi', 'rate_a', 'rate_b', + 'rate_c', 'owner', 'xcoord', 'ycoord')), + ('Area', create_entry()), ]) # dict for guessing dynamic models given its idx From 0d9b69dec3e5a45dda015593a612cfcadc4b7055 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 25 Sep 2024 09:35:24 -0400 Subject: [PATCH 021/181] Include Jumper in ANDES conversion --- ams/interop/andes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ams/interop/andes.py b/ams/interop/andes.py index 74a3437f..c6202063 100644 --- a/ams/interop/andes.py +++ b/ams/interop/andes.py @@ -42,6 +42,7 @@ def create_entry(*fields): 'trans', 'tap', 'phi', 'rate_a', 'rate_b', 'rate_c', 'owner', 'xcoord', 'ycoord')), ('Area', create_entry()), + ('Jumper', create_entry('bus1', 'bus2')), ]) # dict for guessing dynamic models given its idx From b4552dac156365e9f527a57a3e975a2fec0d24b5 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 25 Sep 2024 09:36:35 -0400 Subject: [PATCH 022/181] Add a Jumper case --- ams/cases/5bus/pjm5bus_jumper.xlsx | Bin 0 -> 28377 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 ams/cases/5bus/pjm5bus_jumper.xlsx diff --git a/ams/cases/5bus/pjm5bus_jumper.xlsx b/ams/cases/5bus/pjm5bus_jumper.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..ce39842090bdada4b163b911cc0f7890cdee19f0 GIT binary patch literal 28377 zcmeFZWpG?=mLx1@W@ct)X0Vuuk7xberIQT;){*w ziH+E;A0>E7_m*xwc{0z*tWuN(1w#XZ0D=Mn0wM-7NkaKq3Je59g8&4C3IqkBBkEx9 zYG&_hpz7sl=Ay^oX=h7R1O`G?00aVf{QtZCUwj3|)Abbkn9#y53E^feC*eY3j?=eH!sad+eV=S(E-_$Ud23U^Adhaz47mP*uQM10NT4P)= z&WcoCcZ~~G*y_I^45;I7_F(|k)^67gu`}+adB^*4*;;8XW(sG0z=p=}9+GUeN%T1d zhwNZLOCmxKHya3(%>cHROPblAfPS3he7 z{|q+JiTre1RtIj8`dQc2d8JTVA4Dvg176zU5grKW;{y~(@jsxuS&fPG7O-VM0l*6j zK(~RjnXL;W!|(h5hwA@}q5PMBy*f!wp`QsZ>_X}_eB|NRS^|=YtcS2vC$Xwufb=S2 zb8Img?phBe9+E0fFsP*eH^0{(D{FkQ$0Nk|+ica*C}_N-Egm&t>CaAXkTev|X%bG= zTLZ}MzwUnBXG+R=QMrGMr>SbID3l%DAeETA601j=V$i^WMlHn)!{E;h)E<)4S~q^I z1^y+Xa#j;o*UDLNoII87zmQgT1TPrD{poZz6Me+R*kY;HXT+BH?g>Xt#gf~q&N$DR zhs4Xk)b{(0SauKUn-9Hw&WJJz8_FZgl;kjZ!JWT$6Wi5TuGau3bbr;z9WEnAa(6Yym5sa6mvfKv2M*wv2zniHC!;jj@A+&2JL?H_QM7m=N&T|GRHp>0|c6 zOi1Dnsn3WbFUFrxl2ik2zsM}L96@R_@uAfvo(Vj@XsgOaqRBN1H43v}>{PjY<@ap; zI9}cajj?Nz$%G=s2>9VPQ$b_qFnkaW4_lr5C80(=p#hj~#p1!E6VtNvhH$Cl$ZoTW zD!XC1{XNkodtq7{_n06hpSt;|2^`CGqg*pq3z=O~cKAY1 zVIf^@9vN9E5uAkl(-Q0lMP9ScvNjHte(u#n?9s)q+q?qwo8RUGYvt`$0QpJzD;x1P#mWqt`#iQnM2Jqe zu<3(AG&Vr-q2|H|MeyEp-iWk`y8wJWZrcMhgxekiHXgbn+ z+C?>!th-RXF>^8@c%>Ctp+~1QtGqgMo9}o0o$p&GeTXZLjz2hi)pyjOVZA6Cn!A9l zUD-Oe1Y3}YFp0U?OdRo5aGjnfQ^Hc!R^Lk0><9PqM(f_@Y_XK9M$i$19;933&9Y76 zAZ#$^4or@5Ok2H)B9&=4Vl$w)qcciorce9qTFMVXTdXd9I7hYMIv3AP7Q~8aatYg! zXV^zg+C#(`j(`uF@XLinjAKlsy=2I8%_~Fr_@3?>xjAuw9$d49!*E@m6(+et`+{CU z47Q|1;;RQp6Lm-ruBOElKyoDg8`(mw*OFa>Rh3ARBDOJ6gu#!iodcV6$tU_A372F< z(CJi{yt-GZW*Ja2+Yc*SY-`|otEBItcj;hY+f^rJA8tP8zWi(XBE_!7 zkpBGcbDfmP==xDob(pzi<^>gQ_{4GGPs@*PTUwi%cz(OjO}D}`J&Qakp9DV&B<;uW`D5{j*C2=zVf z<>kuYkC9~jy$)m^52e};8xOit9IS0UC6AMzCRpk?7@v(o&wkarbWm$wa9b1Ef?q6b z^~kXj-(eXX8gY`aeBCjk$iK6JYJ4CL+C0JB<^zituzwm-eln)3V&H$q z{I4p8d<+|!JfLC-02(~3e^d+?OEWW97sfwd*nSgSPO_ZC%5NP2dQS+phPouL-6WA? zY&lc&9bvGe#zMTzggEI?`iDmN+rz9(vg-(W#TKG=3E?-k(b0(^oE8v5wvMQS{uowA z7@Mh}mDW1;J4c#FyDe2MWpT&*GNAB&0#G=nf{QGQ>gFy+4=+5b(Tk zZq+3|-?g4CFpqJN}}> zF;an<)co+aGbdeB+2^Z14xk=JuP45JV;TT8-WV;ImP$a9$t49z9Dr=G|zu$3A-?U7{amd$b&9Cr6MVW`b_tp`?e+9hYA0L12Qqsok`v9En4{jEJ zLxzS%!Y*XxNgg^~i!6Q|r4BZ8e{u4YBl0U`bDhY71t04@zNl*V*+coveN$}l&Wptf zSDkK^nx8Eq`N#Z&Pu1NX#ERa>op%$X$4kGJA@_LWwxve>1Ntj&-(6DG!Qvh13!db? zJV|6E7Y{QT$4q3Rv?mWU1(ei{A&KH~2o>4%4Ihe^F1~c69uE%-d4i&(xfsczmpsw* zOps7PF?pa+duF6ibGx)q7G-h(9qqQF57!F&;P-koF;T_yFrkj;CqdbPdRwdA^fKcc zqxMfR9%BQlj_l08sslt(yWKO3k!qba;qeI>Gh)o8${|IGMe%aL3cUpmllRT-p&pWx zz(0k;@J^UQKsLDibO$yt|V~tT5#}_P>Kf~ z3rC^?pKVBY7S7(1Sk^+H^N?Ne8H^5m-u+{f<@(Y&73{1eN`zOB+Fhk@U_pgel@DVS*bW%IK9r8^wxth_eVJH!}Lzq|pFKUiqD{i_LOX zLrcoSzPEAmF(HErRl|nT=@sA(Mp8t|^e

y3(D>Jlz7OvY|!rofY6s(Rv@PFMp)) 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 A`v3p{ literal 0 HcmV?d00001 From be5c8aa651d5b84fe3b65ac8cd35b6ac5230ff9c Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 25 Sep 2024 09:44:40 -0400 Subject: [PATCH 023/181] Add tests for jumper --- tests/test_jumper.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 tests/test_jumper.py diff --git a/tests/test_jumper.py b/tests/test_jumper.py new file mode 100644 index 00000000..9395ae97 --- /dev/null +++ b/tests/test_jumper.py @@ -0,0 +1,27 @@ +""" +Test `Jumper` class. +""" + +import unittest + +import ams + + +class TessJumper(unittest.TestCase): + """ + Test `Jumper` class. + """ + + def setUp(self) -> None: + """ + Test setup. + """ + self.sp = ams.load(ams.get_case("5bus/pjm5bus_jumper.xlsx"), + setup=True, no_output=True, default_config=True) + + def test_to_andes(self): + """ + Test `to_andes` method when `Jumper` exists. + """ + sa = self.sp.to_andes() + self.assertEqual(sa.Jumper.n, 1) From 02dbb1f1be35a9d81d4b509a573c668af0bdcc4b Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 25 Sep 2024 10:37:28 -0400 Subject: [PATCH 024/181] Move helper function create_entry into module utils --- ams/interop/andes.py | 10 ++-------- ams/utils/__init__.py | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/ams/interop/andes.py b/ams/interop/andes.py index c6202063..b4094032 100644 --- a/ams/interop/andes.py +++ b/ams/interop/andes.py @@ -10,6 +10,7 @@ from andes.utils.misc import elapsed from andes.system import System as andes_System +from ams.utils import create_entry from ams.io import input_formats from ams.shared import nan @@ -17,13 +18,6 @@ # Models used in ANDES PFlow -def create_entry(*fields): - """ - Helper function to create a list of fields for a model entry. - """ - return list(('idx', 'u', 'name')) + list(fields) - - pflow_dict = OrderedDict([ ('Bus', create_entry('Vn', 'vmax', 'vmin', 'v0', 'a0', 'xcoord', 'ycoord', 'area', 'zone', @@ -930,7 +924,7 @@ def make_link_table(adsys): ssa_key0 = pd.merge(left=ssa_key0, how='left', on='stg_idx', right=ssa_rg[['stg_idx', 'rg_idx']]) - ssa_key0 = ssa_key0.fillna(value=False) + ssa_key0 = ssa_key0.fillna(value=False).infer_objects(copy=False) dyr = ssa_key0['syg_idx'].astype(bool) + ssa_key0['dg_idx'].astype(bool) + ssa_key0['rg_idx'].astype(bool) non_dyr = np.logical_not(dyr) ssa_dyr0 = ssa_key0[non_dyr] diff --git a/ams/utils/__init__.py b/ams/utils/__init__.py index e4a4f908..99609706 100644 --- a/ams/utils/__init__.py +++ b/ams/utils/__init__.py @@ -11,3 +11,23 @@ def wrapper(*args, **kwargs): _, s = elapsed(t0) return result, s return wrapper + + +def create_entry(*fields, three_params=True): + """ + Helper function to create a list of fields for a model entry. + + Parameters + ---------- + fields : tuple + Additional fields to include in the list. + three_params : bool, optional + Whether to include 'idx', 'u', 'name' in the list (default is True). + + Returns + ------- + list + A list of fields for the model entry. + """ + base_fields = ['idx', 'u', 'name'] if three_params else [] + return base_fields + list(fields) From 7d4badbbb5c9a50cb0ae77f899671ddaafc9fb9f Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 25 Sep 2024 13:14:24 -0400 Subject: [PATCH 025/181] Avoid using pandas fillna --- ams/interop/andes.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ams/interop/andes.py b/ams/interop/andes.py index b4094032..33a59c57 100644 --- a/ams/interop/andes.py +++ b/ams/interop/andes.py @@ -924,8 +924,11 @@ def make_link_table(adsys): ssa_key0 = pd.merge(left=ssa_key0, how='left', on='stg_idx', right=ssa_rg[['stg_idx', 'rg_idx']]) - ssa_key0 = ssa_key0.fillna(value=False).infer_objects(copy=False) - dyr = ssa_key0['syg_idx'].astype(bool) + ssa_key0['dg_idx'].astype(bool) + ssa_key0['rg_idx'].astype(bool) + # NOTE: use this instead of fillna to avoid type conversion + idxc = ['stg_idx', 'syg_idx', 'dg_idx', 'rg_idx'] + ssa_key0[idxc] = ssa_key0[idxc].astype('str').replace({'nan': ''}).astype('bool') + + dyr = ssa_key0['syg_idx'] + ssa_key0['dg_idx'] + ssa_key0['rg_idx'] non_dyr = np.logical_not(dyr) ssa_dyr0 = ssa_key0[non_dyr] ssa_dyr0['gammap'] = 1 From b483674d90f94c86c7c8a88913c0fb518d509b0f Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 25 Sep 2024 13:19:49 -0400 Subject: [PATCH 026/181] Fix deprecation related to pkg_resources --- tests/test_andes_mats.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_andes_mats.py b/tests/test_andes_mats.py index bdf89eca..eb27b560 100644 --- a/tests/test_andes_mats.py +++ b/tests/test_andes_mats.py @@ -4,8 +4,8 @@ import unittest import numpy as np -import pkg_resources -from pkg_resources import parse_version +import importlib.metadata +from packaging.version import parse as parse_version import ams @@ -15,7 +15,7 @@ class TestMatrices(unittest.TestCase): Tests for system matrices consistency. """ - andes_version = pkg_resources.get_distribution("andes").version + andes_version = importlib.metadata.version("andes") if parse_version(andes_version) < parse_version('1.9.2'): raise unittest.SkipTest("Requires ANDES version >= 1.9.2") From 3cb00efb46968324b6b9a651f71c7a7f0f787333 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 25 Sep 2024 13:32:34 -0400 Subject: [PATCH 027/181] Fix deprecation related to numpy newshape --- 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 0b156388..ca07c010 100644 --- a/ams/routines/rted.py +++ b/ams/routines/rted.py @@ -78,11 +78,11 @@ def __init__(self): model='SFR', src='dd', unit='%', no_parse=True,) self.dud = NumOpDual(u=self.pdz, u2=self.du, fun=np.multiply, - rfun=np.reshape, rargs=dict(newshape=(-1,)), + rfun=np.reshape, rargs=dict(shape=(-1,)), name='dud', tex_name=r'd_{u, d}', info='zonal RegUp reserve requirement',) self.ddd = NumOpDual(u=self.pdz, u2=self.dd, fun=np.multiply, - rfun=np.reshape, rargs=dict(newshape=(-1,)), + rfun=np.reshape, rargs=dict(shape=(-1,)), name='ddd', tex_name=r'd_{d, d}', info='zonal RegDn reserve requirement',) # --- SFR --- From cacd1a0ddc3f1263463f7d9ec90452f4e5796238 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 25 Sep 2024 13:36:28 -0400 Subject: [PATCH 028/181] Specify solver in test_dctypes --- tests/test_dctypes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_dctypes.py b/tests/test_dctypes.py index 478e1718..66250e27 100644 --- a/tests/test_dctypes.py +++ b/tests/test_dctypes.py @@ -11,7 +11,7 @@ def require_MIP_solver(f): """ def wrapper(*args, **kwargs): all_solvers = cp.installed_solvers() - mip_solvers = ['CPLEX', 'GUROBI', 'MOSEK'] + mip_solvers = ['SCIP', 'CPLEX', 'GUROBI', 'MOSEK'] if any(s in mip_solvers for s in all_solvers): pass else: @@ -65,7 +65,7 @@ def test_rtedes(self) -> None: """ init = self.ss.RTEDES.init() self.assertTrue(init, "RTEDES initialization failed!") - self.ss.RTEDES.run() + self.ss.RTEDES.run(solver='SCIP') np.testing.assert_equal(self.ss.RTEDES.exit_code, 0) @require_MIP_solver @@ -75,7 +75,7 @@ def test_edes(self) -> None: """ init = self.ss.EDES.init() self.assertTrue(init, "EDES initialization failed!") - self.ss.EDES.run() + self.ss.EDES.run(solver='SCIP') np.testing.assert_equal(self.ss.EDES.exit_code, 0) From 3954245032e846d570c03911a57aab935cdd75e4 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 25 Sep 2024 13:37:57 -0400 Subject: [PATCH 029/181] Update release notes --- docs/source/release-notes.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 8d44a2d6..6d5c31c3 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -13,6 +13,8 @@ v0.9.11 (2024-xx-xx) -------------------- - Add pyproject.toml for PEP 517 and PEP 518 compliance +- Add model `Jumper` +- Fix deprecation warning related to `pandas.fillna` and `newshape` in NumPy v0.9.10 (2024-09-03) -------------------- From 125aeb3458413caceea7eef750c25717b7d8f7c7 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 30 Oct 2024 13:36:59 -0400 Subject: [PATCH 030/181] Ignore folder icebar in flake8 lint --- .flake8 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.flake8 b/.flake8 index 48c00266..893e1277 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,3 @@ [flake8] -exclude = .git,__pycache__,build,dist,demos,cases,dev,ams/solver/pypower/,cards,versioneer.py,ams/_version.py,docs/source/conf.py,docs/source/_build,benchmarks,andes/pycode,scripts,.history,ams/pypower, +exclude = .git,__pycache__,build,dist,demos,cases,dev,ams/solver/pypower/,cards,versioneer.py,ams/_version.py,docs/source/conf.py,docs/source/_build,benchmarks,andes/pycode,scripts,.history,ams/pypower,icebar/* max-line-length=115 \ No newline at end of file From 87ffeb32cf91fa23b437ec702ed59d120318cd82 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 30 Oct 2024 13:38:51 -0400 Subject: [PATCH 031/181] In module shared, make INSTALLED_SOLVE and MIP_SOLVERS lowercase --- ams/shared.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/ams/shared.py b/ams/shared.py index a9ec87e2..a40bc5d6 100644 --- a/ams/shared.py +++ b/ams/shared.py @@ -23,16 +23,18 @@ inf = np.inf nan = np.nan -# NOTE: copied from CVXPY documentation -MIP_SOLVERS = ['CBC', 'COPT', 'GLPK_MI', 'CPLEX', 'GUROBI', - 'MOSEK', 'SCIP', 'XPRESS', 'SCIPY'] - -INSTALLED_SOLVERS = cp.installed_solvers() - # NOTE: copyright year_end = datetime.now().year copyright_msg = f'Copyright (C) 2023-{year_end} Jinning Wang' +# NOTE: copied from CVXPY documentation, last checked on 2024/10/30, v1.5 +mip_solvers = ['CBC', 'COPT', 'GLPK_MI', 'CPLEX', 'GUROBI', + 'MOSEK', 'SCIP', 'XPRESS', 'SCIPY'] + +installed_solvers = cp.installed_solvers() + +installed_mip_solvers = [s for s in installed_solvers if s in mip_solvers] + def require_MIP_solver(f): """ @@ -41,7 +43,7 @@ def require_MIP_solver(f): @wraps(f) def wrapper(*args, **kwargs): - if not any(s in MIP_SOLVERS for s in INSTALLED_SOLVERS): + if not any(s in mip_solvers for s in installed_solvers): raise ModuleNotFoundError("No MIP solver is available.") return f(*args, **kwargs) From 2cb943db3f6e2f4a9889f40132e6145ec9f1047e Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 31 Oct 2024 14:20:17 -0400 Subject: [PATCH 032/181] Fix parameter name for np.reshape: shape to newshape --- 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 ca07c010..0b156388 100644 --- a/ams/routines/rted.py +++ b/ams/routines/rted.py @@ -78,11 +78,11 @@ def __init__(self): model='SFR', src='dd', unit='%', no_parse=True,) self.dud = NumOpDual(u=self.pdz, u2=self.du, fun=np.multiply, - rfun=np.reshape, rargs=dict(shape=(-1,)), + rfun=np.reshape, rargs=dict(newshape=(-1,)), name='dud', tex_name=r'd_{u, d}', info='zonal RegUp reserve requirement',) self.ddd = NumOpDual(u=self.pdz, u2=self.dd, fun=np.multiply, - rfun=np.reshape, rargs=dict(shape=(-1,)), + rfun=np.reshape, rargs=dict(newshape=(-1,)), name='ddd', tex_name=r'd_{d, d}', info='zonal RegDn reserve requirement',) # --- SFR --- From cdbc297aa2923ab88d9a6716b7e6629af7236a50 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 31 Oct 2024 15:48:59 -0400 Subject: [PATCH 033/181] Revise default minimum ON/OFF duration time for generators to be 1 and 0.5 respectively --- ams/models/static/gen.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ams/models/static/gen.py b/ams/models/static/gen.py index dabe42fc..baae9c8f 100644 --- a/ams/models/static/gen.py +++ b/ams/models/static/gen.py @@ -65,12 +65,12 @@ def __init__(self) -> None: tex_name=r'p_{g0}', unit='p.u.', ) - self.td1 = NumParam(default=0, + self.td1 = NumParam(default=1, info='minimum ON duration', tex_name=r't_{d1}', unit='h', ) - self.td2 = NumParam(default=0, + self.td2 = NumParam(default=0.5, info='minimum OFF duration', tex_name=r't_{d2}', unit='h', From 518a83f01cb80e0852e28b17142d29a04ef4c60e Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 31 Oct 2024 16:27:33 -0400 Subject: [PATCH 034/181] Add params uenf and uf to model Gen --- ams/models/static/gen.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/ams/models/static/gen.py b/ams/models/static/gen.py index baae9c8f..1f3fb436 100644 --- a/ams/models/static/gen.py +++ b/ams/models/static/gen.py @@ -16,7 +16,16 @@ class GenParam: def __init__(self) -> None: self.ctrl = NumParam(default=1, info="generator controllability", - tex_name=r'c_{trl}',) + tex_name=r'c_{trl}', + unit='bool',) + self.uenf = NumParam(default=0, + info="Indicates if on/off status is enforced (1 for enforced, 0 for not enforced)", + tex_name=r'u_{enf}', + unit='bool',) + self.uf = NumParam(default=1, + info="enforced on/off status", + tex_name=r'u_{d,f}', + unit='bool',) self.Pc1 = NumParam(default=0.0, info="lower real power output of PQ capability curve", tex_name=r'P_{c1}', From dd9e81e3f7d4835a1985d86cd5e5ad500488aef2 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 31 Oct 2024 16:38:04 -0400 Subject: [PATCH 035/181] Refactor params uenf and uf into one param udf in model Gen --- ams/models/static/gen.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/ams/models/static/gen.py b/ams/models/static/gen.py index 1f3fb436..4a982c81 100644 --- a/ams/models/static/gen.py +++ b/ams/models/static/gen.py @@ -18,14 +18,11 @@ def __init__(self) -> None: info="generator controllability", tex_name=r'c_{trl}', unit='bool',) - self.uenf = NumParam(default=0, - info="Indicates if on/off status is enforced (1 for enforced, 0 for not enforced)", - tex_name=r'u_{enf}', - unit='bool',) - self.uf = NumParam(default=1, - info="enforced on/off status", - tex_name=r'u_{d,f}', - unit='bool',) + self.udf = NumParam(default=1, + info="enforced on/off status; 0 unenforced, 1 enforced on, -1 enforced off", + tex_name=r'u_{d,f}', + unit='int', + vrange=(-1, 1)) self.Pc1 = NumParam(default=0.0, info="lower real power output of PQ capability curve", tex_name=r'P_{c1}', From 17f5dbcbeee212f7023fce9a4513da038b5debc1 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 31 Oct 2024 18:27:10 -0400 Subject: [PATCH 036/181] Set gen param uenf default as zero for unenforced --- ams/models/static/gen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ams/models/static/gen.py b/ams/models/static/gen.py index 4a982c81..1543768b 100644 --- a/ams/models/static/gen.py +++ b/ams/models/static/gen.py @@ -18,7 +18,7 @@ def __init__(self) -> None: info="generator controllability", tex_name=r'c_{trl}', unit='bool',) - self.udf = NumParam(default=1, + self.udf = NumParam(default=0, info="enforced on/off status; 0 unenforced, 1 enforced on, -1 enforced off", tex_name=r'u_{d,f}', unit='int', From 6679013a32d73f00b07528197a158cfb66fce39c Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 31 Oct 2024 20:31:55 -0400 Subject: [PATCH 037/181] Rename gen param udf as uf --- ams/models/static/gen.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ams/models/static/gen.py b/ams/models/static/gen.py index 1543768b..fe6ff619 100644 --- a/ams/models/static/gen.py +++ b/ams/models/static/gen.py @@ -18,11 +18,11 @@ def __init__(self) -> None: info="generator controllability", tex_name=r'c_{trl}', unit='bool',) - self.udf = NumParam(default=0, - info="enforced on/off status; 0 unenforced, 1 enforced on, -1 enforced off", - tex_name=r'u_{d,f}', - unit='int', - vrange=(-1, 1)) + self.uf = NumParam(default=0, + info="enforced on/off status; 0 unenforced, 1 enforced on, -1 enforced off", + tex_name=r'u_{f}', + unit='int', + vrange=(-1, 1)) self.Pc1 = NumParam(default=0.0, info="lower real power output of PQ capability curve", tex_name=r'P_{c1}', From efd4abfdf766873c542fc5657014be5c4d62cf3b Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 31 Oct 2024 21:08:08 -0400 Subject: [PATCH 038/181] Typo --- ams/models/reserve.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ams/models/reserve.py b/ams/models/reserve.py index 59aee826..04022daa 100644 --- a/ams/models/reserve.py +++ b/ams/models/reserve.py @@ -29,10 +29,10 @@ def __init__(self, system, config): self.group = 'Reserve' self.du = NumParam(default=0, info='Zonal RegUp reserve demand', - tex_name=r'd_{u}', unit='%',) + tex_name=r'd_{u}',) self.dd = NumParam(default=0, info='Zonal RegDown reserve demand', - tex_name=r'd_{d}', unit='%',) + tex_name=r'd_{d}',) class SR(ReserveData, Model): @@ -49,7 +49,7 @@ def __init__(self, system, config): ReserveData.__init__(self) Model.__init__(self, system, config) self.group = 'Reserve' - self.demand = NumParam(default=0.1, unit='%', + self.demand = NumParam(default=0.1, info='Zonal spinning reserve demand', tex_name=r'd_{SR}') @@ -68,7 +68,7 @@ def __init__(self, system, config): ReserveData.__init__(self) Model.__init__(self, system, config) self.group = 'Reserve' - self.demand = NumParam(default=0.1, unit='%', + self.demand = NumParam(default=0.1, info='Zonal non-spinning reserve demand', tex_name=r'd_{NSR}') From 35dc3641bdfa5e3b56f27a349c04f25314657ba2 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 08:23:32 -0400 Subject: [PATCH 039/181] Improve logging --- ams/core/matprocessor.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ams/core/matprocessor.py b/ams/core/matprocessor.py index e4b27bcd..c091dab6 100644 --- a/ams/core/matprocessor.py +++ b/ams/core/matprocessor.py @@ -186,7 +186,7 @@ def __init__(self, system): info='Line outage distribution factor', v=None, sparse=False, owner=self) - def build(self): + def build(self, force_mats=False): """ Build the system matrices. It build connectivity matrices first: Cg, Cl, Csh, Cft, and CftT. @@ -197,8 +197,11 @@ def build(self): initialized : bool True if the matrices are built successfully. """ - t_mat, _ = elapsed() + if not force_mats and self.initialized: + logger.debug("System matrices are already built.") + return self.initialized + t_mat, _ = elapsed() # --- connectivity matrices --- _ = self.build_cg() _ = self.build_cl() From 44302f4d86a983c230904fe0cb55ab269e287365 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 08:23:40 -0400 Subject: [PATCH 040/181] Improve logging --- ams/core/symprocessor.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/ams/core/symprocessor.py b/ams/core/symprocessor.py index ba95b833..0e8ef9af 100644 --- a/ams/core/symprocessor.py +++ b/ams/core/symprocessor.py @@ -9,6 +9,7 @@ import sympy as sp +from andes.utils.misc import elapsed from ams.core.matprocessor import MatProcessor logger = logging.getLogger(__name__) @@ -107,9 +108,12 @@ def generate_symbols(self, force_generate=False): """ Generate symbols for all variables. """ - if not force_generate and self.parent._syms: + logger.debug(f'Entering symbol generation for {self.parent.class_name}') + + if (not force_generate) and self.parent._syms: + logger.debug(f' - Symbols already generated for {self.parent.class_name}') return True - logger.debug(f'- Generating symbols for {self.parent.class_name}') + t, _ = elapsed() # process tex_names defined in routines # ----------------------------------------------------------- @@ -186,8 +190,10 @@ def generate_symbols(self, force_generate=False): self.inputs_dict['sys_mva'] = sp.symbols('sys_mva') self.parent._syms = True + _, s = elapsed(t) - return True + logger.debug(f' - Generated in {s}') + return self.parent._syms def _check_expr_symbols(self, expr): """ From acdead157c719f13e59aa09abf6106c280db3e34 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 08:23:51 -0400 Subject: [PATCH 041/181] Improve logging --- ams/opt/omodel.py | 63 ++++++++++++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index a5e62131..55d390a2 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -135,8 +135,6 @@ def parse(self, no_code=True): # store the parsed expression str code self.code = code_expr code_expr = "self.optz = " + code_expr - msg = f"- Parse ExpressionCalc <{self.name}>: {self.e_str} " - logger.debug(msg) if not no_code: logger.info(f"<{self.name}> code: {code_expr}") # execute the expression @@ -538,9 +536,6 @@ def parse(self, no_code=True): # parse the constraint type code_constr = "self.optz=" + code_constr code_constr += " == 0" if self.is_eq else " <= 0" - msg = f"- Parse Constr <{self.name}>: {self.e_str} " - msg += "== 0" if self.is_eq else "<= 0" - logger.debug(msg) if not no_code: logger.info(f"<{self.name}> code: {code_constr}") # set the parsed constraint @@ -705,7 +700,6 @@ def parse(self, no_code=True): raise ValueError(f'Objective sense {self.sense} is not supported.') sense = 'cp.Minimize' if self.sense == 'min' else 'cp.Maximize' code_obj = f"self.optz={sense}({code_obj})" - logger.debug(f"Parse Objective <{self.name}>: {self.sense.upper()}. {self.e_str}") if not no_code: logger.info(f"Code: {code_obj}") # set the parsed objective function @@ -750,7 +744,7 @@ def __init__(self, routine): self.initialized = False self.parsed = False - def parse(self, no_code=True): + def parse(self, no_code=True, force_generate=False): """ Parse the optimization model from the symbolic description. @@ -759,15 +753,18 @@ def parse(self, no_code=True): no_code : bool, optional Flag indicating if the parsing code should be displayed, True by default. + force_generate : bool, optional + Flag indicating if the symbols should be generated, goes to `self.rtn.syms.generate_symbols()`. """ rtn = self.rtn - rtn.syms.generate_symbols(force_generate=False) + rtn.syms.generate_symbols(force_generate=force_generate) # --- add RParams and Services as parameters --- t0, _ = elapsed() logger.debug(f'Parsing OModel for {rtn.class_name}') for key, val in rtn.params.items(): if not val.no_parse: + logger.debug(f" - Parsing Param <{key}>") try: val.parse() except Exception as e: @@ -776,12 +773,13 @@ def parse(self, no_code=True): raise Exception(msg) setattr(self, key, val.optz) _, s = elapsed(t0) - logger.debug(f"Parse Params in {s}") + logger.debug(f" -> Parse Params in {s}") # --- add decision variables --- t0, _ = elapsed() for key, val in rtn.vars.items(): try: + logger.debug(f" - Parsing Var <{key}>") val.parse() except Exception as e: msg = f"Failed to parse Var <{key}>. " @@ -789,11 +787,12 @@ def parse(self, no_code=True): raise Exception(msg) setattr(self, key, val.optz) _, s = elapsed(t0) - logger.debug(f"Parse Vars in {s}") + logger.debug(f" -> Parse Vars in {s}") # --- add constraints --- t0, _ = elapsed() for key, val in rtn.constrs.items(): + logger.debug(f" - Parsing Constr <{key}>: {val.e_str}") try: val.parse(no_code=no_code) except Exception as e: @@ -802,11 +801,12 @@ def parse(self, no_code=True): raise Exception(msg) setattr(self, key, val.optz) _, s = elapsed(t0) - logger.debug(f"Parse Constrs in {s}") + logger.debug(f" -> Parse Constrs in {s}") # --- parse objective functions --- t0, _ = elapsed() if rtn.type != 'PF': + logger.debug(f" - Parsing Objective <{rtn.obj.name}>") if rtn.obj is not None: try: rtn.obj.parse(no_code=no_code) @@ -820,19 +820,26 @@ def parse(self, no_code=True): self.parsed = False return self.parsed _, s = elapsed(t0) - logger.debug(f"Parse Objective in {s}") + logger.debug(f" -> Parse Objective in {s}") # --- parse expressions --- t0, _ = elapsed() for key, val in self.rtn.exprs.items(): - val.parse(no_code=no_code) + msg = f" - Parsing ExpressionCalc <{key}>: {val.e_str} " + logger.debug(msg) + try: + val.parse(no_code=no_code) + except Exception as e: + msg = f"Failed to parse ExpressionCalc <{key}>. " + msg += f"Original error: {e}" + raise Exception(msg) _, s = elapsed(t0) - logger.debug(f"Parse Expressions in {s}") + logger.debug(f" -> Parse Expressions in {s}") self.parsed = True return self.parsed - def init(self, no_code=True): + def init(self, no_code=True, force_parse=False, force_generate=False): """ Set up the optimization model from the symbolic description. @@ -844,24 +851,29 @@ def init(self, no_code=True): no_code : bool, optional Flag indicating if the parsing code should be displayed, True by default. + force_parse : bool, optional + Flag indicating if the parsing should be forced, goes to `self.parse()`. + force_generate : bool, optional + Flag indicating if the symbols should be generated, goes to `self.parse()`. Returns ------- bool Returns True if the setup is successful, False otherwise. """ - t_setup, _ = elapsed() + t_init, _ = elapsed() - if not self.parsed: - self.parse(no_code=no_code) + if force_parse or not self.parsed: + self.parse(no_code=no_code, force_generate=force_generate) if self.rtn.type == 'PF': - _, s_setup = elapsed(t_setup) + _, s_init = elapsed(t_init) self.initialized = True - logger.debug(f"OModel for <{self.rtn.class_name}> initialized in {s_setup}.") + logger.debug(f"OModel for <{self.rtn.class_name}> initialized in {s_init}.") return self.initialized - # --- finalize the optimziation formulation --- + # --- evaluate the optimziation --- + t_eva, _ = elapsed() code_prob = "self.prob = problem(self.obj, " constrs_skip = [] constrs_add = [] @@ -874,14 +886,13 @@ def init(self, no_code=True): for pattern, replacement in self.rtn.syms.sub_map.items(): code_prob = re.sub(pattern, replacement, code_prob) - t_final, _ = elapsed() exec(code_prob, globals(), locals()) - _, s_final = elapsed(t_final) - logger.debug(f"Finalize in {s_final}") + _, s_eva = elapsed(t_eva) + logger.debug(f"OModel for <{self.rtn.class_name}> evaluated in {s_eva}") - _, s_setup = elapsed(t_setup) + _, s_init = elapsed(t_init) self.initialized = True - logger.debug(f"OModel for <{self.rtn.class_name}> initialized in {s_setup}.") + logger.debug(f"OModel for <{self.rtn.class_name}> initialized in {s_init}.") return self.initialized From 305baf2adbdf5d1f75d2c5a0a285fa9ea06131c5 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 08:24:12 -0400 Subject: [PATCH 042/181] Improve RoutineBase.init --- ams/routines/routine.py | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/ams/routines/routine.py b/ams/routines/routine.py index d09895ef..569d55fb 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -235,23 +235,27 @@ def _data_check(self, info=True): # TODO: add data validation for RParam, typical range, etc. return True - def init(self, force=False, no_code=True, **kwargs): + def init(self, no_code=True, + force_mats=False, force_constr=False, + force_parse=False, force_generate=False, + **kwargs): """ Initialize the routine. - Force initialization (`force=True`) will do the following: - - Rebuild the system matrices - - Enable all constraints - - Reinitialize the optimization model - Parameters ---------- - force: bool - Whether to force initialization. no_code: bool Whether to show generated code. + force_mats: bool + Whether to force build the system matrices, goes to `self.system.mats.build()`. + force_constr: bool + Whether to turn on all constraints. + force_parse: bool + Whether to force parse the optimization model, goes to `self.om.init()`. + force_generate: bool + Whether to force generate symbols, goes to `self.om.init()`. """ - skip_all = (not force) and self.initialized and self.om.initialized + skip_all = not (force_mats and force_parse and force_generate) and self.initialized if skip_all: logger.debug(f"{self.class_name} has already been initialized.") @@ -265,20 +269,19 @@ def init(self, force=False, no_code=True, **kwargs): msg = f"{self.class_name} data check failed, setup may run into error!" logger.warning(msg) - # --- force initialization --- - if force: - self.system.mats.build() + # --- turn on all constrs --- + if force_constr: for constr in self.constrs.values(): constr.is_disabled = False # --- matrix build --- - if not self.system.mats.initialized: - self.system.mats.build() + self.system.mats.build(force_mats=force_mats) # --- constraint check --- _ = self._get_off_constrs() - om_init = self.om.init(no_code=no_code) + if not self.om.initialized: + om_init = self.om.init(no_code=no_code, force_parse=force_parse, force_generate=force_generate) _, s_init = elapsed(t0) msg = f"<{self.class_name}> " From 67cf5b19f6990ba20f4e82a38fe50289fafa0c6e Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 08:29:26 -0400 Subject: [PATCH 043/181] Improve logging --- ams/core/symprocessor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ams/core/symprocessor.py b/ams/core/symprocessor.py index 0e8ef9af..dda7205e 100644 --- a/ams/core/symprocessor.py +++ b/ams/core/symprocessor.py @@ -108,10 +108,10 @@ def generate_symbols(self, force_generate=False): """ Generate symbols for all variables. """ - logger.debug(f'Entering symbol generation for {self.parent.class_name}') + logger.debug(f'Entering symbol generation for <{self.parent.class_name}>') if (not force_generate) and self.parent._syms: - logger.debug(f' - Symbols already generated for {self.parent.class_name}') + logger.debug(' - Symbols already generated') return True t, _ = elapsed() @@ -192,7 +192,7 @@ def generate_symbols(self, force_generate=False): self.parent._syms = True _, s = elapsed(t) - logger.debug(f' - Generated in {s}') + logger.debug(f' - Symbols generated in {s}') return self.parent._syms def _check_expr_symbols(self, expr): From 9ef53b711b0b1dc4ab1c2deff5baf72312ede7f3 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 08:29:40 -0400 Subject: [PATCH 044/181] Improve RoutinBase._data_check --- ams/routines/routine.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/ams/routines/routine.py b/ams/routines/routine.py index 569d55fb..8d194443 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -213,6 +213,7 @@ def _data_check(self, info=True): info: bool Whether to print warning messages. """ + logger.debug(f"Entering data check for <{self.class_name}>.") no_input = [] owner_list = [] for rname, rparam in self.rparams.items(): @@ -230,9 +231,10 @@ def _data_check(self, info=True): if len(no_input) > 0: if info: msg = f"Following models are missing in input: {set(owner_list)}" - logger.warning(msg) + logger.error(msg) return False # TODO: add data validation for RParam, typical range, etc. + logger.debug(" - Data check passed") return True def init(self, no_code=True, @@ -263,11 +265,7 @@ def init(self, no_code=True, t0, _ = elapsed() # --- data check --- - if self._data_check(): - logger.debug(f"{self.class_name} data check passed.") - else: - msg = f"{self.class_name} data check failed, setup may run into error!" - logger.warning(msg) + self._data_check() # --- turn on all constrs --- if force_constr: From c0be2e7207a73239814915a041541649221a2c99 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 08:49:35 -0400 Subject: [PATCH 045/181] Consider load and line connection status in DCOPF --- ams/routines/dcopf.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/ams/routines/dcopf.py b/ams/routines/dcopf.py index 844e423e..b8422f39 100644 --- a/ams/routines/dcopf.py +++ b/ams/routines/dcopf.py @@ -94,11 +94,19 @@ def __init__(self, system, config): model='Slack', src='bus', no_parse=True,) # --- load --- + self.upq = RParam(info='Load connection status', + name='upq', tex_name=r'u_{PQ}', + model='StaticLoad', src='u', + no_parse=True,) self.pd = RParam(info='active demand', name='pd', tex_name=r'p_{d}', model='StaticLoad', src='p0', unit='p.u.',) # --- line --- + self.ul = RParam(info='Line connection status', + name='ul', tex_name=r'u_{l}', + model='Line', src='u', + no_parse=True,) self.rate_a = RParam(info='long-term flow limit', name='rate_a', tex_name=r'R_{ATEA}', unit='p.u.', model='Line',) @@ -181,7 +189,7 @@ def __init__(self, system, config): unit='$/p.u.', model='Bus',) # --- power balance --- - pb = 'Bbus@aBus + Pbusinj + Cl@pd + Csh@gsh - Cg@pg' + pb = 'Bbus@aBus + Pbusinj + Cl@(mul(upq, pd)) + Csh@gsh - Cg@pg' self.pb = Constraint(name='pb', info='power balance', e_str=pb, is_eq=True,) # --- line flow --- @@ -191,10 +199,10 @@ def __init__(self, system, config): model='Line',) self.plflb = Constraint(info='line flow lower bound', name='plflb', is_eq=False, - e_str='-Bf@aBus - Pfinj - rate_a',) + e_str='-Bf@aBus - Pfinj - mul(ul, rate_a)',) self.plfub = Constraint(info='line flow upper bound', name='plfub', is_eq=False, - e_str='Bf@aBus + Pfinj - rate_a',) + e_str='Bf@aBus + Pfinj - mul(ul, rate_a)',) self.alflb = Constraint(info='line angle difference lower bound', name='alflb', is_eq=False, e_str='-CftT@aBus + amin',) From 2e5d60957e566834a59f000bd1b5c7ddcfe46207 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 08:49:52 -0400 Subject: [PATCH 046/181] Fix test_trip --- tests/test_routine.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_routine.py b/tests/test_routine.py index 2297d352..50a0dc5a 100644 --- a/tests/test_routine.py +++ b/tests/test_routine.py @@ -131,8 +131,7 @@ def test_trip(self): np.testing.assert_almost_equal(pg_trip, 0, decimal=6) # --- trip line --- - self.ss.Line.set(src='u', attr='v', idx='Line_4', value=0) - + self.ss.Line.set(src='u', attr='v', idx='Line_3', value=0) self.ss.DCOPF.update() self.ss.DCOPF.run(solver='CLARABEL') @@ -140,7 +139,7 @@ def test_trip(self): obj_lt = self.ss.DCOPF.obj.v self.assertGreater(obj_lt, obj_gt) - plf_trip = self.ss.DCOPF.get(src='plf', attr='v', idx='Line_4') + plf_trip = self.ss.DCOPF.get(src='plf', attr='v', idx='Line_3') np.testing.assert_almost_equal(plf_trip, 0, decimal=6) From 604ec54b772877a9094e2193c4045746de350069 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 09:01:24 -0400 Subject: [PATCH 047/181] Rename test_OModel to test_DCOPF --- tests/test_routine.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/test_routine.py b/tests/test_routine.py index 50a0dc5a..faf371ce 100644 --- a/tests/test_routine.py +++ b/tests/test_routine.py @@ -98,9 +98,9 @@ def test_value_method(self): self.assertAlmostEqual(self.ss.DCOPF.obj.v, self.ss.DCOPF.obj.v2, places=6) -class TestOModel(unittest.TestCase): +class TestDCOPF(unittest.TestCase): """ - Test methods of `RTED`. + Test routine `DCOPF`. """ def setUp(self) -> None: @@ -117,14 +117,24 @@ def test_trip(self): self.ss.DCOPF.run(solver='CLARABEL') obj = self.ss.DCOPF.obj.v - # --- generator trip --- - self.ss.StaticGen.set(src='u', attr='v', idx='PV_1', value=0) + # --- trip load --- + self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0) + self.ss.DCOPF.update() + self.ss.DCOPF.run(solver='CLARABEL') + obj_pqt = self.ss.DCOPF.obj.v + + # NOTE: ensure load is successfully tripped + self.assertLess(obj_pqt, obj) + + # --- trip generator --- + self.ss.StaticGen.set(src='u', attr='v', idx='PV_1', value=0) self.ss.DCOPF.update() self.ss.DCOPF.run(solver='CLARABEL') self.assertTrue(self.ss.DCOPF.converged, "DCOPF did not converge under generator trip!") obj_gt = self.ss.DCOPF.obj.v + # NOTE: ensure generator is successfully tripped self.assertGreater(obj_gt, obj) pg_trip = self.ss.DCOPF.get(src='pg', attr='v', idx='PV_1') @@ -137,6 +147,7 @@ def test_trip(self): self.ss.DCOPF.run(solver='CLARABEL') self.assertTrue(self.ss.DCOPF.converged, "DCOPF did not converge under line trip!") obj_lt = self.ss.DCOPF.obj.v + # NOTE: ensure line is successfully tripped self.assertGreater(obj_lt, obj_gt) plf_trip = self.ss.DCOPF.get(src='plf', attr='v', idx='Line_3') From c896829c951e050a9b05ca3507ad9d21486dca7e Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 09:11:33 -0400 Subject: [PATCH 048/181] [WIP] Add TestED in test_routine --- tests/test_routine.py | 61 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/tests/test_routine.py b/tests/test_routine.py index faf371ce..e3710d7f 100644 --- a/tests/test_routine.py +++ b/tests/test_routine.py @@ -124,8 +124,7 @@ def test_trip(self): self.ss.DCOPF.run(solver='CLARABEL') obj_pqt = self.ss.DCOPF.obj.v - # NOTE: ensure load is successfully tripped - self.assertLess(obj_pqt, obj) + self.assertLess(obj_pqt, obj, "Load trip does not take effect in DCOPF!") # --- trip generator --- self.ss.StaticGen.set(src='u', attr='v', idx='PV_1', value=0) @@ -134,8 +133,7 @@ def test_trip(self): self.ss.DCOPF.run(solver='CLARABEL') self.assertTrue(self.ss.DCOPF.converged, "DCOPF did not converge under generator trip!") obj_gt = self.ss.DCOPF.obj.v - # NOTE: ensure generator is successfully tripped - self.assertGreater(obj_gt, obj) + self.assertGreater(obj_gt, obj, "Generator trip does not take effect in DCOPF!") pg_trip = self.ss.DCOPF.get(src='pg', attr='v', idx='PV_1') np.testing.assert_almost_equal(pg_trip, 0, decimal=6) @@ -147,13 +145,64 @@ def test_trip(self): self.ss.DCOPF.run(solver='CLARABEL') self.assertTrue(self.ss.DCOPF.converged, "DCOPF did not converge under line trip!") obj_lt = self.ss.DCOPF.obj.v - # NOTE: ensure line is successfully tripped - self.assertGreater(obj_lt, obj_gt) + self.assertGreater(obj_lt, obj_gt, "Line trip does not take effect in DCOPF!") plf_trip = self.ss.DCOPF.get(src='plf', attr='v', idx='Line_3') np.testing.assert_almost_equal(plf_trip, 0, decimal=6) +class TestED(unittest.TestCase): + """ + Test routine `ED`. + """ + + def setUp(self) -> None: + self.ss = ams.load(ams.get_case("5bus/pjm5bus_demo.xlsx"), + setup=True, default_config=True, no_output=True) + # decrease load first + self.ss.PQ.set(src='p0', attr='v', idx=['PQ_1', 'PQ_2'], value=[0.3, 0.3]) + + def test_trip(self): + """ + Test generator trip. + """ + self.ss.ED.run(solver='CLARABEL') + obj = self.ss.ED.obj.v + + # --- trip load --- + self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0) + self.ss.ED.update() + + self.ss.ED.run(solver='CLARABEL') + obj_pqt = self.ss.ED.obj.v + + self.assertLess(obj_pqt, obj, "Load trip does not take effect in ED!") + + # --- trip generator --- + self.ss.StaticGen.set(src='u', attr='v', idx='PV_1', value=0) + self.ss.ED.update() + + self.ss.ED.run(solver='CLARABEL') + self.assertTrue(self.ss.ED.converged, "ED did not converge under generator trip!") + obj_gt = self.ss.ED.obj.v + self.assertGreater(obj_gt, obj, "Generator trip does not take effect in ED!") + + pg_trip = self.ss.ED.get(src='pg', attr='v', idx='PV_1') + np.testing.assert_almost_equal(pg_trip, 0, decimal=6) + + # --- trip line --- + self.ss.Line.set(src='u', attr='v', idx='Line_3', value=0) + self.ss.ED.update() + + self.ss.ED.run(solver='CLARABEL') + self.assertTrue(self.ss.Ed.converged, "ED did not converge under line trip!") + obj_lt = self.ss.ED.obj.v + self.assertGreater(obj_lt, obj_gt, "Line trip does not take effect in ED!") + + plf_trip = self.ss.ED.get(src='plf', attr='v', idx='Line_3') + np.testing.assert_almost_equal(plf_trip, 0, decimal=6) + + class TestSetOptzValueACOPF(unittest.TestCase): """ Test value settings of `OptzBase` series in `ACOPF`. From aafcacb53c5d6f1cac938774f9ddda4d61fcede5 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 09:20:22 -0400 Subject: [PATCH 049/181] [WIP] Add test_trip to test_routine.TestRTED --- tests/test_routine.py | 112 ++++++++++++++++++++++++++++-------------- 1 file changed, 76 insertions(+), 36 deletions(-) diff --git a/tests/test_routine.py b/tests/test_routine.py index e3710d7f..c2dd33a6 100644 --- a/tests/test_routine.py +++ b/tests/test_routine.py @@ -151,6 +151,82 @@ def test_trip(self): np.testing.assert_almost_equal(plf_trip, 0, decimal=6) +class TestRTED(unittest.TestCase): + """ + Test methods of `RTED`. + """ + + def setUp(self) -> None: + self.ss = ams.load(ams.get_case("5bus/pjm5bus_demo.xlsx"), + setup=True, + default_config=True, + no_output=True, + ) + self.ss.RTED.run(solver='CLARABEL') + + def test_dc2ac(self): + """ + Test `RTED.init()` method. + """ + self.ss.RTED.dc2ac() + self.assertTrue(self.ss.RTED.converted, "AC conversion failed!") + self.assertTrue(self.ss.RTED.exec_time > 0, "Execution time is not greater than 0.") + + stg_idx = self.ss.StaticGen.get_idx() + pg_rted = self.ss.RTED.get(src='pg', attr='v', idx=stg_idx) + pg_acopf = self.ss.ACOPF.get(src='pg', attr='v', idx=stg_idx) + np.testing.assert_almost_equal(pg_rted, pg_acopf, decimal=3) + + bus_idx = self.ss.Bus.get_idx() + v_rted = self.ss.RTED.get(src='vBus', attr='v', idx=bus_idx) + v_acopf = self.ss.ACOPF.get(src='vBus', attr='v', idx=bus_idx) + np.testing.assert_almost_equal(v_rted, v_acopf, decimal=3) + + a_rted = self.ss.RTED.get(src='aBus', attr='v', idx=bus_idx) + a_acopf = self.ss.ACOPF.get(src='aBus', attr='v', idx=bus_idx) + np.testing.assert_almost_equal(a_rted, a_acopf, decimal=3) + + def test_trip(self): + """ + Test generator trip. + """ + self.ss.RTED.run(solver='CLARABEL') + obj = self.ss.RTED.obj.v + + # --- trip load --- + self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0) + self.ss.RTED.update() + + self.ss.RTED.run(solver='CLARABEL') + obj_pqt = self.ss.RTED.obj.v + + self.assertLess(obj_pqt, obj, "Load trip does not take effect in RTED!") + + # --- trip generator --- + self.ss.StaticGen.set(src='u', attr='v', idx='PV_1', value=0) + self.ss.RTED.update() + + self.ss.RTED.run(solver='CLARABEL') + self.assertTrue(self.ss.RTED.converged, "RTED did not converge under generator trip!") + obj_gt = self.ss.RTED.obj.v + self.assertGreater(obj_gt, obj, "Generator trip does not take effect in RTED!") + + pg_trip = self.ss.RTED.get(src='pg', attr='v', idx='PV_1') + np.testing.assert_almost_equal(pg_trip, 0, decimal=6) + + # --- trip line --- + self.ss.Line.set(src='u', attr='v', idx='Line_3', value=0) + self.ss.RTED.update() + + self.ss.RTED.run(solver='CLARABEL') + self.assertTrue(self.ss.RTED.converged, "RTED did not converge under line trip!") + obj_lt = self.ss.RTED.obj.v + self.assertGreater(obj_lt, obj_gt, "Line trip does not take effect in RTED!") + + plf_trip = self.ss.RTED.get(src='plf', attr='v', idx='Line_3') + np.testing.assert_almost_equal(plf_trip, 0, decimal=6) + + class TestED(unittest.TestCase): """ Test routine `ED`. @@ -281,39 +357,3 @@ def test_vset_after_init(self): self.assertRaises(AttributeError, lambda: setattr(self.ss.DCOPF.plfub, 'v', 1)) # set values to `Objective` is not allowed self.assertRaises(AttributeError, lambda: setattr(self.ss.DCOPF.obj, 'v', 1)) - - -class TestRTED(unittest.TestCase): - """ - Test methods of `RTED`. - """ - - def setUp(self) -> None: - self.ss = ams.load(ams.get_case("5bus/pjm5bus_demo.xlsx"), - setup=True, - default_config=True, - no_output=True, - ) - self.ss.RTED.run(solver='CLARABEL') - - def test_dc2ac(self): - """ - Test `RTED.init()` method. - """ - self.ss.RTED.dc2ac() - self.assertTrue(self.ss.RTED.converted, "AC conversion failed!") - self.assertTrue(self.ss.RTED.exec_time > 0, "Execution time is not greater than 0.") - - stg_idx = self.ss.StaticGen.get_idx() - pg_rted = self.ss.RTED.get(src='pg', attr='v', idx=stg_idx) - pg_acopf = self.ss.ACOPF.get(src='pg', attr='v', idx=stg_idx) - np.testing.assert_almost_equal(pg_rted, pg_acopf, decimal=3) - - bus_idx = self.ss.Bus.get_idx() - v_rted = self.ss.RTED.get(src='vBus', attr='v', idx=bus_idx) - v_acopf = self.ss.ACOPF.get(src='vBus', attr='v', idx=bus_idx) - np.testing.assert_almost_equal(v_rted, v_acopf, decimal=3) - - a_rted = self.ss.RTED.get(src='aBus', attr='v', idx=bus_idx) - a_acopf = self.ss.ACOPF.get(src='aBus', attr='v', idx=bus_idx) - np.testing.assert_almost_equal(a_rted, a_acopf, decimal=3) From 22f01b5d34e3f3c675cc16417e10bdcc08250e1c Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 09:28:50 -0400 Subject: [PATCH 050/181] Fix test_trip in TestDCOPF and TestRTED --- tests/test_routine.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/tests/test_routine.py b/tests/test_routine.py index c2dd33a6..9ba39c4e 100644 --- a/tests/test_routine.py +++ b/tests/test_routine.py @@ -123,7 +123,6 @@ def test_trip(self): self.ss.DCOPF.run(solver='CLARABEL') obj_pqt = self.ss.DCOPF.obj.v - self.assertLess(obj_pqt, obj, "Load trip does not take effect in DCOPF!") # --- trip generator --- @@ -132,8 +131,9 @@ def test_trip(self): self.ss.DCOPF.run(solver='CLARABEL') self.assertTrue(self.ss.DCOPF.converged, "DCOPF did not converge under generator trip!") - obj_gt = self.ss.DCOPF.obj.v - self.assertGreater(obj_gt, obj, "Generator trip does not take effect in DCOPF!") + self.assertAlmostEqual(self.ss.DCOPF.get(src='pg', attr='v', idx='PV_1'), + 0, decimal=6, + msg="Generator trip does not take effect in DCOPF!") pg_trip = self.ss.DCOPF.get(src='pg', attr='v', idx='PV_1') np.testing.assert_almost_equal(pg_trip, 0, decimal=6) @@ -144,11 +144,10 @@ def test_trip(self): self.ss.DCOPF.run(solver='CLARABEL') self.assertTrue(self.ss.DCOPF.converged, "DCOPF did not converge under line trip!") - obj_lt = self.ss.DCOPF.obj.v - self.assertGreater(obj_lt, obj_gt, "Line trip does not take effect in DCOPF!") - plf_trip = self.ss.DCOPF.get(src='plf', attr='v', idx='Line_3') - np.testing.assert_almost_equal(plf_trip, 0, decimal=6) + np.testing.assert_almost_equal(self.ss.DCOPF.get(src='plf', attr='v', idx='Line_3'), + 0, decimal=6, + msg="Line trip does not take effect in DCOPF!") class TestRTED(unittest.TestCase): @@ -208,8 +207,9 @@ def test_trip(self): self.ss.RTED.run(solver='CLARABEL') self.assertTrue(self.ss.RTED.converged, "RTED did not converge under generator trip!") - obj_gt = self.ss.RTED.obj.v - self.assertGreater(obj_gt, obj, "Generator trip does not take effect in RTED!") + self.assertAlmostEqual(self.ss.RTED.get(src='pg', attr='v', idx='PV_1'), + 0, decimal=6, + msg="Generator trip does not take effect in RTED!") pg_trip = self.ss.RTED.get(src='pg', attr='v', idx='PV_1') np.testing.assert_almost_equal(pg_trip, 0, decimal=6) @@ -220,11 +220,9 @@ def test_trip(self): self.ss.RTED.run(solver='CLARABEL') self.assertTrue(self.ss.RTED.converged, "RTED did not converge under line trip!") - obj_lt = self.ss.RTED.obj.v - self.assertGreater(obj_lt, obj_gt, "Line trip does not take effect in RTED!") - - plf_trip = self.ss.RTED.get(src='plf', attr='v', idx='Line_3') - np.testing.assert_almost_equal(plf_trip, 0, decimal=6) + self.assertAlmostEqual(self.ss.RTED.get(src='plf', attr='v', idx='Line_3'), + 0, decimal=6, + msg="Line trip does not take effect in RTED!") class TestED(unittest.TestCase): From 2f21d216e7cc0885992e409e51574be658ada1f0 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 09:33:50 -0400 Subject: [PATCH 051/181] Fix test_trip in TestDCOPF and TestRTED --- tests/test_routine.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test_routine.py b/tests/test_routine.py index 9ba39c4e..56968dbb 100644 --- a/tests/test_routine.py +++ b/tests/test_routine.py @@ -132,11 +132,11 @@ def test_trip(self): self.ss.DCOPF.run(solver='CLARABEL') self.assertTrue(self.ss.DCOPF.converged, "DCOPF did not converge under generator trip!") self.assertAlmostEqual(self.ss.DCOPF.get(src='pg', attr='v', idx='PV_1'), - 0, decimal=6, + 0, places=6, + msg="Generator trip does not take effect in DCOPF!") + self.assertAlmostEqual(self.ss.DCOPF.get(src='pg', attr='v', idx='PV_1'), + 0, places=6, msg="Generator trip does not take effect in DCOPF!") - - pg_trip = self.ss.DCOPF.get(src='pg', attr='v', idx='PV_1') - np.testing.assert_almost_equal(pg_trip, 0, decimal=6) # --- trip line --- self.ss.Line.set(src='u', attr='v', idx='Line_3', value=0) @@ -145,9 +145,9 @@ def test_trip(self): self.ss.DCOPF.run(solver='CLARABEL') self.assertTrue(self.ss.DCOPF.converged, "DCOPF did not converge under line trip!") - np.testing.assert_almost_equal(self.ss.DCOPF.get(src='plf', attr='v', idx='Line_3'), - 0, decimal=6, - msg="Line trip does not take effect in DCOPF!") + self.assertAlmostEqual(self.ss.DCOPF.get(src='plf', attr='v', idx='Line_3'), + 0, places=6, + msg="Line trip does not take effect in DCOPF!") class TestRTED(unittest.TestCase): @@ -208,7 +208,7 @@ def test_trip(self): self.ss.RTED.run(solver='CLARABEL') self.assertTrue(self.ss.RTED.converged, "RTED did not converge under generator trip!") self.assertAlmostEqual(self.ss.RTED.get(src='pg', attr='v', idx='PV_1'), - 0, decimal=6, + 0, places=6, msg="Generator trip does not take effect in RTED!") pg_trip = self.ss.RTED.get(src='pg', attr='v', idx='PV_1') @@ -221,7 +221,7 @@ def test_trip(self): self.ss.RTED.run(solver='CLARABEL') self.assertTrue(self.ss.RTED.converged, "RTED did not converge under line trip!") self.assertAlmostEqual(self.ss.RTED.get(src='plf', attr='v', idx='Line_3'), - 0, decimal=6, + 0, places=6, msg="Line trip does not take effect in RTED!") From 018e9fe048d304f3abbc4eccb030bfc452424e7c Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 09:40:35 -0400 Subject: [PATCH 052/181] Fix test_trip in TestDCOPF and TestRTED --- tests/test_routine.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/tests/test_routine.py b/tests/test_routine.py index 56968dbb..c0873ed8 100644 --- a/tests/test_routine.py +++ b/tests/test_routine.py @@ -117,13 +117,21 @@ def test_trip(self): self.ss.DCOPF.run(solver='CLARABEL') obj = self.ss.DCOPF.obj.v - # --- trip load --- + # --- set load --- self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0) self.ss.DCOPF.update() self.ss.DCOPF.run(solver='CLARABEL') obj_pqt = self.ss.DCOPF.obj.v - self.assertLess(obj_pqt, obj, "Load trip does not take effect in DCOPF!") + self.assertLess(obj_pqt, obj, "Load set does not take effect in DCOPF!") + + # --- trip load --- + self.ss.PQ.set(src='u', attr='v', idx='PQ_1', value=0) + self.ss.DCOPF.update() + + self.ss.DCOPF.run(solver='CLARABEL') + obj_pqt2 = self.ss.DCOPF.obj.v + self.assertLess(obj_pqt2, obj_pqt, "Load trip does not take effect in DCOPF!") # --- trip generator --- self.ss.StaticGen.set(src='u', attr='v', idx='PV_1', value=0) @@ -192,14 +200,21 @@ def test_trip(self): self.ss.RTED.run(solver='CLARABEL') obj = self.ss.RTED.obj.v - # --- trip load --- + # --- set load --- self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0) - self.ss.RTED.update() + self.ss.DCOPF.update() self.ss.RTED.run(solver='CLARABEL') obj_pqt = self.ss.RTED.obj.v + self.assertLess(obj_pqt, obj, "Load set does not take effect in RTED!") + + # --- trip load --- + self.ss.PQ.set(src='u', attr='v', idx='PQ_1', value=0) + self.ss.RTED.update() - self.assertLess(obj_pqt, obj, "Load trip does not take effect in RTED!") + self.ss.RTED.run(solver='CLARABEL') + obj_pqt2 = self.ss.RTED.obj.v + self.assertLess(obj_pqt2, obj_pqt, "Load trip does not take effect in RTED!") # --- trip generator --- self.ss.StaticGen.set(src='u', attr='v', idx='PV_1', value=0) From 9678c852cbf6da01e13c3043e9ad7b4cd9109e0f Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 09:44:07 -0400 Subject: [PATCH 053/181] Refactor test_routine --- tests/test_routine.py | 194 ---------------------------------------- tests/test_rtn_dcopf.py | 63 +++++++++++++ tests/test_rtn_ed.py | 56 ++++++++++++ tests/test_rtn_rted.py | 88 ++++++++++++++++++ 4 files changed, 207 insertions(+), 194 deletions(-) create mode 100644 tests/test_rtn_dcopf.py create mode 100644 tests/test_rtn_ed.py create mode 100644 tests/test_rtn_rted.py diff --git a/tests/test_routine.py b/tests/test_routine.py index c0873ed8..f9ddf843 100644 --- a/tests/test_routine.py +++ b/tests/test_routine.py @@ -98,200 +98,6 @@ def test_value_method(self): self.assertAlmostEqual(self.ss.DCOPF.obj.v, self.ss.DCOPF.obj.v2, places=6) -class TestDCOPF(unittest.TestCase): - """ - Test routine `DCOPF`. - """ - - def setUp(self) -> None: - self.ss = ams.load(ams.get_case("5bus/pjm5bus_demo.xlsx"), - setup=True, default_config=True, no_output=True) - # decrease load first - self.ss.PQ.set(src='p0', attr='v', idx=['PQ_1', 'PQ_2'], value=[0.3, 0.3]) - - def test_trip(self): - """ - Test generator trip. - """ - # --- run DCOPF --- - self.ss.DCOPF.run(solver='CLARABEL') - obj = self.ss.DCOPF.obj.v - - # --- set load --- - self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0) - self.ss.DCOPF.update() - - self.ss.DCOPF.run(solver='CLARABEL') - obj_pqt = self.ss.DCOPF.obj.v - self.assertLess(obj_pqt, obj, "Load set does not take effect in DCOPF!") - - # --- trip load --- - self.ss.PQ.set(src='u', attr='v', idx='PQ_1', value=0) - self.ss.DCOPF.update() - - self.ss.DCOPF.run(solver='CLARABEL') - obj_pqt2 = self.ss.DCOPF.obj.v - self.assertLess(obj_pqt2, obj_pqt, "Load trip does not take effect in DCOPF!") - - # --- trip generator --- - self.ss.StaticGen.set(src='u', attr='v', idx='PV_1', value=0) - self.ss.DCOPF.update() - - self.ss.DCOPF.run(solver='CLARABEL') - self.assertTrue(self.ss.DCOPF.converged, "DCOPF did not converge under generator trip!") - self.assertAlmostEqual(self.ss.DCOPF.get(src='pg', attr='v', idx='PV_1'), - 0, places=6, - msg="Generator trip does not take effect in DCOPF!") - self.assertAlmostEqual(self.ss.DCOPF.get(src='pg', attr='v', idx='PV_1'), - 0, places=6, - msg="Generator trip does not take effect in DCOPF!") - - # --- trip line --- - self.ss.Line.set(src='u', attr='v', idx='Line_3', value=0) - self.ss.DCOPF.update() - - self.ss.DCOPF.run(solver='CLARABEL') - self.assertTrue(self.ss.DCOPF.converged, "DCOPF did not converge under line trip!") - - self.assertAlmostEqual(self.ss.DCOPF.get(src='plf', attr='v', idx='Line_3'), - 0, places=6, - msg="Line trip does not take effect in DCOPF!") - - -class TestRTED(unittest.TestCase): - """ - Test methods of `RTED`. - """ - - def setUp(self) -> None: - self.ss = ams.load(ams.get_case("5bus/pjm5bus_demo.xlsx"), - setup=True, - default_config=True, - no_output=True, - ) - self.ss.RTED.run(solver='CLARABEL') - - def test_dc2ac(self): - """ - Test `RTED.init()` method. - """ - self.ss.RTED.dc2ac() - self.assertTrue(self.ss.RTED.converted, "AC conversion failed!") - self.assertTrue(self.ss.RTED.exec_time > 0, "Execution time is not greater than 0.") - - stg_idx = self.ss.StaticGen.get_idx() - pg_rted = self.ss.RTED.get(src='pg', attr='v', idx=stg_idx) - pg_acopf = self.ss.ACOPF.get(src='pg', attr='v', idx=stg_idx) - np.testing.assert_almost_equal(pg_rted, pg_acopf, decimal=3) - - bus_idx = self.ss.Bus.get_idx() - v_rted = self.ss.RTED.get(src='vBus', attr='v', idx=bus_idx) - v_acopf = self.ss.ACOPF.get(src='vBus', attr='v', idx=bus_idx) - np.testing.assert_almost_equal(v_rted, v_acopf, decimal=3) - - a_rted = self.ss.RTED.get(src='aBus', attr='v', idx=bus_idx) - a_acopf = self.ss.ACOPF.get(src='aBus', attr='v', idx=bus_idx) - np.testing.assert_almost_equal(a_rted, a_acopf, decimal=3) - - def test_trip(self): - """ - Test generator trip. - """ - self.ss.RTED.run(solver='CLARABEL') - obj = self.ss.RTED.obj.v - - # --- set load --- - self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0) - self.ss.DCOPF.update() - - self.ss.RTED.run(solver='CLARABEL') - obj_pqt = self.ss.RTED.obj.v - self.assertLess(obj_pqt, obj, "Load set does not take effect in RTED!") - - # --- trip load --- - self.ss.PQ.set(src='u', attr='v', idx='PQ_1', value=0) - self.ss.RTED.update() - - self.ss.RTED.run(solver='CLARABEL') - obj_pqt2 = self.ss.RTED.obj.v - self.assertLess(obj_pqt2, obj_pqt, "Load trip does not take effect in RTED!") - - # --- trip generator --- - self.ss.StaticGen.set(src='u', attr='v', idx='PV_1', value=0) - self.ss.RTED.update() - - self.ss.RTED.run(solver='CLARABEL') - self.assertTrue(self.ss.RTED.converged, "RTED did not converge under generator trip!") - self.assertAlmostEqual(self.ss.RTED.get(src='pg', attr='v', idx='PV_1'), - 0, places=6, - msg="Generator trip does not take effect in RTED!") - - pg_trip = self.ss.RTED.get(src='pg', attr='v', idx='PV_1') - np.testing.assert_almost_equal(pg_trip, 0, decimal=6) - - # --- trip line --- - self.ss.Line.set(src='u', attr='v', idx='Line_3', value=0) - self.ss.RTED.update() - - self.ss.RTED.run(solver='CLARABEL') - self.assertTrue(self.ss.RTED.converged, "RTED did not converge under line trip!") - self.assertAlmostEqual(self.ss.RTED.get(src='plf', attr='v', idx='Line_3'), - 0, places=6, - msg="Line trip does not take effect in RTED!") - - -class TestED(unittest.TestCase): - """ - Test routine `ED`. - """ - - def setUp(self) -> None: - self.ss = ams.load(ams.get_case("5bus/pjm5bus_demo.xlsx"), - setup=True, default_config=True, no_output=True) - # decrease load first - self.ss.PQ.set(src='p0', attr='v', idx=['PQ_1', 'PQ_2'], value=[0.3, 0.3]) - - def test_trip(self): - """ - Test generator trip. - """ - self.ss.ED.run(solver='CLARABEL') - obj = self.ss.ED.obj.v - - # --- trip load --- - self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0) - self.ss.ED.update() - - self.ss.ED.run(solver='CLARABEL') - obj_pqt = self.ss.ED.obj.v - - self.assertLess(obj_pqt, obj, "Load trip does not take effect in ED!") - - # --- trip generator --- - self.ss.StaticGen.set(src='u', attr='v', idx='PV_1', value=0) - self.ss.ED.update() - - self.ss.ED.run(solver='CLARABEL') - self.assertTrue(self.ss.ED.converged, "ED did not converge under generator trip!") - obj_gt = self.ss.ED.obj.v - self.assertGreater(obj_gt, obj, "Generator trip does not take effect in ED!") - - pg_trip = self.ss.ED.get(src='pg', attr='v', idx='PV_1') - np.testing.assert_almost_equal(pg_trip, 0, decimal=6) - - # --- trip line --- - self.ss.Line.set(src='u', attr='v', idx='Line_3', value=0) - self.ss.ED.update() - - self.ss.ED.run(solver='CLARABEL') - self.assertTrue(self.ss.Ed.converged, "ED did not converge under line trip!") - obj_lt = self.ss.ED.obj.v - self.assertGreater(obj_lt, obj_gt, "Line trip does not take effect in ED!") - - plf_trip = self.ss.ED.get(src='plf', attr='v', idx='Line_3') - np.testing.assert_almost_equal(plf_trip, 0, decimal=6) - - class TestSetOptzValueACOPF(unittest.TestCase): """ Test value settings of `OptzBase` series in `ACOPF`. diff --git a/tests/test_rtn_dcopf.py b/tests/test_rtn_dcopf.py new file mode 100644 index 00000000..e4ba7d58 --- /dev/null +++ b/tests/test_rtn_dcopf.py @@ -0,0 +1,63 @@ +import unittest + +import ams + + +class TestDCOPF(unittest.TestCase): + """ + Test routine `DCOPF`. + """ + + def setUp(self) -> None: + self.ss = ams.load(ams.get_case("5bus/pjm5bus_demo.xlsx"), + setup=True, default_config=True, no_output=True) + # decrease load first + self.ss.PQ.set(src='p0', attr='v', idx=['PQ_1', 'PQ_2'], value=[0.3, 0.3]) + + def test_trip(self): + """ + Test generator trip. + """ + # --- run DCOPF --- + self.ss.DCOPF.run(solver='CLARABEL') + obj = self.ss.DCOPF.obj.v + + # --- set load --- + self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0) + self.ss.DCOPF.update() + + self.ss.DCOPF.run(solver='CLARABEL') + obj_pqt = self.ss.DCOPF.obj.v + self.assertLess(obj_pqt, obj, "Load set does not take effect in DCOPF!") + + # --- trip load --- + self.ss.PQ.set(src='u', attr='v', idx='PQ_1', value=0) + self.ss.DCOPF.update() + + self.ss.DCOPF.run(solver='CLARABEL') + obj_pqt2 = self.ss.DCOPF.obj.v + self.assertLess(obj_pqt2, obj_pqt, "Load trip does not take effect in DCOPF!") + + # --- trip generator --- + self.ss.StaticGen.set(src='u', attr='v', idx='PV_1', value=0) + self.ss.DCOPF.update() + + self.ss.DCOPF.run(solver='CLARABEL') + self.assertTrue(self.ss.DCOPF.converged, "DCOPF did not converge under generator trip!") + self.assertAlmostEqual(self.ss.DCOPF.get(src='pg', attr='v', idx='PV_1'), + 0, places=6, + msg="Generator trip does not take effect in DCOPF!") + self.assertAlmostEqual(self.ss.DCOPF.get(src='pg', attr='v', idx='PV_1'), + 0, places=6, + msg="Generator trip does not take effect in DCOPF!") + + # --- trip line --- + self.ss.Line.set(src='u', attr='v', idx='Line_3', value=0) + self.ss.DCOPF.update() + + self.ss.DCOPF.run(solver='CLARABEL') + self.assertTrue(self.ss.DCOPF.converged, "DCOPF did not converge under line trip!") + + self.assertAlmostEqual(self.ss.DCOPF.get(src='plf', attr='v', idx='Line_3'), + 0, places=6, + msg="Line trip does not take effect in DCOPF!") diff --git a/tests/test_rtn_ed.py b/tests/test_rtn_ed.py new file mode 100644 index 00000000..2eaccea3 --- /dev/null +++ b/tests/test_rtn_ed.py @@ -0,0 +1,56 @@ +import unittest +import numpy as np + +import ams + + +class TestED(unittest.TestCase): + """ + Test routine `ED`. + """ + + def setUp(self) -> None: + self.ss = ams.load(ams.get_case("5bus/pjm5bus_demo.xlsx"), + setup=True, default_config=True, no_output=True) + # decrease load first + self.ss.PQ.set(src='p0', attr='v', idx=['PQ_1', 'PQ_2'], value=[0.3, 0.3]) + + def test_trip(self): + """ + Test generator trip. + """ + self.ss.ED.run(solver='CLARABEL') + obj = self.ss.ED.obj.v + + # --- trip load --- + self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0) + self.ss.ED.update() + + self.ss.ED.run(solver='CLARABEL') + obj_pqt = self.ss.ED.obj.v + + self.assertLess(obj_pqt, obj, "Load trip does not take effect in ED!") + + # --- trip generator --- + self.ss.StaticGen.set(src='u', attr='v', idx='PV_1', value=0) + self.ss.ED.update() + + self.ss.ED.run(solver='CLARABEL') + self.assertTrue(self.ss.ED.converged, "ED did not converge under generator trip!") + obj_gt = self.ss.ED.obj.v + self.assertGreater(obj_gt, obj, "Generator trip does not take effect in ED!") + + pg_trip = self.ss.ED.get(src='pg', attr='v', idx='PV_1') + np.testing.assert_almost_equal(pg_trip, 0, decimal=6) + + # --- trip line --- + self.ss.Line.set(src='u', attr='v', idx='Line_3', value=0) + self.ss.ED.update() + + self.ss.ED.run(solver='CLARABEL') + self.assertTrue(self.ss.Ed.converged, "ED did not converge under line trip!") + obj_lt = self.ss.ED.obj.v + self.assertGreater(obj_lt, obj_gt, "Line trip does not take effect in ED!") + + plf_trip = self.ss.ED.get(src='plf', attr='v', idx='Line_3') + np.testing.assert_almost_equal(plf_trip, 0, decimal=6) diff --git a/tests/test_rtn_rted.py b/tests/test_rtn_rted.py new file mode 100644 index 00000000..d9523584 --- /dev/null +++ b/tests/test_rtn_rted.py @@ -0,0 +1,88 @@ +import unittest +import numpy as np + +from andes.shared import pd + +import ams + + +class TestRTED(unittest.TestCase): + """ + Test methods of `RTED`. + """ + + def setUp(self) -> None: + self.ss = ams.load(ams.get_case("5bus/pjm5bus_demo.xlsx"), + setup=True, + default_config=True, + no_output=True, + ) + self.ss.RTED.run(solver='CLARABEL') + + def test_dc2ac(self): + """ + Test `RTED.init()` method. + """ + self.ss.RTED.dc2ac() + self.assertTrue(self.ss.RTED.converted, "AC conversion failed!") + self.assertTrue(self.ss.RTED.exec_time > 0, "Execution time is not greater than 0.") + + stg_idx = self.ss.StaticGen.get_idx() + pg_rted = self.ss.RTED.get(src='pg', attr='v', idx=stg_idx) + pg_acopf = self.ss.ACOPF.get(src='pg', attr='v', idx=stg_idx) + np.testing.assert_almost_equal(pg_rted, pg_acopf, decimal=3) + + bus_idx = self.ss.Bus.get_idx() + v_rted = self.ss.RTED.get(src='vBus', attr='v', idx=bus_idx) + v_acopf = self.ss.ACOPF.get(src='vBus', attr='v', idx=bus_idx) + np.testing.assert_almost_equal(v_rted, v_acopf, decimal=3) + + a_rted = self.ss.RTED.get(src='aBus', attr='v', idx=bus_idx) + a_acopf = self.ss.ACOPF.get(src='aBus', attr='v', idx=bus_idx) + np.testing.assert_almost_equal(a_rted, a_acopf, decimal=3) + + def test_trip(self): + """ + Test generator trip. + """ + self.ss.RTED.run(solver='CLARABEL') + obj = self.ss.RTED.obj.v + + # --- set load --- + self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0) + self.ss.DCOPF.update() + + self.ss.RTED.run(solver='CLARABEL') + obj_pqt = self.ss.RTED.obj.v + self.assertLess(obj_pqt, obj, "Load set does not take effect in RTED!") + + # --- trip load --- + self.ss.PQ.set(src='u', attr='v', idx='PQ_1', value=0) + self.ss.RTED.update() + + self.ss.RTED.run(solver='CLARABEL') + obj_pqt2 = self.ss.RTED.obj.v + self.assertLess(obj_pqt2, obj_pqt, "Load trip does not take effect in RTED!") + + # --- trip generator --- + self.ss.StaticGen.set(src='u', attr='v', idx='PV_1', value=0) + self.ss.RTED.update() + + self.ss.RTED.run(solver='CLARABEL') + self.assertTrue(self.ss.RTED.converged, "RTED did not converge under generator trip!") + self.assertAlmostEqual(self.ss.RTED.get(src='pg', attr='v', idx='PV_1'), + 0, places=6, + msg="Generator trip does not take effect in RTED!") + + pg_trip = self.ss.RTED.get(src='pg', attr='v', idx='PV_1') + np.testing.assert_almost_equal(pg_trip, 0, decimal=6) + + # --- trip line --- + self.ss.Line.set(src='u', attr='v', idx='Line_3', value=0) + self.ss.RTED.update() + + self.ss.RTED.run(solver='CLARABEL') + self.assertTrue(self.ss.RTED.converged, "RTED did not converge under line trip!") + self.assertAlmostEqual(self.ss.RTED.get(src='plf', attr='v', idx='Line_3'), + 0, places=6, + msg="Line trip does not take effect in RTED!") From 11e2c3fb40c6f35f20e3d43f7cacc148aabaa5c7 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 09:46:23 -0400 Subject: [PATCH 054/181] Typo --- tests/test_rtn_rted.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_rtn_rted.py b/tests/test_rtn_rted.py index d9523584..e02a95f4 100644 --- a/tests/test_rtn_rted.py +++ b/tests/test_rtn_rted.py @@ -54,15 +54,15 @@ def test_trip(self): self.ss.RTED.run(solver='CLARABEL') obj_pqt = self.ss.RTED.obj.v - self.assertLess(obj_pqt, obj, "Load set does not take effect in RTED!") + self.assertLess(obj_pqt, obj, "Load set does not take effect!") # --- trip load --- - self.ss.PQ.set(src='u', attr='v', idx='PQ_1', value=0) + self.ss.PQ.set(src='u', attr='v', idx='PQ_2', value=0) self.ss.RTED.update() self.ss.RTED.run(solver='CLARABEL') obj_pqt2 = self.ss.RTED.obj.v - self.assertLess(obj_pqt2, obj_pqt, "Load trip does not take effect in RTED!") + self.assertLess(obj_pqt2, obj_pqt, "Load trip does not take effect!") # --- trip generator --- self.ss.StaticGen.set(src='u', attr='v', idx='PV_1', value=0) @@ -72,7 +72,7 @@ def test_trip(self): self.assertTrue(self.ss.RTED.converged, "RTED did not converge under generator trip!") self.assertAlmostEqual(self.ss.RTED.get(src='pg', attr='v', idx='PV_1'), 0, places=6, - msg="Generator trip does not take effect in RTED!") + msg="Generator trip does not take effect!") pg_trip = self.ss.RTED.get(src='pg', attr='v', idx='PV_1') np.testing.assert_almost_equal(pg_trip, 0, decimal=6) @@ -85,4 +85,4 @@ def test_trip(self): self.assertTrue(self.ss.RTED.converged, "RTED did not converge under line trip!") self.assertAlmostEqual(self.ss.RTED.get(src='plf', attr='v', idx='Line_3'), 0, places=6, - msg="Line trip does not take effect in RTED!") + msg="Line trip does not take effect!") From 6ebc2b3c002c7d002abd34010efc8ff98dd26748 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 09:47:05 -0400 Subject: [PATCH 055/181] Typo --- tests/test_rtn_dcopf.py | 12 ++++++------ tests/test_rtn_ed.py | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test_rtn_dcopf.py b/tests/test_rtn_dcopf.py index e4ba7d58..72f956a7 100644 --- a/tests/test_rtn_dcopf.py +++ b/tests/test_rtn_dcopf.py @@ -28,15 +28,15 @@ def test_trip(self): self.ss.DCOPF.run(solver='CLARABEL') obj_pqt = self.ss.DCOPF.obj.v - self.assertLess(obj_pqt, obj, "Load set does not take effect in DCOPF!") + self.assertLess(obj_pqt, obj, "Load set does not take effect!") # --- trip load --- - self.ss.PQ.set(src='u', attr='v', idx='PQ_1', value=0) + self.ss.PQ.set(src='u', attr='v', idx='PQ_2', value=0) self.ss.DCOPF.update() self.ss.DCOPF.run(solver='CLARABEL') obj_pqt2 = self.ss.DCOPF.obj.v - self.assertLess(obj_pqt2, obj_pqt, "Load trip does not take effect in DCOPF!") + self.assertLess(obj_pqt2, obj_pqt, "Load trip does not take effect!") # --- trip generator --- self.ss.StaticGen.set(src='u', attr='v', idx='PV_1', value=0) @@ -46,10 +46,10 @@ def test_trip(self): self.assertTrue(self.ss.DCOPF.converged, "DCOPF did not converge under generator trip!") self.assertAlmostEqual(self.ss.DCOPF.get(src='pg', attr='v', idx='PV_1'), 0, places=6, - msg="Generator trip does not take effect in DCOPF!") + msg="Generator trip does not take effect!") self.assertAlmostEqual(self.ss.DCOPF.get(src='pg', attr='v', idx='PV_1'), 0, places=6, - msg="Generator trip does not take effect in DCOPF!") + msg="Generator trip does not take effect!") # --- trip line --- self.ss.Line.set(src='u', attr='v', idx='Line_3', value=0) @@ -60,4 +60,4 @@ def test_trip(self): self.assertAlmostEqual(self.ss.DCOPF.get(src='plf', attr='v', idx='Line_3'), 0, places=6, - msg="Line trip does not take effect in DCOPF!") + msg="Line trip does not take effect!") diff --git a/tests/test_rtn_ed.py b/tests/test_rtn_ed.py index 2eaccea3..6d2ec77f 100644 --- a/tests/test_rtn_ed.py +++ b/tests/test_rtn_ed.py @@ -29,7 +29,7 @@ def test_trip(self): self.ss.ED.run(solver='CLARABEL') obj_pqt = self.ss.ED.obj.v - self.assertLess(obj_pqt, obj, "Load trip does not take effect in ED!") + self.assertLess(obj_pqt, obj, "Load trip does not take effect!") # --- trip generator --- self.ss.StaticGen.set(src='u', attr='v', idx='PV_1', value=0) @@ -38,7 +38,7 @@ def test_trip(self): self.ss.ED.run(solver='CLARABEL') self.assertTrue(self.ss.ED.converged, "ED did not converge under generator trip!") obj_gt = self.ss.ED.obj.v - self.assertGreater(obj_gt, obj, "Generator trip does not take effect in ED!") + self.assertGreater(obj_gt, obj, "Generator trip does not take effect!") pg_trip = self.ss.ED.get(src='pg', attr='v', idx='PV_1') np.testing.assert_almost_equal(pg_trip, 0, decimal=6) @@ -50,7 +50,7 @@ def test_trip(self): self.ss.ED.run(solver='CLARABEL') self.assertTrue(self.ss.Ed.converged, "ED did not converge under line trip!") obj_lt = self.ss.ED.obj.v - self.assertGreater(obj_lt, obj_gt, "Line trip does not take effect in ED!") + self.assertGreater(obj_lt, obj_gt, "Line trip does not take effect!") plf_trip = self.ss.ED.get(src='plf', attr='v', idx='Line_3') np.testing.assert_almost_equal(plf_trip, 0, decimal=6) From 44c4c459c02ba43ac8c4570152229a908e2d36e4 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 09:48:05 -0400 Subject: [PATCH 056/181] Fix TestRTED --- tests/test_rtn_rted.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_rtn_rted.py b/tests/test_rtn_rted.py index e02a95f4..122d4aa7 100644 --- a/tests/test_rtn_rted.py +++ b/tests/test_rtn_rted.py @@ -50,7 +50,7 @@ def test_trip(self): # --- set load --- self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0) - self.ss.DCOPF.update() + self.ss.RTED.update() self.ss.RTED.run(solver='CLARABEL') obj_pqt = self.ss.RTED.obj.v From 8a58d3cf48c3560c1c3e30003ba07f0b280987bc Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 09:48:57 -0400 Subject: [PATCH 057/181] Format --- tests/test_rtn_rted.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_rtn_rted.py b/tests/test_rtn_rted.py index 122d4aa7..d6461b09 100644 --- a/tests/test_rtn_rted.py +++ b/tests/test_rtn_rted.py @@ -1,8 +1,6 @@ import unittest import numpy as np -from andes.shared import pd - import ams From 6130f1859a51b5762c10e3a94e4cb04e69d34be8 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 10:04:33 -0400 Subject: [PATCH 058/181] Minor --- tests/test_rtn_dcopf.py | 2 +- tests/test_rtn_rted.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_rtn_dcopf.py b/tests/test_rtn_dcopf.py index 72f956a7..a036a91c 100644 --- a/tests/test_rtn_dcopf.py +++ b/tests/test_rtn_dcopf.py @@ -23,7 +23,7 @@ def test_trip(self): obj = self.ss.DCOPF.obj.v # --- set load --- - self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0) + self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0.1) self.ss.DCOPF.update() self.ss.DCOPF.run(solver='CLARABEL') diff --git a/tests/test_rtn_rted.py b/tests/test_rtn_rted.py index d6461b09..57b5505b 100644 --- a/tests/test_rtn_rted.py +++ b/tests/test_rtn_rted.py @@ -47,7 +47,7 @@ def test_trip(self): obj = self.ss.RTED.obj.v # --- set load --- - self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0) + self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0.1) self.ss.RTED.update() self.ss.RTED.run(solver='CLARABEL') From bb29aa1acf71d94f886fd9a90cdd6815575f4189 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 10:19:48 -0400 Subject: [PATCH 059/181] In Service LoadScale, consider load online status --- ams/core/service.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ams/core/service.py b/ams/core/service.py index 84d1ed26..d94ce5e8 100644 --- a/ams/core/service.py +++ b/ams/core/service.py @@ -178,7 +178,7 @@ def __init__(self, class LoadScale(ROperationService): """ - Return load. + Get zonal load by scale nodal load given the corresponding load scale factor. Parameters ---------- @@ -218,11 +218,12 @@ def __init__(self, def v(self): sys = self.rtn.system u_idx = self.u.get_idx() + ue = self.u.owner.get(src='u', attr='v', idx=u_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]) + (ue * self.u.v)[:, np.newaxis]) if self.sparse: return spr.csr_matrix(p0s) return p0s From 3e90e4fcf21dedc06d6052d617819ac0f2960866 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 10:21:35 -0400 Subject: [PATCH 060/181] Add init test to TestDCOPF and TestRTED --- tests/test_rtn_dcopf.py | 7 +++++++ tests/test_rtn_rted.py | 43 ++++++++++++++++++++++++----------------- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/tests/test_rtn_dcopf.py b/tests/test_rtn_dcopf.py index a036a91c..fc928d84 100644 --- a/tests/test_rtn_dcopf.py +++ b/tests/test_rtn_dcopf.py @@ -14,6 +14,13 @@ def setUp(self) -> None: # decrease load first self.ss.PQ.set(src='p0', attr='v', idx=['PQ_1', 'PQ_2'], value=[0.3, 0.3]) + def test_init(self): + """ + Test initialization. + """ + self.ss.DCOPF.init() + self.assertTrue(self.ss.DCOPF.initialized, "DCOPF initialization failed!") + def test_trip(self): """ Test generator trip. diff --git a/tests/test_rtn_rted.py b/tests/test_rtn_rted.py index 57b5505b..df9d6845 100644 --- a/tests/test_rtn_rted.py +++ b/tests/test_rtn_rted.py @@ -17,27 +17,12 @@ def setUp(self) -> None: ) self.ss.RTED.run(solver='CLARABEL') - def test_dc2ac(self): + def test_init(self): """ Test `RTED.init()` method. """ - self.ss.RTED.dc2ac() - self.assertTrue(self.ss.RTED.converted, "AC conversion failed!") - self.assertTrue(self.ss.RTED.exec_time > 0, "Execution time is not greater than 0.") - - stg_idx = self.ss.StaticGen.get_idx() - pg_rted = self.ss.RTED.get(src='pg', attr='v', idx=stg_idx) - pg_acopf = self.ss.ACOPF.get(src='pg', attr='v', idx=stg_idx) - np.testing.assert_almost_equal(pg_rted, pg_acopf, decimal=3) - - bus_idx = self.ss.Bus.get_idx() - v_rted = self.ss.RTED.get(src='vBus', attr='v', idx=bus_idx) - v_acopf = self.ss.ACOPF.get(src='vBus', attr='v', idx=bus_idx) - np.testing.assert_almost_equal(v_rted, v_acopf, decimal=3) - - a_rted = self.ss.RTED.get(src='aBus', attr='v', idx=bus_idx) - a_acopf = self.ss.ACOPF.get(src='aBus', attr='v', idx=bus_idx) - np.testing.assert_almost_equal(a_rted, a_acopf, decimal=3) + self.ss.RTED.init() + self.assertTrue(self.ss.RTED.initialized, "RTED initialization failed!") def test_trip(self): """ @@ -84,3 +69,25 @@ def test_trip(self): self.assertAlmostEqual(self.ss.RTED.get(src='plf', attr='v', idx='Line_3'), 0, places=6, msg="Line trip does not take effect!") + + def test_dc2ac(self): + """ + Test `RTED.init()` method. + """ + self.ss.RTED.dc2ac() + self.assertTrue(self.ss.RTED.converted, "AC conversion failed!") + self.assertTrue(self.ss.RTED.exec_time > 0, "Execution time is not greater than 0.") + + stg_idx = self.ss.StaticGen.get_idx() + pg_rted = self.ss.RTED.get(src='pg', attr='v', idx=stg_idx) + pg_acopf = self.ss.ACOPF.get(src='pg', attr='v', idx=stg_idx) + np.testing.assert_almost_equal(pg_rted, pg_acopf, decimal=3) + + bus_idx = self.ss.Bus.get_idx() + v_rted = self.ss.RTED.get(src='vBus', attr='v', idx=bus_idx) + v_acopf = self.ss.ACOPF.get(src='vBus', attr='v', idx=bus_idx) + np.testing.assert_almost_equal(v_rted, v_acopf, decimal=3) + + a_rted = self.ss.RTED.get(src='aBus', attr='v', idx=bus_idx) + a_acopf = self.ss.ACOPF.get(src='aBus', attr='v', idx=bus_idx) + np.testing.assert_almost_equal(a_rted, a_acopf, decimal=3) From 17d69ed88082f3c8c8ab8ef40715bfdf3fde842b Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 10:22:11 -0400 Subject: [PATCH 061/181] Minor --- ams/routines/ed.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/ams/routines/ed.py b/ams/routines/ed.py index 02f17c1a..e1c5500a 100644 --- a/ams/routines/ed.py +++ b/ams/routines/ed.py @@ -92,15 +92,10 @@ def __init__(self) -> None: name='RR30', tex_name=r'R_{30,R}', info='Repeated ramp rate', no_parse=True,) - 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.Pfinj.expand_dims = 1 - self.Pbusinj.expand_dims = 1 - self.gsh.expand_dims = 1 + items_to_expand = ['ctrl', 'c0', 'pmax', 'pmin', 'pg0', 'rate_a', + 'Pfinj', 'Pbusinj', 'gsh'] + for item in items_to_expand: + self.__dict__[item].expand_dims = 1 # NOTE: extend pg to 2D matrix: row for gen and col for timeslot self.pg.horizon = self.timeslot From ae1ea2e152783c42f9e166ecfcd552b3b70cf771 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 10:23:11 -0400 Subject: [PATCH 062/181] Add init test to TestED --- tests/test_rtn_ed.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/tests/test_rtn_ed.py b/tests/test_rtn_ed.py index 6d2ec77f..ce17dbea 100644 --- a/tests/test_rtn_ed.py +++ b/tests/test_rtn_ed.py @@ -15,6 +15,13 @@ def setUp(self) -> None: # decrease load first self.ss.PQ.set(src='p0', attr='v', idx=['PQ_1', 'PQ_2'], value=[0.3, 0.3]) + def test_init(self): + """ + Test initialization. + """ + self.ss.ED.init() + self.assertTrue(self.ss.ED.initialized, "ED initialization failed!") + def test_trip(self): """ Test generator trip. @@ -22,14 +29,21 @@ def test_trip(self): self.ss.ED.run(solver='CLARABEL') obj = self.ss.ED.obj.v - # --- trip load --- - self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0) + # --- set load --- + self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0.1) self.ss.ED.update() self.ss.ED.run(solver='CLARABEL') obj_pqt = self.ss.ED.obj.v + self.assertLess(obj_pqt, obj, "Load set does not take effect!") - self.assertLess(obj_pqt, obj, "Load trip does not take effect!") + # --- trip load --- + self.ss.PQ.set(src='u', attr='v', idx='PQ_2', value=0) + self.ss.ED.update() + + self.ss.ED.run(solver='CLARABEL') + obj_pqt2 = self.ss.ED.obj.v + self.assertLess(obj_pqt2, obj_pqt, "Load trip does not take effect!") # --- trip generator --- self.ss.StaticGen.set(src='u', attr='v', idx='PV_1', value=0) From 3fae4d14963047475bb7cca45a491ce67e91262e Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 10:47:56 -0400 Subject: [PATCH 063/181] Set overwrite as True in io test --- tests/test_case.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_case.py b/tests/test_case.py index 8c2869ef..6f65d5f2 100644 --- a/tests/test_case.py +++ b/tests/test_case.py @@ -148,7 +148,7 @@ def test_ieee14_raw2xlsx(self): no_output=True, default_config=True, ) - ams.io.xlsx.write(ss, "ieee14.xlsx") + ams.io.xlsx.write(ss, "ieee14.xlsx", overwrite=True) self.assertTrue(os.path.exists("ieee14.xlsx")) os.remove("ieee14.xlsx") @@ -159,7 +159,7 @@ def test_ieee14_raw2json(self): no_output=True, default_config=True, ) - ams.io.json.write(ss, "ieee14.json") + ams.io.json.write(ss, "ieee14.json", overwrite=True) self.assertTrue(os.path.exists("ieee14.json")) os.remove("ieee14.json") From 719e10c29b25e7fb6cd2e8ff3fb26e7922fc6ae7 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 10:50:43 -0400 Subject: [PATCH 064/181] Format input parameters of RoutineBase.init and all derived calsses --- ams/routines/acopf.py | 10 +++++----- ams/routines/dcopf.py | 10 ++++++---- ams/routines/dcpf.py | 11 +++++------ ams/routines/pflow.py | 4 ++-- ams/routines/routine.py | 30 +++++++++++++++++++++--------- ams/routines/uc.py | 8 ++++++-- 6 files changed, 45 insertions(+), 28 deletions(-) diff --git a/ams/routines/acopf.py b/ams/routines/acopf.py index 3f46f6d7..8142e883 100644 --- a/ams/routines/acopf.py +++ b/ams/routines/acopf.py @@ -93,10 +93,11 @@ def solve(self, method=None, **kwargs): res, sstats = runopf(casedata=ppc, ppopt=ppopt, **kwargs) return res, sstats - def run(self, force_init=False, no_code=True, - method=None, **kwargs): + def run(self, no_code=True, method=None, + *args, **kwargs): """ Run ACOPF using PYPOWER with PIPS. + *args and **kwargs go to `self.solve()`, which are not used yet. Examples -------- @@ -117,6 +118,5 @@ def run(self, force_init=False, no_code=True, exit_code : int Exit code of the routine. """ - super().run(force_init=force_init, - no_code=no_code, method=method, - **kwargs, ) + super().run(no_code=no_code, method=method, + *args, **kwargs, ) diff --git a/ams/routines/dcopf.py b/ams/routines/dcopf.py index b8422f39..7e9ba1fd 100644 --- a/ams/routines/dcopf.py +++ b/ams/routines/dcopf.py @@ -225,15 +225,17 @@ def __init__(self, system, config): info='total cost', unit='$', sense='min', e_str=obj,) - def solve(self, **kwargs): + def solve(self, *args, **kwargs): """ Solve the routine optimization model. + *args and **kwargs go to `self.om.prob.solve()` (`cvxpy.Problem.solve()`). """ - return self.om.prob.solve(**kwargs) + return self.om.prob.solve(*args, **kwargs) - def run(self, no_code=True, **kwargs): + def run(self, no_code=True, *args, **kwargs): """ Run the routine. + *args and **kwargs go to `self.solve()`. Parameters ---------- @@ -270,7 +272,7 @@ def run(self, no_code=True, **kwargs): kwargs : keywords, optional Additional solver specific arguments. See CVXPY documentation for details. """ - return RoutineBase.run(self, no_code=no_code, **kwargs) + return RoutineBase.run(self, no_code=no_code, *args, **kwargs) def _post_solve(self): """ diff --git a/ams/routines/dcpf.py b/ams/routines/dcpf.py index ff0e89de..68e69e30 100644 --- a/ams/routines/dcpf.py +++ b/ams/routines/dcpf.py @@ -127,10 +127,11 @@ def solve(self, method=None): res, sstats = runpf(casedata=ppc, ppopt=ppopt) return res, sstats - def run(self, force_init=False, no_code=True, - method=None, **kwargs): + def run(self, no_code=True, method=None, + *args, **kwargs): """ Run DC pwoer flow. + *args and **kwargs go to `self.solve()`, which are not used yet. Examples -------- @@ -139,8 +140,6 @@ def run(self, force_init=False, no_code=True, Parameters ---------- - force_init : bool - Force initialization. no_code : bool Disable showing code. method : str @@ -152,9 +151,9 @@ def run(self, force_init=False, no_code=True, Exit code of the routine. """ if not self.initialized: - self.init(force=force_init, no_code=no_code) + self.init(no_code=no_code) t0, _ = elapsed() - res, sstats = self.solve(method=method) + res, sstats = self.solve(method=method, *args, **kwargs) self.converged = res['success'] self.exit_code = 0 if res['success'] else 1 _, s = elapsed(t0) diff --git a/ams/routines/pflow.py b/ams/routines/pflow.py index 057b8c92..27318067 100644 --- a/ams/routines/pflow.py +++ b/ams/routines/pflow.py @@ -59,7 +59,7 @@ def __init__(self, system, config): model="StaticGen", src="q",) # NOTE: omit AC power flow formulation here - def solve(self, method="newton"): + def solve(self, method="newton", **kwargs): """ Solve the AC power flow using PYPOWER. """ @@ -73,7 +73,7 @@ def solve(self, method="newton"): if alg is None: msg = f"Invalid method `{method}` for PFlow." raise ValueError(msg) - ppopt = ppoption(PF_ALG=alg, ENFORCE_Q_LIMS=self.config.qlim) + ppopt = ppoption(PF_ALG=alg, ENFORCE_Q_LIMS=self.config.qlim, **kwargs) res, sstats = runpf(casedata=ppc, ppopt=ppopt) return res, sstats diff --git a/ams/routines/routine.py b/ams/routines/routine.py index 8d194443..e9fb2eff 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -239,8 +239,7 @@ def _data_check(self, info=True): def init(self, no_code=True, force_mats=False, force_constr=False, - force_parse=False, force_generate=False, - **kwargs): + force_parse=False, force_generate=False): """ Initialize the routine. @@ -279,11 +278,11 @@ def init(self, no_code=True, _ = self._get_off_constrs() if not self.om.initialized: - om_init = self.om.init(no_code=no_code, force_parse=force_parse, force_generate=force_generate) + self.om.init(no_code=no_code, force_parse=force_parse, force_generate=force_generate) _, s_init = elapsed(t0) msg = f"<{self.class_name}> " - if om_init: + if self.om.initialized: msg += f"initialized in {s_init}." self.initialized = True else: @@ -310,9 +309,14 @@ def _post_solve(self): """ return None - def run(self, force_init=False, no_code=True, **kwargs): + def run(self, + force_mats=False, force_constr=False, + force_parse=False, force_generate=False, + no_code=True, + *args, **kwargs): """ Run the routine. + *args and **kwargs go to `self.solve()`. Force initialization (`force_init=True`) will do the following: - Rebuild the system matrices @@ -321,16 +325,24 @@ def run(self, force_init=False, no_code=True, **kwargs): Parameters ---------- - force_init: bool - Whether to force initialization. + force_mats: bool + Whether to force build the system matrices, goes to `self.init()`. + force_constr: bool + Whether to turn on all constraints, goes to `self.init()`. + force_parse: bool + Whether to force parse the optimization model, goes to `self.init()`. + force_generate: bool + Whether to force generate symbols, goes to `self.init()` no_code: bool Whether to show generated code. """ # --- setup check --- - self.init(force=force_init, no_code=no_code) + self.init(force_mats=force_mats, force_constr=force_constr, + force_parse=force_parse, force_generate=force_generate, + no_code=no_code) # --- solve optimization --- t0, _ = elapsed() - _ = self.solve(**kwargs) + _ = self.solve(*args, **kwargs) status = self.om.prob.status self.exit_code = self.syms.status[status] self.converged = self.exit_code == 0 diff --git a/ams/routines/uc.py b/ams/routines/uc.py index ba5735d4..b2003127 100644 --- a/ams/routines/uc.py +++ b/ams/routines/uc.py @@ -308,9 +308,13 @@ def _initial_guess(self): logger.warning(f'Turn off StaticGen {g_idx} as initial commitment guess.') return True - def init(self, force=False, no_code=True, **kwargs): + def init(self, no_code=True, + force_mats=False, force_constr=False, + force_parse=False, force_generate=False): self._initial_guess() - return super().init(force=force, no_code=no_code, **kwargs) + return super().init(no_code=no_code, + force_mats=force_mats, force_constr=force_constr, + force_parse=force_parse, force_generate=force_generate) def dc2ac(self, **kwargs): """ From 35cdf89e6822e9532d12b78f5a1164a83a90c6db Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 11:12:12 -0400 Subject: [PATCH 065/181] Clarify gen online status is determined by EDTSlot.ug rather than StaticGen.u in routine ED --- ams/routines/ed.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ams/routines/ed.py b/ams/routines/ed.py index e1c5500a..f534a85d 100644 --- a/ams/routines/ed.py +++ b/ams/routines/ed.py @@ -115,13 +115,15 @@ class ED(RTED, MPBase, SRBase): - Vars ``pg``, ``pru``, ``prd`` are extended to 2D - 2D Vars ``rgu`` and ``rgd`` are introduced - - Param ``ug`` is sourced from ``EDTSlot.ug`` as commitment decisions + - Param ``ug`` is sourced from ``EDTSlot.ug`` as generator commitment Notes ----- 1. Formulations has been adjusted with interval ``config.t`` 2. The tie-line flow is not implemented in this model. + + 3. `EDTSlot.ug` is used instead of `StaticGen.u` for generator commitment. """ def __init__(self, system, config): From bdc90a5e278022318c6a3bda9fbb5752e9f8c348 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 11:34:58 -0400 Subject: [PATCH 066/181] Clarify matprocessor connectivity matrices do not consider devices online status --- ams/core/matprocessor.py | 5 +++++ examples/ex2.ipynb | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/ams/core/matprocessor.py b/ams/core/matprocessor.py index c091dab6..4f34ce25 100644 --- a/ams/core/matprocessor.py +++ b/ams/core/matprocessor.py @@ -192,6 +192,11 @@ def build(self, force_mats=False): It build connectivity matrices first: Cg, Cl, Csh, Cft, and CftT. Then build bus matrices: Bf, Bbus, Pfinj, and Pbusinj. + Notes + ----- + Generator online status is NOT considered in its connectivity matrix. + The same applies for load, line, and shunt. + Returns ------- initialized : bool diff --git a/examples/ex2.ipynb b/examples/ex2.ipynb index bfa427c3..8a19c83d 100644 --- a/examples/ex2.ipynb +++ b/examples/ex2.ipynb @@ -753,7 +753,10 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We can see that there are three PV generators in the system." + "We can see that there are three PV generators in the system.\n", + "\n", + "> **Warning:** in `MatProcessor`, `StaticGen` online status is NOT considered in its connectivity matrix `Cg`.\n", + "> The same applies for `PQ`, `Line`, and `Shunt`." ] }, { From eb26aa3c2fd7d0a1c396e878060cb2cbdc36605b Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 11:55:55 -0400 Subject: [PATCH 067/181] Consider line status in ED --- ams/routines/ed.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ams/routines/ed.py b/ams/routines/ed.py index f534a85d..6364955d 100644 --- a/ams/routines/ed.py +++ b/ams/routines/ed.py @@ -93,7 +93,7 @@ def __init__(self) -> None: info='Repeated ramp rate', no_parse=True,) items_to_expand = ['ctrl', 'c0', 'pmax', 'pmin', 'pg0', 'rate_a', - 'Pfinj', 'Pbusinj', 'gsh'] + 'Pfinj', 'Pbusinj', 'gsh', 'ul'] for item in items_to_expand: self.__dict__[item].expand_dims = 1 @@ -179,8 +179,8 @@ def __init__(self, system, config): # --- line --- self.plf.horizon = self.timeslot self.plf.info = '2D Line flow' - self.plflb.e_str = '-Bf@aBus - Pfinj@tlv - rate_a@tlv' - self.plfub.e_str = 'Bf@aBus + Pfinj@tlv - rate_a@tlv' + self.plflb.e_str = '-Bf@aBus - Pfinj@tlv - mul(ul, rate_a)@tlv' + self.plfub.e_str = 'Bf@aBus + Pfinj@tlv - mul(ul, rate_a)@tlv' self.alflb.e_str = '-CftT@aBus + amin@tlv' self.alfub.e_str = 'CftT@aBus - amax@tlv' From 324e63bc2b5478ecf78aabc949bd9432a4286a62 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 11:57:40 -0400 Subject: [PATCH 068/181] [WIP] Fix TestED --- tests/test_rtn_ed.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/test_rtn_ed.py b/tests/test_rtn_ed.py index ce17dbea..dd8f3de3 100644 --- a/tests/test_rtn_ed.py +++ b/tests/test_rtn_ed.py @@ -45,26 +45,26 @@ def test_trip(self): obj_pqt2 = self.ss.ED.obj.v self.assertLess(obj_pqt2, obj_pqt, "Load trip does not take effect!") - # --- trip generator --- - self.ss.StaticGen.set(src='u', attr='v', idx='PV_1', value=0) + # --- trip line --- + self.ss.Line.set(src='u', attr='v', idx='Line_3', value=0) self.ss.ED.update() self.ss.ED.run(solver='CLARABEL') - self.assertTrue(self.ss.ED.converged, "ED did not converge under generator trip!") - obj_gt = self.ss.ED.obj.v - self.assertGreater(obj_gt, obj, "Generator trip does not take effect!") - - pg_trip = self.ss.ED.get(src='pg', attr='v', idx='PV_1') - np.testing.assert_almost_equal(pg_trip, 0, decimal=6) + self.assertTrue(self.ss.ED.converged, "ED did not converge under line trip!") + plf_l3 = self.ss.ED.get(src='plf', attr='v', idx='Line_3') + np.testing.assert_almost_equal(np.zeros_like(plf_l3), + plf_l3, decimal=6) - # --- trip line --- - self.ss.Line.set(src='u', attr='v', idx='Line_3', value=0) + # --- trip generator --- + # a) check StaticGen.u does not take effect + # NOTE: in ED, `EDTSlot.ug` is used instead of `StaticGen.u` + self.ss.StaticGen.set(src='u', attr='v', idx='PV_1', value=0) self.ss.ED.update() self.ss.ED.run(solver='CLARABEL') - self.assertTrue(self.ss.Ed.converged, "ED did not converge under line trip!") - obj_lt = self.ss.ED.obj.v - self.assertGreater(obj_lt, obj_gt, "Line trip does not take effect!") + self.assertTrue(self.ss.ED.converged, "ED did not converge under generator trip!") + pg_pv1 = self.ss.ED.get(src='pg', attr='v', idx='PV_1') + np.testing.assert_array_less(np.zeros_like(pg_pv1), pg_pv1, + err_msg="Generator trip take effect, which is unexpected!") - plf_trip = self.ss.ED.get(src='plf', attr='v', idx='Line_3') - np.testing.assert_almost_equal(plf_trip, 0, decimal=6) + # b) check EDTSlot.ug takes effect From 06901732f8b03faabb69ce4b503aabb3623e3430 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 12:22:36 -0400 Subject: [PATCH 069/181] Set ED default interval config.t as 1 hour --- ams/routines/ed.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/ams/routines/ed.py b/ams/routines/ed.py index 6364955d..937d2c31 100644 --- a/ams/routines/ed.py +++ b/ams/routines/ed.py @@ -2,7 +2,6 @@ Economic dispatch routines. """ import logging -from collections import OrderedDict import numpy as np from ams.core.param import RParam @@ -131,8 +130,7 @@ def __init__(self, system, config): MPBase.__init__(self) SRBase.__init__(self) - self.config.add(OrderedDict((('t', 1), - ))) + self.config.t = 1 # scheduling interval in hour self.config.add_extra("_help", t="time interval in hours", ) @@ -244,8 +242,6 @@ def __init__(self, system, config): ED.__init__(self, system, config) DGBase.__init__(self) - self.config.t = 1 # scheduling interval in hour - self.info = 'Economic dispatch with distributed generation' self.type = 'DCED' @@ -298,8 +294,6 @@ def __init__(self, system, config): ED.__init__(self, system, config) ESD1MPBase.__init__(self) - self.config.t = 1 # scheduling interval in hour - self.info = 'Economic dispatch with energy storage' self.type = 'DCED' From 9f303f228823b572393c53a88751b7b4fbc53b23 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 14:04:49 -0400 Subject: [PATCH 070/181] Rename property method v2 as e for classes Constraint and Objective --- ams/opt/omodel.py | 19 +++++++++++++------ tests/test_routine.py | 4 ++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index 55d390a2..9c36e1c2 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -548,12 +548,15 @@ def __repr__(self): return out @property - def v2(self): + def e(self): """ Return the calculated constraint LHS value. - Note that ``v`` should be used primarily as it is obtained + Note that `v` should be used primarily as it is obtained from the solver directly. - ``v2`` is for debugging purpose, and should be consistent with ``v``. + + `e` is for debugging purpose. For a successfully solved problem, + `e` should equal to `v`. However, when a problem is infeasible + or unbounded, `e` can be used to check the constraint LHS value. """ if self.code is None: logger.info(f"Constraint <{self.name}> is not parsed yet.") @@ -638,12 +641,16 @@ def __init__(self, self.code = None @property - def v2(self): + def e(self): """ Return the calculated objective value. - Note that ``v`` should be used primarily as it is obtained + + Note that `v` should be used primarily as it is obtained from the solver directly. - ``v2`` is for debugging purpose, and should be consistent with ``v``. + + `e` is for debugging purpose. For a successfully solved problem, + `e` should equal to `v`. However, when a problem is infeasible + or unbounded, `e` can be used to check the objective value. """ if self.code is None: logger.info(f"Objective <{self.name}> is not parsed yet.") diff --git a/tests/test_routine.py b/tests/test_routine.py index f9ddf843..86ce37be 100644 --- a/tests/test_routine.py +++ b/tests/test_routine.py @@ -92,10 +92,10 @@ def test_value_method(self): # --- constraint values --- for constr in self.ss.DCOPF.constrs.values(): - np.testing.assert_almost_equal(constr.v, constr.v2, decimal=6) + np.testing.assert_almost_equal(constr.v, constr.e, decimal=6) # --- objective value --- - self.assertAlmostEqual(self.ss.DCOPF.obj.v, self.ss.DCOPF.obj.v2, places=6) + self.assertAlmostEqual(self.ss.DCOPF.obj.v, self.ss.DCOPF.obj.e, places=6) class TestSetOptzValueACOPF(unittest.TestCase): From 71a842c70ce6fe160abef38f1a8a0d4407f416ad Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 14:28:51 -0400 Subject: [PATCH 071/181] Fix expressions of rbu, rbd, rgu0, rgud0 of ED --- ams/routines/ed.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ams/routines/ed.py b/ams/routines/ed.py index 937d2c31..24b4b989 100644 --- a/ams/routines/ed.py +++ b/ams/routines/ed.py @@ -191,19 +191,19 @@ def __init__(self, system, config): 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(ugt, pg + pru) - mul(pmax, tlv)' - self.rrd.e_str = 'mul(ugt, -pg + prd) + mul(pmin, tlv)' + self.rru.e_str = 'pg + pru - mul(mul(ugt, pmax), tlv)' + self.rrd.e_str = '-pg + prd + mul(mul(ugt, 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[:, 0] - R30', + e_str='mul(ugt[:, 0], pg[:, 0] - pg0[:, 0] - R30)', is_eq=False,) self.rgd0 = Constraint(name='rgd0', info='Initial gen ramping down', - e_str='- pg[:, 0] + pg0[:, 0] - R30', + e_str='mul(ugt[:, 0], -pg[:, 0] + pg0[:, 0] - R30)', is_eq=False,) # --- objective --- From bbe6e9c6f620dbb72c313d28ba146433c8fad5e0 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 15:09:45 -0400 Subject: [PATCH 072/181] [WIP] Fix TestED test_trip --- tests/test_rtn_ed.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/tests/test_rtn_ed.py b/tests/test_rtn_ed.py index dd8f3de3..e0d13e41 100644 --- a/tests/test_rtn_ed.py +++ b/tests/test_rtn_ed.py @@ -56,15 +56,32 @@ def test_trip(self): plf_l3, decimal=6) # --- trip generator --- - # a) check StaticGen.u does not take effect + stg = 'PV_1' + stg_uid = self.ss.ED.pg.get_idx().index(stg) + # a) ensure StaticGen.u does not take effect # NOTE: in ED, `EDTSlot.ug` is used instead of `StaticGen.u` - self.ss.StaticGen.set(src='u', attr='v', idx='PV_1', value=0) + self.ss.StaticGen.set(src='u', attr='v', idx=stg, value=0) self.ss.ED.update() self.ss.ED.run(solver='CLARABEL') self.assertTrue(self.ss.ED.converged, "ED did not converge under generator trip!") - pg_pv1 = self.ss.ED.get(src='pg', attr='v', idx='PV_1') + pg_pv1 = self.ss.ED.get(src='pg', attr='v', idx=stg) np.testing.assert_array_less(np.zeros_like(pg_pv1), pg_pv1, err_msg="Generator trip take effect, which is unexpected!") - # b) check EDTSlot.ug takes effect + # TODO: DEBUG: NOT SURE THIS TEST FAILED + # b) ensure EDTSlot.ug takes effect + # NOTE: manually chang ug.v for testing purpose + loc_offtime = np.array([0, 2, 4]) + self.ss.EDTSlot.ug.v[loc_offtime, stg_uid] = 0 + + self.ss.ED.run(solver='CLARABEL') + self.assertTrue(self.ss.ED.converged, "ED did not converge under generator trip!") + pg_pv1 = self.ss.ED.get(src='pg', attr='v', idx=stg) + print('stg_uid', stg_uid) + print(self.ss.EDTSlot.ug.v) + print(pg_pv1) + np.testing.assert_almost_equal(np.zeros_like(loc_offtime), + pg_pv1[loc_offtime], + decimal=6, + err_msg="Generator trip does not take effect!") From 7a5cce643bf78bbc2dfc6ab0dcd6b8ba2ea4b562 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 15:35:03 -0400 Subject: [PATCH 073/181] [WIP] Fix TestED.test_trip_gen --- tests/test_rtn_ed.py | 92 +++++++++++++++++++++++++++++--------------- 1 file changed, 62 insertions(+), 30 deletions(-) diff --git a/tests/test_rtn_ed.py b/tests/test_rtn_ed.py index e0d13e41..9e2e54f6 100644 --- a/tests/test_rtn_ed.py +++ b/tests/test_rtn_ed.py @@ -22,6 +22,38 @@ def test_init(self): self.ss.ED.init() self.assertTrue(self.ss.ED.initialized, "ED initialization failed!") + def test_trip_gen(self): + """ + Test generator tripping. + """ + # a) ensure EDTSlot.ug takes effect + # NOTE: manually chang ug.v for testing purpose + stg = 'PV_1' + stg_uid = self.ss.ED.pg.get_idx().index(stg) + loc_offtime = np.array([0, 2, 4]) + self.ss.EDTSlot.ug.v[loc_offtime, stg_uid] = 0 + + self.ss.ED.run(solver='CLARABEL') + self.assertTrue(self.ss.ED.converged, "ED did not converge under generator trip!") + pg_pv1 = self.ss.ED.get(src='pg', attr='v', idx=stg) + np.testing.assert_almost_equal(np.zeros_like(loc_offtime), + pg_pv1[loc_offtime], + decimal=6, + err_msg="Generator trip does not take effect!") + + self.ss.EDTSlot.ug.v[...] = 1 # reset + + # b) ensure StaticGen.u does not take effect + # NOTE: in ED, `EDTSlot.ug` is used instead of `StaticGen.u` + self.ss.StaticGen.set(src='u', attr='v', idx=stg, value=0) + self.ss.ED.update() + + self.ss.ED.run(solver='CLARABEL') + self.assertTrue(self.ss.ED.converged, "ED did not converge under generator trip!") + pg_pv1 = self.ss.ED.get(src='pg', attr='v', idx=stg) + np.testing.assert_array_less(np.zeros_like(pg_pv1), pg_pv1, + err_msg="Generator trip take effect, which is unexpected!") + def test_trip(self): """ Test generator trip. @@ -55,33 +87,33 @@ def test_trip(self): np.testing.assert_almost_equal(np.zeros_like(plf_l3), plf_l3, decimal=6) - # --- trip generator --- - stg = 'PV_1' - stg_uid = self.ss.ED.pg.get_idx().index(stg) - # a) ensure StaticGen.u does not take effect - # NOTE: in ED, `EDTSlot.ug` is used instead of `StaticGen.u` - self.ss.StaticGen.set(src='u', attr='v', idx=stg, value=0) - self.ss.ED.update() - - self.ss.ED.run(solver='CLARABEL') - self.assertTrue(self.ss.ED.converged, "ED did not converge under generator trip!") - pg_pv1 = self.ss.ED.get(src='pg', attr='v', idx=stg) - np.testing.assert_array_less(np.zeros_like(pg_pv1), pg_pv1, - err_msg="Generator trip take effect, which is unexpected!") - - # TODO: DEBUG: NOT SURE THIS TEST FAILED - # b) ensure EDTSlot.ug takes effect - # NOTE: manually chang ug.v for testing purpose - loc_offtime = np.array([0, 2, 4]) - self.ss.EDTSlot.ug.v[loc_offtime, stg_uid] = 0 - - self.ss.ED.run(solver='CLARABEL') - self.assertTrue(self.ss.ED.converged, "ED did not converge under generator trip!") - pg_pv1 = self.ss.ED.get(src='pg', attr='v', idx=stg) - print('stg_uid', stg_uid) - print(self.ss.EDTSlot.ug.v) - print(pg_pv1) - np.testing.assert_almost_equal(np.zeros_like(loc_offtime), - pg_pv1[loc_offtime], - decimal=6, - err_msg="Generator trip does not take effect!") + # # --- trip generator --- + # # TODO: DEBUG: NOT SURE THIS TEST FAILED + # # b) ensure EDTSlot.ug takes effect + # # NOTE: manually chang ug.v for testing purpose + # stg = 'PV_1' + # stg_uid = self.ss.ED.pg.get_idx().index(stg) + # loc_offtime = np.array([0, 2, 4]) + # self.ss.EDTSlot.ug.v[loc_offtime, stg_uid] = 0 + + # self.ss.ED.run(solver='CLARABEL') + # self.assertTrue(self.ss.ED.converged, "ED did not converge under generator trip!") + # pg_pv1 = self.ss.ED.get(src='pg', attr='v', idx=stg) + # print('stg_uid', stg_uid) + # print(self.ss.EDTSlot.ug.v) + # print(pg_pv1) + # np.testing.assert_almost_equal(np.zeros_like(loc_offtime), + # pg_pv1[loc_offtime], + # decimal=6, + # err_msg="Generator trip does not take effect!") + + # # a) ensure StaticGen.u does not take effect + # # NOTE: in ED, `EDTSlot.ug` is used instead of `StaticGen.u` + # self.ss.StaticGen.set(src='u', attr='v', idx=stg, value=0) + # self.ss.ED.update() + + # self.ss.ED.run(solver='CLARABEL') + # self.assertTrue(self.ss.ED.converged, "ED did not converge under generator trip!") + # pg_pv1 = self.ss.ED.get(src='pg', attr='v', idx=stg) + # np.testing.assert_array_less(np.zeros_like(pg_pv1), pg_pv1, + # err_msg="Generator trip take effect, which is unexpected!") From 3ef6e247fdd9e25def23e36a22d2d5ef33a51f9d Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 15:38:49 -0400 Subject: [PATCH 074/181] Fix TestED --- tests/test_rtn_ed.py | 58 ++++++++++++-------------------------------- 1 file changed, 15 insertions(+), 43 deletions(-) diff --git a/tests/test_rtn_ed.py b/tests/test_rtn_ed.py index 9e2e54f6..dfa13ec6 100644 --- a/tests/test_rtn_ed.py +++ b/tests/test_rtn_ed.py @@ -54,9 +54,22 @@ def test_trip_gen(self): np.testing.assert_array_less(np.zeros_like(pg_pv1), pg_pv1, err_msg="Generator trip take effect, which is unexpected!") - def test_trip(self): + def test_trip_line(self): """ - Test generator trip. + Test line tripping. + """ + self.ss.Line.set(src='u', attr='v', idx='Line_3', value=0) + self.ss.ED.update() + + self.ss.ED.run(solver='CLARABEL') + self.assertTrue(self.ss.ED.converged, "ED did not converge under line trip!") + plf_l3 = self.ss.ED.get(src='plf', attr='v', idx='Line_3') + np.testing.assert_almost_equal(np.zeros_like(plf_l3), + plf_l3, decimal=6) + + def test_set_load(self): + """ + Test setting load. """ self.ss.ED.run(solver='CLARABEL') obj = self.ss.ED.obj.v @@ -76,44 +89,3 @@ def test_trip(self): self.ss.ED.run(solver='CLARABEL') obj_pqt2 = self.ss.ED.obj.v self.assertLess(obj_pqt2, obj_pqt, "Load trip does not take effect!") - - # --- trip line --- - self.ss.Line.set(src='u', attr='v', idx='Line_3', value=0) - self.ss.ED.update() - - self.ss.ED.run(solver='CLARABEL') - self.assertTrue(self.ss.ED.converged, "ED did not converge under line trip!") - plf_l3 = self.ss.ED.get(src='plf', attr='v', idx='Line_3') - np.testing.assert_almost_equal(np.zeros_like(plf_l3), - plf_l3, decimal=6) - - # # --- trip generator --- - # # TODO: DEBUG: NOT SURE THIS TEST FAILED - # # b) ensure EDTSlot.ug takes effect - # # NOTE: manually chang ug.v for testing purpose - # stg = 'PV_1' - # stg_uid = self.ss.ED.pg.get_idx().index(stg) - # loc_offtime = np.array([0, 2, 4]) - # self.ss.EDTSlot.ug.v[loc_offtime, stg_uid] = 0 - - # self.ss.ED.run(solver='CLARABEL') - # self.assertTrue(self.ss.ED.converged, "ED did not converge under generator trip!") - # pg_pv1 = self.ss.ED.get(src='pg', attr='v', idx=stg) - # print('stg_uid', stg_uid) - # print(self.ss.EDTSlot.ug.v) - # print(pg_pv1) - # np.testing.assert_almost_equal(np.zeros_like(loc_offtime), - # pg_pv1[loc_offtime], - # decimal=6, - # err_msg="Generator trip does not take effect!") - - # # a) ensure StaticGen.u does not take effect - # # NOTE: in ED, `EDTSlot.ug` is used instead of `StaticGen.u` - # self.ss.StaticGen.set(src='u', attr='v', idx=stg, value=0) - # self.ss.ED.update() - - # self.ss.ED.run(solver='CLARABEL') - # self.assertTrue(self.ss.ED.converged, "ED did not converge under generator trip!") - # pg_pv1 = self.ss.ED.get(src='pg', attr='v', idx=stg) - # np.testing.assert_array_less(np.zeros_like(pg_pv1), pg_pv1, - # err_msg="Generator trip take effect, which is unexpected!") From eb453bd82f58695e59f8297bacc69118e77656dd Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 15:40:25 -0400 Subject: [PATCH 075/181] Minor --- tests/test_rtn_ed.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_rtn_ed.py b/tests/test_rtn_ed.py index dfa13ec6..51e09fcd 100644 --- a/tests/test_rtn_ed.py +++ b/tests/test_rtn_ed.py @@ -54,6 +54,8 @@ def test_trip_gen(self): np.testing.assert_array_less(np.zeros_like(pg_pv1), pg_pv1, err_msg="Generator trip take effect, which is unexpected!") + self.ss.StaticGen.alter(src='u', idx=stg, value=1) # reset + def test_trip_line(self): """ Test line tripping. From c894fdc4f570e47cd7244d6609d3894ea42b7c2c Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 15:42:31 -0400 Subject: [PATCH 076/181] Minor --- tests/test_rtn_ed.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_rtn_ed.py b/tests/test_rtn_ed.py index 51e09fcd..14084c14 100644 --- a/tests/test_rtn_ed.py +++ b/tests/test_rtn_ed.py @@ -69,6 +69,8 @@ def test_trip_line(self): np.testing.assert_almost_equal(np.zeros_like(plf_l3), plf_l3, decimal=6) + self.ss.Line.alter(src='u', idx='Line_3', value=1) # reset + def test_set_load(self): """ Test setting load. @@ -85,7 +87,7 @@ def test_set_load(self): self.assertLess(obj_pqt, obj, "Load set does not take effect!") # --- trip load --- - self.ss.PQ.set(src='u', attr='v', idx='PQ_2', value=0) + self.ss.PQ.alter(src='u', idx='PQ_2', value=0) self.ss.ED.update() self.ss.ED.run(solver='CLARABEL') From 35c37f55cd1711ede5522203b42f72729c903fbc Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 15:43:17 -0400 Subject: [PATCH 077/181] Typo --- tests/test_rtn_ed.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_rtn_ed.py b/tests/test_rtn_ed.py index 14084c14..448c8777 100644 --- a/tests/test_rtn_ed.py +++ b/tests/test_rtn_ed.py @@ -73,7 +73,7 @@ def test_trip_line(self): def test_set_load(self): """ - Test setting load. + Test setting and tripping load. """ self.ss.ED.run(solver='CLARABEL') obj = self.ss.ED.obj.v From ef8d149e722ec6bc2b4bfbba797521f3b952ff08 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 15:44:05 -0400 Subject: [PATCH 078/181] Refactor TestDCOPF --- tests/test_rtn_dcopf.py | 59 +++++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/tests/test_rtn_dcopf.py b/tests/test_rtn_dcopf.py index fc928d84..5fc8f8b0 100644 --- a/tests/test_rtn_dcopf.py +++ b/tests/test_rtn_dcopf.py @@ -21,9 +21,40 @@ def test_init(self): self.ss.DCOPF.init() self.assertTrue(self.ss.DCOPF.initialized, "DCOPF initialization failed!") - def test_trip(self): + def test_trip_gen(self): """ - Test generator trip. + Test generator tripping. + """ + stg = 'PV_1' + self.ss.StaticGen.set(src='u', attr='v', idx=stg, value=0) + + self.ss.DCOPF.update() + self.ss.DCOPF.run(solver='CLARABEL') + self.assertTrue(self.ss.DCOPF.converged, "DCOPF did not converge under generator trip!") + self.assertAlmostEqual(self.ss.DCOPF.get(src='pg', attr='v', idx=stg), + 0, places=6, + msg="Generator trip does not take effect!") + + self.ss.StaticGen.alter(src='u', idx=stg, value=1) # reset + + def test_trip_line(self): + """ + Test line tripping. + """ + self.ss.Line.set(src='u', attr='v', idx='Line_3', value=0) + + self.ss.DCOPF.update() + self.ss.DCOPF.run(solver='CLARABEL') + self.assertTrue(self.ss.DCOPF.converged, "DCOPF did not converge under line trip!") + self.assertAlmostEqual(self.ss.DCOPF.get(src='plf', attr='v', idx='Line_3'), + 0, places=6, + msg="Line trip does not take effect!") + + self.ss.Line.alter(src='u', idx='Line_3', value=1) # reset + + def test_set_load(self): + """ + Test setting and tripping load. """ # --- run DCOPF --- self.ss.DCOPF.run(solver='CLARABEL') @@ -44,27 +75,3 @@ def test_trip(self): self.ss.DCOPF.run(solver='CLARABEL') obj_pqt2 = self.ss.DCOPF.obj.v self.assertLess(obj_pqt2, obj_pqt, "Load trip does not take effect!") - - # --- trip generator --- - self.ss.StaticGen.set(src='u', attr='v', idx='PV_1', value=0) - self.ss.DCOPF.update() - - self.ss.DCOPF.run(solver='CLARABEL') - self.assertTrue(self.ss.DCOPF.converged, "DCOPF did not converge under generator trip!") - self.assertAlmostEqual(self.ss.DCOPF.get(src='pg', attr='v', idx='PV_1'), - 0, places=6, - msg="Generator trip does not take effect!") - self.assertAlmostEqual(self.ss.DCOPF.get(src='pg', attr='v', idx='PV_1'), - 0, places=6, - msg="Generator trip does not take effect!") - - # --- trip line --- - self.ss.Line.set(src='u', attr='v', idx='Line_3', value=0) - self.ss.DCOPF.update() - - self.ss.DCOPF.run(solver='CLARABEL') - self.assertTrue(self.ss.DCOPF.converged, "DCOPF did not converge under line trip!") - - self.assertAlmostEqual(self.ss.DCOPF.get(src='plf', attr='v', idx='Line_3'), - 0, places=6, - msg="Line trip does not take effect!") From d93f94d6a57982313264c3b3498376f89cb69ed0 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 15:46:45 -0400 Subject: [PATCH 079/181] Refactor TestRTED --- tests/test_rtn_rted.py | 58 ++++++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/tests/test_rtn_rted.py b/tests/test_rtn_rted.py index df9d6845..526c7af1 100644 --- a/tests/test_rtn_rted.py +++ b/tests/test_rtn_rted.py @@ -24,9 +24,40 @@ def test_init(self): self.ss.RTED.init() self.assertTrue(self.ss.RTED.initialized, "RTED initialization failed!") - def test_trip(self): + def test_trip_gen(self): """ - Test generator trip. + Test generator tripping. + """ + stg = 'PV_1' + self.ss.StaticGen.set(src='u', attr='v', idx=stg, value=0) + + self.ss.RTED.update() + self.ss.RTED.run(solver='CLARABEL') + self.assertTrue(self.ss.RTED.converged, "RTED did not converge under generator trip!") + self.assertAlmostEqual(self.ss.RTED.get(src='pg', attr='v', idx=stg), + 0, places=6, + msg="Generator trip does not take effect!") + + self.ss.StaticGen.alter(src='u', idx=stg, value=1) # reset + + def test_trip_line(self): + """ + Test line tripping. + """ + self.ss.Line.set(src='u', attr='v', idx='Line_3', value=0) + + self.ss.RTED.update() + self.ss.RTED.run(solver='CLARABEL') + self.assertTrue(self.ss.RTED.converged, "RTED did not converge under line trip!") + self.assertAlmostEqual(self.ss.RTED.get(src='plf', attr='v', idx='Line_3'), + 0, places=6, + msg="Line trip does not take effect!") + + self.ss.Line.alter(src='u', idx='Line_3', value=1) + + def test_set_load(self): + """ + Test setting and tripping load. """ self.ss.RTED.run(solver='CLARABEL') obj = self.ss.RTED.obj.v @@ -47,29 +78,6 @@ def test_trip(self): obj_pqt2 = self.ss.RTED.obj.v self.assertLess(obj_pqt2, obj_pqt, "Load trip does not take effect!") - # --- trip generator --- - self.ss.StaticGen.set(src='u', attr='v', idx='PV_1', value=0) - self.ss.RTED.update() - - self.ss.RTED.run(solver='CLARABEL') - self.assertTrue(self.ss.RTED.converged, "RTED did not converge under generator trip!") - self.assertAlmostEqual(self.ss.RTED.get(src='pg', attr='v', idx='PV_1'), - 0, places=6, - msg="Generator trip does not take effect!") - - pg_trip = self.ss.RTED.get(src='pg', attr='v', idx='PV_1') - np.testing.assert_almost_equal(pg_trip, 0, decimal=6) - - # --- trip line --- - self.ss.Line.set(src='u', attr='v', idx='Line_3', value=0) - self.ss.RTED.update() - - self.ss.RTED.run(solver='CLARABEL') - self.assertTrue(self.ss.RTED.converged, "RTED did not converge under line trip!") - self.assertAlmostEqual(self.ss.RTED.get(src='plf', attr='v', idx='Line_3'), - 0, places=6, - msg="Line trip does not take effect!") - def test_dc2ac(self): """ Test `RTED.init()` method. From 42c420198e9937589cee4d16fae67d5d7e886cb1 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 17:07:37 -0400 Subject: [PATCH 080/181] Add more common_params in module group --- ams/models/group.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ams/models/group.py b/ams/models/group.py index bbb41437..e762cf3a 100644 --- a/ams/models/group.py +++ b/ams/models/group.py @@ -199,8 +199,9 @@ class StaticGen(GroupBase): def __init__(self): super().__init__() - self.common_params.extend(('Sn', 'Vn', 'p0', 'q0', 'ra', 'xs', 'subidx', - 'bus', 'pmax', 'pmin', 'pg0', 'ctrl', 'R10')) + self.common_params.extend(('bus', 'Sn', 'Vn', 'p0', 'q0', 'ra', 'xs', 'subidx', + 'pmax', 'pmin', 'pg0', 'ctrl', 'R10', 'td1', 'td2', + 'zone')) self.common_vars.extend(('p', 'q')) @@ -223,7 +224,7 @@ class StaticLoad(GroupBase): def __init__(self): super().__init__() - self.common_params.extend(('p0', 'q0', 'zone')) + self.common_params.extend(('bus', 'p0', 'q0', 'ctrl', 'zone')) class StaticShunt(GroupBase): From 0191ed72db5c0c7ce04caa5cbf95e22ee973d112 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 17:11:56 -0400 Subject: [PATCH 081/181] Use alter instead of set in UC._initial_guess --- ams/routines/uc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ams/routines/uc.py b/ams/routines/uc.py index b2003127..7dc6622c 100644 --- a/ams/routines/uc.py +++ b/ams/routines/uc.py @@ -304,7 +304,7 @@ def _initial_guess(self): if len(g_idx) == 0: g_idx = priority[0] ug0 = 0 - self.system.StaticGen.set(src='u', attr='v', idx=g_idx, value=ug0) + self.system.StaticGen.alter(src='u', idx=g_idx, value=ug0) logger.warning(f'Turn off StaticGen {g_idx} as initial commitment guess.') return True From eb31eac2de5b6db7cb6d4a6f63a0093eb0177c10 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 21:33:06 -0400 Subject: [PATCH 082/181] [WIP] Refactor TestUC --- tests/test_rtn_uc.py | 95 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 tests/test_rtn_uc.py diff --git a/tests/test_rtn_uc.py b/tests/test_rtn_uc.py new file mode 100644 index 00000000..dcb90dab --- /dev/null +++ b/tests/test_rtn_uc.py @@ -0,0 +1,95 @@ +import unittest +import numpy as np + +import ams + + +class TestUC(unittest.TestCase): + """ + Test routine `UC`. + """ + + def setUp(self) -> None: + self.ss = ams.load(ams.get_case("5bus/pjm5bus_demo.xlsx"), + setup=True, default_config=True, no_output=True) + # decrease load first + self.ss.PQ.set(src='p0', attr='v', idx=['PQ_1', 'PQ_2'], value=[0.3, 0.3]) + + def test_init(self): + """ + Test initialization. + """ + self.ss.UC.init() + self.assertTrue(self.ss.ED.initialized, "UC initialization failed!") + + # def test_trip_gen(self): + # """ + # Test generator tripping. + # """ + # # a) ensure UCTSlot.ug takes effect + # # NOTE: manually chang ug.v for testing purpose + # stg = 'PV_1' + # stg_uid = self.ss.ED.pg.get_idx().index(stg) + # loc_offtime = np.array([0, 2, 4]) + # self.ss.EDTSlot.ug.v[loc_offtime, stg_uid] = 0 + + # self.ss.ED.run(solver='CLARABEL') + # self.assertTrue(self.ss.ED.converged, "ED did not converge under generator trip!") + # pg_pv1 = self.ss.ED.get(src='pg', attr='v', idx=stg) + # np.testing.assert_almost_equal(np.zeros_like(loc_offtime), + # pg_pv1[loc_offtime], + # decimal=6, + # err_msg="Generator trip does not take effect!") + + # self.ss.EDTSlot.ug.v[...] = 1 # reset + + # # b) ensure StaticGen.u does not take effect + # # NOTE: in ED, `EDTSlot.ug` is used instead of `StaticGen.u` + # self.ss.StaticGen.set(src='u', attr='v', idx=stg, value=0) + # self.ss.ED.update() + + # self.ss.ED.run(solver='CLARABEL') + # self.assertTrue(self.ss.ED.converged, "ED did not converge under generator trip!") + # pg_pv1 = self.ss.ED.get(src='pg', attr='v', idx=stg) + # np.testing.assert_array_less(np.zeros_like(pg_pv1), pg_pv1, + # err_msg="Generator trip take effect, which is unexpected!") + + # self.ss.StaticGen.alter(src='u', idx=stg, value=1) # reset + + # def test_trip_line(self): + # """ + # Test line tripping. + # """ + # self.ss.Line.set(src='u', attr='v', idx='Line_3', value=0) + # self.ss.ED.update() + + # self.ss.ED.run(solver='CLARABEL') + # self.assertTrue(self.ss.ED.converged, "ED did not converge under line trip!") + # plf_l3 = self.ss.ED.get(src='plf', attr='v', idx='Line_3') + # np.testing.assert_almost_equal(np.zeros_like(plf_l3), + # plf_l3, decimal=6) + + # self.ss.Line.alter(src='u', idx='Line_3', value=1) # reset + + # def test_set_load(self): + # """ + # Test setting and tripping load. + # """ + # self.ss.ED.run(solver='CLARABEL') + # obj = self.ss.ED.obj.v + + # # --- set load --- + # self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0.1) + # self.ss.ED.update() + + # self.ss.ED.run(solver='CLARABEL') + # obj_pqt = self.ss.ED.obj.v + # self.assertLess(obj_pqt, obj, "Load set does not take effect!") + + # # --- trip load --- + # self.ss.PQ.alter(src='u', idx='PQ_2', value=0) + # self.ss.ED.update() + + # self.ss.ED.run(solver='CLARABEL') + # obj_pqt2 = self.ss.ED.obj.v + # self.assertLess(obj_pqt2, obj_pqt, "Load trip does not take effect!") From 24f0152c2ed9786157d7f4831dc3299bb621ca8e Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 21:33:36 -0400 Subject: [PATCH 083/181] Switch from .set to .alter --- ams/interop/andes.py | 36 ++++++------- ams/routines/dcopf.py | 10 ++-- ams/routines/routine.py | 2 +- ams/routines/rted.py | 20 ++++---- ams/system.py | 8 +-- examples/demonstration/demo_AGC.ipynb | 74 +++++++++------------------ examples/ex2.ipynb | 20 ++++---- examples/ex5.ipynb | 6 +-- 8 files changed, 72 insertions(+), 104 deletions(-) diff --git a/ams/interop/andes.py b/ams/interop/andes.py index 33a59c57..7e297b20 100644 --- a/ams/interop/andes.py +++ b/ams/interop/andes.py @@ -317,7 +317,7 @@ def parse_addfile(adsys, amsys, addfile): syg_idx += syg.idx.v syg_bus_idx = adsys.SynGen.get(src='bus', attr='v', idx=syg_idx) syg_bus_vn = adsys.Bus.get(src='Vn', idx=syg_bus_idx) - adsys.SynGen.set(src='Vn', attr='v', idx=syg_idx, value=syg_bus_vn) + adsys.SynGen.alter(src='Vn', idx=syg_idx, value=syg_bus_vn) # --- for debugging --- adsys.df_in = df_models @@ -417,8 +417,7 @@ def _send_tgr(self, sa, sp): if syg_mask.any(): logger.debug('Governor is not complete for SynGen.') # --- pref --- - sa.TurbineGov.set(value=syg_ams, idx=gov_idx, - src='pref0', attr='v') + sa.TurbineGov.alter(value=syg_ams, idx=gov_idx, src='pref0') # --- paux --- # TODO: sync paux, using paux0 @@ -433,8 +432,7 @@ def _send_tgr(self, sa, sp): dg_ams = sp.recent.get(src='pg', attr='v', idx=stg_dg_idx, allow_none=True, default=0) # --- pref --- - sa.DG.set(value=dg_ams, idx=dg_idx, - src='pref0', attr='v') + sa.DG.alter(src='pref0', idx=dg_idx, value=dg_ams) # TODO: paux, using Pext0, this one should be do in other place rather than here # 3) RenGen @@ -488,9 +486,9 @@ def _send_dgu(self, sa, sp): msg += ' Otherwise, unexpected results might occur.' raise ValueError(msg) # FIXME: below code seems to be unnecessary - sa.SynGen.set(src='u', attr='v', idx=syg_idx, value=stg_u_ams) - sa.DG.set(src='u', attr='v', idx=dg_idx, value=dg_u_ams) - sa.RenGen.set(src='u', attr='v', idx=rg_idx, value=rg_u_ams) + sa.SynGen.alter(src='u', idx=syg_idx, value=stg_u_ams) + sa.DG.alter(src='u', idx=dg_idx, value=dg_u_ams) + sa.RenGen.alter(src='u', idx=rg_idx, value=rg_u_ams) return True def _sync_check(self, amsys, adsys): @@ -570,7 +568,7 @@ def send(self, adsys=None, routine=None): stg_idx = sp.StaticGen.get_idx() bus_stg = sp.StaticGen.get(src='bus', attr='v', idx=stg_idx) vBus = rtn.get(src='vBus', attr='v', idx=bus_stg) - sa.StaticGen.set(value=vBus, idx=stg_idx, src='v0', attr='v') + sa.StaticGen.alter(src='v0', idx=stg_idx, value=vBus) logger.info(f'*Send <{vname_ams}> to StaticGen.v0') # 1. gen online status; in TDS running, setting u is invalid @@ -602,7 +600,7 @@ def send(self, adsys=None, routine=None): if syg_mask.any(): logger.debug('Governor is not complete for SynGen.') # --- pref --- - sa.TurbineGov.set(value=syg_ams, idx=gov_idx, src='pref0', attr='v') + sa.TurbineGov.alter(src='pref0', idx=gov_idx, value=syg_ams) # --- DG: DG.pref0 --- dg_idx = sp.dyn.link['dg_idx'].dropna().tolist() # DG idx @@ -612,7 +610,7 @@ def send(self, adsys=None, routine=None): # corresponding StaticGen pg in AMS dg_ams = rtn.get(src='pg', attr='v', idx=stg_dg_idx) # --- pref --- - sa.DG.set(value=dg_ams, idx=dg_idx, src='pref0', attr='v') + sa.DG.alter(src='pref0', idx=dg_idx, value=dg_ams) # --- RenGen: seems unnecessary --- # TODO: which models/params are used to control output and auxillary power? @@ -627,7 +625,7 @@ def send(self, adsys=None, routine=None): # --- other scenarios --- if _dest_check(mname=mname_ads, pname=pname_ads, idx=idx_ads, adsys=sa): - mdl_ads.set(src=pname_ads, attr='v', idx=idx_ads, value=var_ams.v) + mdl_ads.alter(src=pname_ads, idx=idx_ads, value=var_ams.v) logger.warning(f'Send <{vname_ams}> to {mname_ads}.{pname_ads}') return True @@ -697,10 +695,8 @@ def receive(self, adsys=None, routine=None, no_update=False): idx=link['stg_idx'].values) # NOTE: only update u if changed actually u0_rtn = rtn.get(src=vname_ams, attr='v', idx=link['stg_idx'].values).copy() - rtn.set(src=vname_ams, attr='v', value=u_stg, - idx=link['stg_idx'].values) - rtn.set(src=vname_ams, attr='v', value=u_dyg, - idx=link['stg_idx'].values) + rtn.alter(src=vname_ams, idx=link['stg_idx'].values, value=u_stg) + rtn.alter(src=vname_ams, idx=link['stg_idx'].values, value=u_dyg) u_rtn = rtn.get(src=vname_ams, attr='v', idx=link['stg_idx'].values).copy() if not np.array_equal(u0_rtn, u_rtn): pname_to_update.append(vname_ams) @@ -737,10 +733,8 @@ def receive(self, adsys=None, routine=None, no_update=False): # Sync StaticGen.p first, then overwrite the ones with dynamic generator p_stg = sa.StaticGen.get(src='p', attr='v', idx=link['stg_idx'].values) - rtn.set(src=vname_ams, attr='v', value=p_stg, - idx=link['stg_idx'].values) - rtn.set(src=vname_ams, attr='v', value=p_dyg, - idx=link['stg_idx'].values) + rtn.alter(src=vname_ams, idx=link['stg_idx'].values, value=p_stg) + rtn.alter(src=vname_ams, idx=link['stg_idx'].values, value=p_dyg) pname_to_update.append(vname_ams) @@ -755,7 +749,7 @@ def receive(self, adsys=None, routine=None, no_update=False): # --- other scenarios --- if _dest_check(mname=mname_ads, pname=pname_ads, idx=idx_ads, adsys=sa): v_ads = mdl_ads.get(src=pname_ads, attr='v', idx=idx_ads) - rtn.set(src=vname_ams, attr='v', idx=idx_ads, value=v_ads) + rtn.alter(src=vname_ams, idx=idx_ads, value=v_ads) pname_to_update.append(vname_ams) logger.warning(f'Receive <{vname_ams}> from {mname_ads}.{pname_ads}') diff --git a/ams/routines/dcopf.py b/ams/routines/dcopf.py index 7e9ba1fd..4acc80ec 100644 --- a/ams/routines/dcopf.py +++ b/ams/routines/dcopf.py @@ -307,7 +307,7 @@ def unpack(self, **kwargs): pass # NOTE: only unpack the variables that are in the model or group try: - var.owner.set(src=var.src, attr='v', idx=idx, value=var.v) + var.owner.alter(src=var.src, idx=idx, value=var.v) # failed to find source var in the owner (model or group) except (KeyError, TypeError): pass @@ -335,16 +335,16 @@ def dc2ac(self, kloss=1.0, **kwargs): pq_idx = self.system.StaticLoad.get_idx() pd0 = self.system.StaticLoad.get(src='p0', attr='v', idx=pq_idx).copy() qd0 = self.system.StaticLoad.get(src='q0', attr='v', idx=pq_idx).copy() - self.system.StaticLoad.set(src='p0', attr='v', idx=pq_idx, value=pd0 * kloss) - self.system.StaticLoad.set(src='q0', attr='v', idx=pq_idx, value=qd0 * kloss) + self.system.StaticLoad.alter(src='p0', idx=pq_idx, value=pd0 * kloss) + self.system.StaticLoad.alter(src='q0', idx=pq_idx, value=qd0 * kloss) ACOPF = self.system.ACOPF # run ACOPF ACOPF.run() # self.exec_time += ACOPF.exec_time # scale load back - self.system.StaticLoad.set(src='p0', attr='v', idx=pq_idx, value=pd0) - self.system.StaticLoad.set(src='q0', attr='v', idx=pq_idx, value=qd0) + self.system.StaticLoad.alter(src='p0', idx=pq_idx, value=pd0) + self.system.StaticLoad.alter(src='q0', idx=pq_idx, value=qd0) if not ACOPF.exit_code == 0: logger.warning(' did not converge, conversion failed.') # NOTE: mock results to fit interface with ANDES diff --git a/ams/routines/routine.py b/ams/routines/routine.py index e9fb2eff..a55f6e48 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -173,7 +173,7 @@ def set(self, src: str, idx, attr: str = "v", value=0.0): owner = self.__dict__[src].owner src0 = self.__dict__[src].src try: - res = owner.set(src=src0, idx=idx, attr=attr, value=value) + res = owner.alter(src=src0, idx=idx, value=value) return res except KeyError as e: msg = f"Failed to set <{src0}> in <{owner.class_name}>. " diff --git a/ams/routines/rted.py b/ams/routines/rted.py index 0b156388..096d8c06 100644 --- a/ams/routines/rted.py +++ b/ams/routines/rted.py @@ -199,20 +199,20 @@ def dc2ac(self, kloss=1.0, **kwargs): pq_idx = self.system.StaticLoad.get_idx() pd0 = self.system.StaticLoad.get(src='p0', attr='v', idx=pq_idx).copy() qd0 = self.system.StaticLoad.get(src='q0', attr='v', idx=pq_idx).copy() - self.system.StaticLoad.set(src='p0', attr='v', idx=pq_idx, value=pd0 * kloss) - self.system.StaticLoad.set(src='q0', attr='v', idx=pq_idx, value=qd0 * kloss) + self.system.StaticLoad.alter(src='p0', idx=pq_idx, value=pd0 * kloss) + self.system.StaticLoad.alter(src='q0', idx=pq_idx, value=qd0 * kloss) # preserve generator reserve 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) + self.system.StaticGen.alter(src='pmin', idx=pr_idx, value=pmin) + self.system.StaticGen.alter(src='pmax', idx=pr_idx, value=pmax) + self.system.StaticGen.alter(src='p0', idx=pr_idx, value=self.pg.v) # run ACOPF ACOPF.run() # scale load back - self.system.StaticLoad.set(src='p0', attr='v', idx=pq_idx, value=pd0) - self.system.StaticLoad.set(src='q0', attr='v', idx=pq_idx, value=qd0) + self.system.StaticLoad.alter(src='p0', idx=pq_idx, value=pd0) + self.system.StaticLoad.alter(src='q0', idx=pq_idx, value=qd0) if not ACOPF.exit_code == 0: logger.warning(' did not converge, conversion failed.') # NOTE: mock results to fit interface with ANDES @@ -232,9 +232,9 @@ def dc2ac(self, kloss=1.0, **kwargs): self.exec_time = exec_time # 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.StaticGen.alter(src='pmin', idx=pr_idx, value=pmin0) + self.system.StaticGen.alter(src='pmax', idx=pr_idx, value=pmax0) + self.system.StaticGen.alter(src='p0', idx=pr_idx, value=p00) # --- set status --- self.system.recent = self diff --git a/ams/system.py b/ams/system.py index ba243b9c..f0601d46 100644 --- a/ams/system.py +++ b/ams/system.py @@ -436,10 +436,10 @@ def setup(self): # assign bus type as placeholder; 1=PQ, 2=PV, 3=ref, 4=isolated if self.Bus.type.v.sum() == self.Bus.n: # if all type are PQ - self.Bus.set(src='type', attr='v', idx=self.PV.bus.v, - value=np.ones(self.PV.n)) - self.Bus.set(src='type', attr='v', idx=self.Slack.bus.v, - value=np.ones(self.Slack.n)) + self.Bus.alter(src='type', idx=self.PV.bus.v, + value=np.ones(self.PV.n)) + self.Bus.alter(src='type', idx=self.Slack.bus.v, + value=np.ones(self.Slack.n)) # --- assign column and row names --- self.mats.Cft.col_names = self.Line.idx.v diff --git a/examples/demonstration/demo_AGC.ipynb b/examples/demonstration/demo_AGC.ipynb index e7952732..08eeffea 100644 --- a/examples/demonstration/demo_AGC.ipynb +++ b/examples/demonstration/demo_AGC.ipynb @@ -410,7 +410,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -450,8 +450,8 @@ " columns=out_cols)\n", "\n", "# --- AMS settings ---\n", - "sp.SFR.set(src='du', attr='v', idx=sp.SFR.idx.v, value=0.0015*np.ones(sp.SFR.n))\n", - "sp.SFR.set(src='dd', attr='v', idx=sp.SFR.idx.v, value=0.0015*np.ones(sp.SFR.n))\n", + "sp.SFR.alter(src='du', idx=sp.SFR.idx.v, value=0.0015*np.ones(sp.SFR.n))\n", + "sp.SFR.alter(src='dd', idx=sp.SFR.idx.v, value=0.0015*np.ones(sp.SFR.n))\n", "\n", "# --- ANDES settings ---\n", "sa.TDS.config.no_tqdm = True # turn off ANDES progress bar\n", @@ -464,14 +464,14 @@ "# NOTE: might run into error if there exists a TurbineGov model that does not have \"VMAX\"\n", "tbgov_src = [mdl.idx.v for mdl in sa.TurbineGov.models.values()]\n", "tbgov_idx = list(chain.from_iterable(tbgov_src))\n", - "sa.TurbineGov.set(src='VMAX', attr='v', idx=tbgov_idx,\n", - " value=9999 * np.ones(sa.TurbineGov.n),)\n", - "sa.TurbineGov.set(src='VMIN', attr='v', idx=tbgov_idx,\n", - " value=np.zeros(sa.TurbineGov.n),)\n", + "sa.TurbineGov.alter(src='VMAX', idx=tbgov_idx,\n", + " value=9999 * np.ones(sa.TurbineGov.n),)\n", + "sa.TurbineGov.alter(src='VMIN', idx=tbgov_idx,\n", + " value=np.zeros(sa.TurbineGov.n),)\n", "syg_src = [mdl.idx.v for mdl in sa.SynGen.models.values()]\n", "syg_idx = list(chain.from_iterable(syg_src))\n", - "sa.SynGen.set(src='ra', attr='v', idx=syg_idx,\n", - " value=np.zeros(sa.SynGen.n),)\n", + "sa.SynGen.alter(src='ra', idx=syg_idx,\n", + " value=np.zeros(sa.SynGen.n),)\n", "\n", "# use constant power model for PQ\n", "sa.PQ.config.p2p = 1\n", @@ -538,7 +538,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -664,12 +664,8 @@ " # use 5-min average load in dispatch solution\n", " load_avg = load_coeff[t:t+RTED_interval].mean()\n", " # set load in to AMS\n", - " sp.PQ.set(src='p0', attr='v',\n", - " value=load_avg * p0_sp,\n", - " idx=pq_idx)\n", - " sp.PQ.set(src='q0', attr='v',\n", - " value=load_avg * q0_sp,\n", - " idx=pq_idx)\n", + " sp.PQ.alter(src='p0', idx=pq_idx, value=load_avg * p0_sp)\n", + " sp.PQ.alter(src='q0', idx=pq_idx, value=load_avg * q0_sp)\n", " print(f\"--AMS: update disaptch load with factor {load_avg:.6f}.\")\n", "\n", " # get dynamic generator output from TDS\n", @@ -707,23 +703,17 @@ "\n", " # set into governor, Exclude NaN values for governor index\n", " gov_to_set = {gov: pgov for gov, pgov in zip(maptab['gov_idx'], maptab['pgov']) if bool(gov)}\n", - " sa.TurbineGov.set(src='pref0', attr='v',\n", - " idx=list(gov_to_set.keys()),\n", - " value=list(gov_to_set.values()))\n", + " sa.TurbineGov.alter(src='pref0', idx=list(gov_to_set.keys()), value=list(gov_to_set.values()))\n", " print(f\"--ANDES: update TurbineGov reference.\")\n", "\n", " # set into dg, Exclude NaN values for dg index\n", " dg_to_set = {dg: pdg for dg, pdg in zip(maptab['dg_idx'], maptab['pdg']) if bool(dg)}\n", - " sa.DG.set(src='pref0', attr='v',\n", - " idx=list(dg_to_set.keys()),\n", - " value=list(dg_to_set.values()))\n", + " sa.DG.alter(src='pref0', idx=list(dg_to_set.keys()), value=list(dg_to_set.values()))\n", " print(f\"--ANDES: update DG reference.\")\n", "\n", " # set into rg, Exclude NaN values for rg index\n", " rg_to_set = {rg: prg for rg, prg in zip(maptab['rg_idx'], maptab['prg']) if bool(rg)}\n", - " sa.RenGen.set(src='Pref', attr='v',\n", - " idx=list(rg_to_set.keys()),\n", - " value=list(rg_to_set.values()))\n", + " sa.RenGen.alter(src='Pref', idx=list(rg_to_set.keys()), value=list(rg_to_set.values()))\n", " print(f\"--ANDES: update RenGen reference.\")\n", "\n", " # record dispatch data\n", @@ -755,30 +745,22 @@ "\n", " # set into governor, Exclude NaN values for governor index\n", " agov_to_set = {gov: agov for gov, agov in zip(maptab['gov_idx'], maptab['agov']) if bool(gov)}\n", - " sa.TurbineGov.set(src='paux0', attr='v',\n", - " idx=list(agov_to_set.keys()),\n", - " value=list(agov_to_set.values()))\n", + " sa.TurbineGov.alter(src='paux0', idx=list(agov_to_set.keys()), value=list(agov_to_set.values()))\n", "\n", " # set into dg, Exclude NaN values for dg index\n", " adg_to_set = {dg: adg for dg, adg in zip(maptab['dg_idx'], maptab['adg']) if bool(dg)}\n", - " sa.DG.set(src='Pext0', attr='v',\n", - " idx=list(adg_to_set.keys()),\n", - " value=list(adg_to_set.values()))\n", + " sa.DG.alter(src='Pext0', idx=list(adg_to_set.keys()), value=list(adg_to_set.values()))\n", "\n", " # set into rg, Exclude NaN values for rg index\n", " arg_to_set = {rg: arg + prg for rg, arg,\n", " prg in zip(maptab['rg_idx'], maptab['arg'], maptab['prg']) if bool(rg)}\n", - " sa.RenGen.set(src='Pref', attr='v',\n", - " idx=list(arg_to_set.keys()),\n", - " value=list(arg_to_set.values()))\n", + " sa.RenGen.alter(src='Pref', idx=list(arg_to_set.keys()), value=list(arg_to_set.values()))\n", "\n", " # --- TDS interval ---\n", " if t > 0: # --- run TDS ---\n", " # set laod into PQ.Ppf and PQ.Qpf\n", - " sa.PQ.set(src='Ppf', attr='v', idx=pq_idx,\n", - " value=load_coeff[t] * p0_sa)\n", - " sa.PQ.set(src='Qpf', attr='v', idx=pq_idx,\n", - " value=load_coeff[t] * q0_sa)\n", + " sa.PQ.alter(src='Ppf', idx=pq_idx, value=load_coeff[t] * p0_sa)\n", + " sa.PQ.alter(src='Qpf', idx=pq_idx, value=load_coeff[t] * q0_sa)\n", " sa.TDS.config.tf = t\n", " sa.TDS.run()\n", " # Update AGC PI controller\n", @@ -803,22 +785,16 @@ " break\n", " else: # --- init TDS ---\n", " # set pg to StaticGen.p0\n", - " sa.StaticGen.set(src='p0', attr='v', value=sp.RTED.pg.v,\n", - " idx=sp.RTED.pg.get_idx())\n", + " sa.StaticGen.alter(src='p0', idx=sp.RTED.pg.get_idx(), value=sp.RTED.pg.v)\n", " # set Bus.v to StaticGen.v\n", " bus_stg = sp.StaticGen.get(src='bus', attr='v', idx=sp.StaticGen.get_idx())\n", " v_stg = sp.Bus.get(src='v', attr='v', idx=bus_stg)\n", - " sa.StaticGen.set(src='v0', attr='v', value=v_stg,\n", - " idx=sp.StaticGen.get_idx())\n", + " sa.StaticGen.alter(src='v0', idx=sp.StaticGen.get_idx(), value=v_stg)\n", " # set vBus to Bus\n", - " sa.Bus.set(src='v0', attr='v',\n", - " value=sp.RTED.vBus.v,\n", - " idx=sp.RTED.vBus.get_idx())\n", + " sa.Bus.alter(src='v0', idx=sp.RTED.vBus.get_idx(), value=sp.RTED.vBus.v)\n", " # set load into PQ.p0 and PQ.q0\n", - " sa.PQ.set(src='p0', attr='v', idx=pq_idx,\n", - " value=load_coeff[t] * p0_sa)\n", - " sa.PQ.set(src='q0', attr='v', idx=pq_idx,\n", - " value=load_coeff[t] * q0_sa)\n", + " sa.PQ.alter(src='p0', idx=pq_idx, value=load_coeff[t] * p0_sa)\n", + " sa.PQ.alter(src='q0', idx=pq_idx, value=load_coeff[t] * q0_sa)\n", " sa.PFlow.run() # run power flow\n", " sa.TDS.init() # initialize TDS\n", "\n", diff --git a/examples/ex2.ipynb b/examples/ex2.ipynb index 8a19c83d..a40ec62b 100644 --- a/examples/ex2.ipynb +++ b/examples/ex2.ipynb @@ -353,14 +353,12 @@ "\n", "`set`: This method ***WILL NOT*** modify the input values from the case file that have not been converted to the system base. As a result, changes applied by this method ***WILL NOT*** affect the dumped case file.\n", "\n", - "`alter`: If the method operates on an input parameter, the new data ***should be in the same base as that in the input file***. This function will convert ``value`` to per unit in the system base whenever necessary. The values for storing the input data, i.e., the parameter's ``vin`` field, will be overwritten. As a result, altered values ***WILL BE*** reflected in the dumped case file.\n", - "\n", - "Besides, `Group` also has method `set` but has no `alter`." + "`alter`: If the method operates on an input parameter, the new data ***should be in the same base as that in the input file***. This function will convert ``value`` to per unit in the system base whenever necessary. The values for storing the input data, i.e., the parameter's ``vin`` field, will be overwritten. As a result, altered values ***WILL BE*** reflected in the dumped case file." ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -375,7 +373,7 @@ } ], "source": [ - "sp.PQ.set(src='p0', attr='v', idx=['PQ_1', 'PQ_2'], value=[3.2, 3.2])" + "sp.PQ.alter(src='p0', idx=['PQ_1', 'PQ_2'], value=[3.2, 3.2])" ] }, { @@ -944,7 +942,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -959,7 +957,7 @@ } ], "source": [ - "sp.StaticGen.set(src='u', attr='v', idx='PV_1', value=0)" + "sp.StaticGen.alter(src='u', idx='PV_1', value=0)" ] }, { @@ -1377,7 +1375,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -1392,7 +1390,7 @@ } ], "source": [ - "sp.Line.set(src='u', attr='v', idx='Line_1', value=0)" + "sp.Line.alter(src='u', idx='Line_1', value=0)" ] }, { @@ -1543,7 +1541,7 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -1558,7 +1556,7 @@ } ], "source": [ - "spc.RTED.set(src='rate_a', attr='v', idx=['Line_3'], value=0.6)" + "spc.RTED.alter(src='rate_a', idx=['Line_3'], value=0.6)" ] }, { diff --git a/examples/ex5.ipynb b/examples/ex5.ipynb index b1ed6cf0..c55792f4 100644 --- a/examples/ex5.ipynb +++ b/examples/ex5.ipynb @@ -511,7 +511,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -526,8 +526,8 @@ } ], "source": [ - "sa.TGOV1.set(src='VMAX', attr='v', idx=sa.TGOV1.idx.v, value=100*np.ones(sa.TGOV1.n))\n", - "sa.TGOV1.set(src='VMIN', attr='v', idx=sa.TGOV1.idx.v, value=np.zeros(sa.TGOV1.n))" + "sa.TGOV1.alter(src='VMAX', idx=sa.TGOV1.idx.v, value=100*np.ones(sa.TGOV1.n))\n", + "sa.TGOV1.alter(src='VMIN', idx=sa.TGOV1.idx.v, value=np.zeros(sa.TGOV1.n))" ] }, { From 0cc00b0b49f1ee72ccb6e801e58c6a1e81a11951 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 22:55:14 -0400 Subject: [PATCH 084/181] Revise UC._initial_guess() return as indexes of off_gen --- ams/routines/uc.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ams/routines/uc.py b/ams/routines/uc.py index 7dc6622c..98b083e3 100644 --- a/ams/routines/uc.py +++ b/ams/routines/uc.py @@ -304,9 +304,12 @@ def _initial_guess(self): if len(g_idx) == 0: g_idx = priority[0] ug0 = 0 + off_gen = f'{g_idx}' + else: + off_gen = ', '.join(g_idx) self.system.StaticGen.alter(src='u', idx=g_idx, value=ug0) - logger.warning(f'Turn off StaticGen {g_idx} as initial commitment guess.') - return True + logger.warning(f"As initial commitment guess, turn off StaticGen: {off_gen}") + return g_idx def init(self, no_code=True, force_mats=False, force_constr=False, From ae8ddaf4beb454e7f60ba1abc23cfc0ef43a9f17 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 22:55:47 -0400 Subject: [PATCH 085/181] [WIP] Refactor test_dctypes, refactor test_rtn_uc --- ams/shared.py | 14 +++ tests/test_dctypes.py | 16 ---- tests/test_rtn_uc.py | 216 ++++++++++++++++++++++++++++-------------- 3 files changed, 157 insertions(+), 89 deletions(-) diff --git a/ams/shared.py b/ams/shared.py index a40bc5d6..9ab0fa9a 100644 --- a/ams/shared.py +++ b/ams/shared.py @@ -4,6 +4,7 @@ This module is supplementary to the ``andes.shared`` module. """ import logging +import unittest from functools import wraps from datetime import datetime from collections import OrderedDict @@ -50,6 +51,19 @@ def wrapper(*args, **kwargs): return wrapper +def skip_unittest_without_MIP(f): + """ + Decorator for skipping tests that require MIP solver. + """ + def wrapper(*args, **kwargs): + if any(s in mip_solvers for s in installed_solvers): + pass + else: + raise unittest.SkipTest("No MIP solver is available.") + return f(*args, **kwargs) + return wrapper + + ppc_cols = OrderedDict([ ('bus', ['bus_i', 'type', 'pd', 'qd', 'gs', 'bs', 'area', 'vm', 'va', 'baseKV', 'zone', 'vmax', 'vmin', 'lam_p', 'lam_q', diff --git a/tests/test_dctypes.py b/tests/test_dctypes.py index 66250e27..1e08cb94 100644 --- a/tests/test_dctypes.py +++ b/tests/test_dctypes.py @@ -2,22 +2,6 @@ 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 = ['SCIP', 'CPLEX', 'GUROBI', 'MOSEK'] - 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 TestDCED(unittest.TestCase): diff --git a/tests/test_rtn_uc.py b/tests/test_rtn_uc.py index dcb90dab..30bd526d 100644 --- a/tests/test_rtn_uc.py +++ b/tests/test_rtn_uc.py @@ -2,6 +2,7 @@ import numpy as np import ams +from ams.shared import skip_unittest_without_MIP class TestUC(unittest.TestCase): @@ -14,82 +15,151 @@ def setUp(self) -> None: setup=True, default_config=True, no_output=True) # decrease load first self.ss.PQ.set(src='p0', attr='v', idx=['PQ_1', 'PQ_2'], value=[0.3, 0.3]) + # run UC._initial_guess() + self.off_gen = self.ss.UC._initial_guess() + + def test_initial_guess(self): + """ + Test initial guess. + """ + u_off_gen = self.ss.StaticGen.get(src='u', idx=self.off_gen) + np.testing.assert_equal(u_off_gen, np.zeros_like(u_off_gen), + err_msg="UC._initial_guess() failed!") def test_init(self): """ Test initialization. """ self.ss.UC.init() - self.assertTrue(self.ss.ED.initialized, "UC initialization failed!") - - # def test_trip_gen(self): - # """ - # Test generator tripping. - # """ - # # a) ensure UCTSlot.ug takes effect - # # NOTE: manually chang ug.v for testing purpose - # stg = 'PV_1' - # stg_uid = self.ss.ED.pg.get_idx().index(stg) - # loc_offtime = np.array([0, 2, 4]) - # self.ss.EDTSlot.ug.v[loc_offtime, stg_uid] = 0 - - # self.ss.ED.run(solver='CLARABEL') - # self.assertTrue(self.ss.ED.converged, "ED did not converge under generator trip!") - # pg_pv1 = self.ss.ED.get(src='pg', attr='v', idx=stg) - # np.testing.assert_almost_equal(np.zeros_like(loc_offtime), - # pg_pv1[loc_offtime], - # decimal=6, - # err_msg="Generator trip does not take effect!") - - # self.ss.EDTSlot.ug.v[...] = 1 # reset - - # # b) ensure StaticGen.u does not take effect - # # NOTE: in ED, `EDTSlot.ug` is used instead of `StaticGen.u` - # self.ss.StaticGen.set(src='u', attr='v', idx=stg, value=0) - # self.ss.ED.update() - - # self.ss.ED.run(solver='CLARABEL') - # self.assertTrue(self.ss.ED.converged, "ED did not converge under generator trip!") - # pg_pv1 = self.ss.ED.get(src='pg', attr='v', idx=stg) - # np.testing.assert_array_less(np.zeros_like(pg_pv1), pg_pv1, - # err_msg="Generator trip take effect, which is unexpected!") - - # self.ss.StaticGen.alter(src='u', idx=stg, value=1) # reset - - # def test_trip_line(self): - # """ - # Test line tripping. - # """ - # self.ss.Line.set(src='u', attr='v', idx='Line_3', value=0) - # self.ss.ED.update() - - # self.ss.ED.run(solver='CLARABEL') - # self.assertTrue(self.ss.ED.converged, "ED did not converge under line trip!") - # plf_l3 = self.ss.ED.get(src='plf', attr='v', idx='Line_3') - # np.testing.assert_almost_equal(np.zeros_like(plf_l3), - # plf_l3, decimal=6) - - # self.ss.Line.alter(src='u', idx='Line_3', value=1) # reset - - # def test_set_load(self): - # """ - # Test setting and tripping load. - # """ - # self.ss.ED.run(solver='CLARABEL') - # obj = self.ss.ED.obj.v - - # # --- set load --- - # self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0.1) - # self.ss.ED.update() - - # self.ss.ED.run(solver='CLARABEL') - # obj_pqt = self.ss.ED.obj.v - # self.assertLess(obj_pqt, obj, "Load set does not take effect!") - - # # --- trip load --- - # self.ss.PQ.alter(src='u', idx='PQ_2', value=0) - # self.ss.ED.update() - - # self.ss.ED.run(solver='CLARABEL') - # obj_pqt2 = self.ss.ED.obj.v - # self.assertLess(obj_pqt2, obj_pqt, "Load trip does not take effect!") + self.assertTrue(self.ss.UC.initialized, "UC initialization failed!") + + @skip_unittest_without_MIP + def test_trip_gen(self): + """ + Test generator tripping. + """ + self.ss.UC.run(solver='SCIP') + self.assertTrue(self.ss.UC.converged, "UC did not converge!") + pg_off_gen = self.ss.UC.get(src='pg', attr='v', idx=self.off_gen) + np.testing.assert_almost_equal(np.zeros_like(pg_off_gen), + pg_off_gen, decimal=6, + err_msg="Off generators are not turned off!") + + @skip_unittest_without_MIP + def test_set_load(self): + """ + Test setting and tripping load. + """ + self.ss.UC.run(solver='SCIP') + obj = self.ss.UC.obj.v + + # --- set load --- + self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0.1) + self.ss.UC.update() + + self.ss.UC.run(solver='SCIP') + obj_pqt = self.ss.UC.obj.v + self.assertLess(obj_pqt, obj, "Load set does not take effect!") + + # --- trip load --- + self.ss.PQ.alter(src='u', idx='PQ_2', value=0) + self.ss.UC.update() + + self.ss.UC.run(solver='SCIP') + obj_pqt2 = self.ss.UC.obj.v + self.assertLess(obj_pqt2, obj_pqt, "Load trip does not take effect!") + + @skip_unittest_without_MIP + def test_trip_line(self): + """ + Test line tripping. + """ + self.ss.Line.set(src='u', attr='v', idx='Line_3', value=0) + self.ss.UC.update() + + self.ss.UC.run(solver='SCIP') + self.assertTrue(self.ss.UC.converged, "UC did not converge under line trip!") + plf_l3 = self.ss.UC.get(src='plf', attr='v', idx='Line_3') + np.testing.assert_almost_equal(np.zeros_like(plf_l3), + plf_l3, decimal=6) + + self.ss.Line.alter(src='u', idx='Line_3', value=1) + + +class TestUCES(unittest.TestCase): + """ + Test routine `UCES`. + """ + + def setUp(self) -> None: + self.ss = ams.load(ams.get_case("5bus/pjm5bus_uced_esd1.xlsx"), + setup=True, default_config=True, no_output=True) + # run `_initial_guess()` + self.off_gen = self.ss.UCES._initial_guess() + + def test_initial_guess(self): + """ + Test initial guess. + """ + u_off_gen = self.ss.StaticGen.get(src='u', idx=self.off_gen) + np.testing.assert_equal(u_off_gen, np.zeros_like(u_off_gen), + err_msg="UCES._initial_guess() failed!") + + def test_init(self): + """ + Test initialization. + """ + self.ss.UCES.init() + self.assertTrue(self.ss.UCES.initialized, "UCES initialization failed!") + + @skip_unittest_without_MIP + def test_trip_gen(self): + """ + Test generator tripping. + """ + self.ss.UCES.run(solver='SCIP') + self.assertTrue(self.ss.UCES.converged, "UCES did not converge!") + pg_off_gen = self.ss.UCES.get(src='pg', attr='v', idx=self.off_gen) + np.testing.assert_almost_equal(np.zeros_like(pg_off_gen), + pg_off_gen, decimal=6, + err_msg="Off generators are not turned off!") + + @skip_unittest_without_MIP + def test_set_load(self): + """ + Test setting and tripping load. + """ + self.ss.UCES.run(solver='SCIP') + obj = self.ss.UCES.obj.v + + # --- set load --- + self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0.1) + self.ss.UCES.update() + + self.ss.UCES.run(solver='SCIP') + obj_pqt = self.ss.UCES.obj.v + self.assertLess(obj_pqt, obj, "Load set does not take effect!") + + # --- trip load --- + self.ss.PQ.alter(src='u', idx='PQ_2', value=0) + self.ss.UCES.update() + + self.ss.UCES.run(solver='SCIP') + obj_pqt2 = self.ss.UCES.obj.v + self.assertLess(obj_pqt2, obj_pqt, "Load trip does not take effect!") + + @skip_unittest_without_MIP + def test_trip_line(self): + """ + Test line tripping. + """ + self.ss.Line.set(src='u', attr='v', idx=3, value=0) + self.ss.UCES.update() + + self.ss.UCES.run(solver='SCIP') + self.assertTrue(self.ss.UCES.converged, "UCES did not converge under line trip!") + plf_l3 = self.ss.UCES.get(src='plf', attr='v', idx=3) + np.testing.assert_almost_equal(np.zeros_like(plf_l3), + plf_l3, decimal=6) + + self.ss.Line.alter(src='u', idx=3, value=1) From 7b86d9ac13eb1c8aabf624682c0cf6d170b5e1dc Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 23:21:55 -0400 Subject: [PATCH 086/181] [WIP] Refactor test_dctypes, refactor test_rtn_rted --- ams/cases/5bus/pjm5bus_uced_esd1.xlsx | Bin 28070 -> 28064 bytes tests/test_rtn_rted.py | 75 ++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/ams/cases/5bus/pjm5bus_uced_esd1.xlsx b/ams/cases/5bus/pjm5bus_uced_esd1.xlsx index d2ec3f813faf06f9d068ab50825f9b1730ee2a50..18b90430aff6ee55c5c380e1815802ce96ad5ca9 100644 GIT binary patch delta 3101 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 delta 3092 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 SxBZPL&D|j|2t>-~uk{~m|C;{* diff --git a/tests/test_rtn_rted.py b/tests/test_rtn_rted.py index 526c7af1..33fa1b59 100644 --- a/tests/test_rtn_rted.py +++ b/tests/test_rtn_rted.py @@ -2,6 +2,7 @@ import numpy as np import ams +from ams.shared import skip_unittest_without_MIP class TestRTED(unittest.TestCase): @@ -99,3 +100,77 @@ def test_dc2ac(self): a_rted = self.ss.RTED.get(src='aBus', attr='v', idx=bus_idx) a_acopf = self.ss.ACOPF.get(src='aBus', attr='v', idx=bus_idx) np.testing.assert_almost_equal(a_rted, a_acopf, decimal=3) + + +class TestRTEDES(unittest.TestCase): + """ + Test routine `RTEDES`. + """ + + def setUp(self) -> None: + self.ss = ams.load(ams.get_case("5bus/pjm5bus_uced_esd1.xlsx"), + setup=True, default_config=True, no_output=True) + + def test_init(self): + """ + Test initialization. + """ + self.ss.RTEDES.init() + self.assertTrue(self.ss.RTEDES.initialized, "RTEDES initialization failed!") + + @skip_unittest_without_MIP + def test_trip_gen(self): + """ + Test generator tripping. + """ + stg = 'PV_1' + self.ss.StaticGen.set(src='u', attr='v', idx=stg, value=0) + + self.ss.RTEDES.update() + self.ss.RTEDES.run(solver='SCIP') + self.assertTrue(self.ss.RTEDES.converged, "RTEDES did not converge under generator trip!") + self.assertAlmostEqual(self.ss.RTEDES.get(src='pg', attr='v', idx=stg), + 0, places=6, + msg="Generator trip does not take effect!") + + self.ss.StaticGen.alter(src='u', idx=stg, value=1) + + @skip_unittest_without_MIP + def test_trip_line(self): + """ + Test line tripping. + """ + self.ss.Line.set(src='u', attr='v', idx=3, value=0) + + self.ss.RTEDES.update() + self.ss.RTEDES.run(solver='SCIP') + self.assertTrue(self.ss.RTEDES.converged, "RTEDES did not converge under line trip!") + self.assertAlmostEqual(self.ss.RTEDES.get(src='plf', attr='v', idx=3), + 0, places=6, + msg="Line trip does not take effect!") + + self.ss.Line.alter(src='u', idx=3, value=1) + + @skip_unittest_without_MIP + def test_set_load(self): + """ + Test setting and tripping load. + """ + self.ss.RTEDES.run(solver='SCIP') + pgs = self.ss.RTEDES.obj.v.sum() + + # --- set load --- + self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=1) + self.ss.RTEDES.update() + + self.ss.RTEDES.run(solver='SCIP') + pgs_pqt = self.ss.RTEDES.obj.v.sum() + self.assertLess(pgs_pqt, pgs, "Load set does not take effect!") + + # --- trip load --- + self.ss.PQ.set(src='u', attr='v', idx='PQ_2', value=0) + self.ss.RTEDES.update() + + self.ss.RTEDES.run(solver='SCIP') + pgs_pqt2 = self.ss.RTEDES.obj.v.sum() + self.assertLess(pgs_pqt2, pgs_pqt, "Load trip does not take effect!") From 9700ef7e5a9b0b6761b1c6814b326d17034eefb0 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 23:27:11 -0400 Subject: [PATCH 087/181] [WIP] Refactor test_dctypes, refactor test_rtn_dcopf --- tests/test_rtn_dcopf.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_rtn_dcopf.py b/tests/test_rtn_dcopf.py index 5fc8f8b0..0d80127f 100644 --- a/tests/test_rtn_dcopf.py +++ b/tests/test_rtn_dcopf.py @@ -58,20 +58,20 @@ def test_set_load(self): """ # --- run DCOPF --- self.ss.DCOPF.run(solver='CLARABEL') - obj = self.ss.DCOPF.obj.v + pgs = self.ss.DCOPF.pg.v.sum() # --- set load --- self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0.1) self.ss.DCOPF.update() self.ss.DCOPF.run(solver='CLARABEL') - obj_pqt = self.ss.DCOPF.obj.v - self.assertLess(obj_pqt, obj, "Load set does not take effect!") + pgs_pqt = self.ss.DCOPF.pg.v.sum() + self.assertLess(pgs_pqt, pgs, "Load set does not take effect!") # --- trip load --- self.ss.PQ.set(src='u', attr='v', idx='PQ_2', value=0) self.ss.DCOPF.update() self.ss.DCOPF.run(solver='CLARABEL') - obj_pqt2 = self.ss.DCOPF.obj.v - self.assertLess(obj_pqt2, obj_pqt, "Load trip does not take effect!") + pgs_pqt2 = self.ss.DCOPF.pg.v.sum() + self.assertLess(pgs_pqt2, pgs_pqt, "Load trip does not take effect!") From 9f2c56d180142b7323b76b4675d66c84dd133ee4 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 23:27:31 -0400 Subject: [PATCH 088/181] [WIP] Refactor test_dctypes, refactor test_set_load in test_ed and test_uc --- tests/test_rtn_ed.py | 10 +++++----- tests/test_rtn_uc.py | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/test_rtn_ed.py b/tests/test_rtn_ed.py index 448c8777..f98fd05e 100644 --- a/tests/test_rtn_ed.py +++ b/tests/test_rtn_ed.py @@ -76,20 +76,20 @@ def test_set_load(self): Test setting and tripping load. """ self.ss.ED.run(solver='CLARABEL') - obj = self.ss.ED.obj.v + pgs = self.ss.ED.pg.v.sum() # --- set load --- self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0.1) self.ss.ED.update() self.ss.ED.run(solver='CLARABEL') - obj_pqt = self.ss.ED.obj.v - self.assertLess(obj_pqt, obj, "Load set does not take effect!") + pgs_pqt = self.ss.ED.pg.v.sum() + self.assertLess(pgs_pqt, pgs, "Load set does not take effect!") # --- trip load --- self.ss.PQ.alter(src='u', idx='PQ_2', value=0) self.ss.ED.update() self.ss.ED.run(solver='CLARABEL') - obj_pqt2 = self.ss.ED.obj.v - self.assertLess(obj_pqt2, obj_pqt, "Load trip does not take effect!") + pgs_pqt2 = self.ss.ED.pg.v.sum() + self.assertLess(pgs_pqt2, pgs_pqt, "Load trip does not take effect!") diff --git a/tests/test_rtn_uc.py b/tests/test_rtn_uc.py index 30bd526d..df15a6b2 100644 --- a/tests/test_rtn_uc.py +++ b/tests/test_rtn_uc.py @@ -51,23 +51,23 @@ def test_set_load(self): Test setting and tripping load. """ self.ss.UC.run(solver='SCIP') - obj = self.ss.UC.obj.v + pgs = self.ss.UC.obj.v.sum() # --- set load --- self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0.1) self.ss.UC.update() self.ss.UC.run(solver='SCIP') - obj_pqt = self.ss.UC.obj.v - self.assertLess(obj_pqt, obj, "Load set does not take effect!") + pgs_pqt = self.ss.UC.obj.v.sum() + self.assertLess(pgs_pqt, pgs, "Load set does not take effect!") # --- trip load --- self.ss.PQ.alter(src='u', idx='PQ_2', value=0) self.ss.UC.update() self.ss.UC.run(solver='SCIP') - obj_pqt2 = self.ss.UC.obj.v - self.assertLess(obj_pqt2, obj_pqt, "Load trip does not take effect!") + pgs_pqt2 = self.ss.UC.obj.v.sum() + self.assertLess(pgs_pqt2, pgs_pqt, "Load trip does not take effect!") @skip_unittest_without_MIP def test_trip_line(self): @@ -130,23 +130,23 @@ def test_set_load(self): Test setting and tripping load. """ self.ss.UCES.run(solver='SCIP') - obj = self.ss.UCES.obj.v + pgs = self.ss.UCES.pg.v.sum() # --- set load --- self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0.1) self.ss.UCES.update() self.ss.UCES.run(solver='SCIP') - obj_pqt = self.ss.UCES.obj.v - self.assertLess(obj_pqt, obj, "Load set does not take effect!") + pgs_pqt = self.ss.UCES.pg.v.sum() + self.assertLess(pgs_pqt, pgs, "Load set does not take effect!") # --- trip load --- self.ss.PQ.alter(src='u', idx='PQ_2', value=0) self.ss.UCES.update() self.ss.UCES.run(solver='SCIP') - obj_pqt2 = self.ss.UCES.obj.v - self.assertLess(obj_pqt2, obj_pqt, "Load trip does not take effect!") + pgs_pqt2 = self.ss.UCES.pg.v.sum() + self.assertLess(pgs_pqt2, pgs_pqt, "Load trip does not take effect!") @skip_unittest_without_MIP def test_trip_line(self): From 7db58b1378d06001903557cbd803a1524968338a Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 23:38:45 -0400 Subject: [PATCH 089/181] [DONE] Refactor test_dctypes, refactor test_ed --- tests/test_dctypes.py | 95 ------------------------------------------- tests/test_rtn_ed.py | 89 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 95 deletions(-) delete mode 100644 tests/test_dctypes.py diff --git a/tests/test_dctypes.py b/tests/test_dctypes.py deleted file mode 100644 index 1e08cb94..00000000 --- a/tests/test_dctypes.py +++ /dev/null @@ -1,95 +0,0 @@ -import unittest -import numpy as np - -import ams - - -class TestDCED(unittest.TestCase): - """ - Test DCED types routine. - """ - - def setUp(self) -> None: - self.ss = ams.load(ams.get_case("ieee39/ieee39_uced_esd1.xlsx"), - default_config=True, - no_output=True, - ) - - def test_dcopf(self): - """ - Test DCOPF. - """ - init = self.ss.DCOPF.init() - self.assertTrue(init, "DCOPF initialization failed!") - self.ss.DCOPF.run(solver='CLARABEL') - np.testing.assert_equal(self.ss.DCOPF.exit_code, 0) - - def test_rted(self) -> None: - """ - Test RTED. - """ - init = self.ss.RTED.init() - self.assertTrue(init, "RTED initialization failed!") - self.ss.RTED.run(solver='CLARABEL') - np.testing.assert_equal(self.ss.RTED.exit_code, 0) - - def test_ed(self) -> None: - """ - Test ED. - """ - init = self.ss.ED.init() - self.assertTrue(init, "ED initialization failed!") - self.ss.ED.run(solver='CLARABEL') - np.testing.assert_equal(self.ss.ED.exit_code, 0) - - @require_MIP_solver - def test_rtedes(self) -> None: - """ - Test RTEDES. - """ - init = self.ss.RTEDES.init() - self.assertTrue(init, "RTEDES initialization failed!") - self.ss.RTEDES.run(solver='SCIP') - np.testing.assert_equal(self.ss.RTEDES.exit_code, 0) - - @require_MIP_solver - def test_edes(self) -> None: - """ - Test EDES. - """ - init = self.ss.EDES.init() - self.assertTrue(init, "EDES initialization failed!") - self.ss.EDES.run(solver='SCIP') - np.testing.assert_equal(self.ss.EDES.exit_code, 0) - - -class test_DCUC(unittest.TestCase): - """ - Test DCUC types routine. - """ - - def setUp(self) -> None: - self.ss = ams.load(ams.get_case("ieee39/ieee39_uced_esd1.xlsx"), - default_config=True, - no_output=True, - ) - - @require_MIP_solver - def test_uc(self) -> None: - """ - Test UC. - """ - init = self.ss.UC.init() - self.assertTrue(init, "UC initialization failed!") - self.ss.UC.run() - np.testing.assert_equal(self.ss.UC.exit_code, 0) - - @require_MIP_solver - def test_uces(self) -> None: - """ - Test UCES. - """ - init = self.ss.UCES.init() - self.assertTrue(init, "UCES initialization failed!") - self.ss.UCES.run() - np.testing.assert_equal(self.ss.UCES.exit_code, 0) diff --git a/tests/test_rtn_ed.py b/tests/test_rtn_ed.py index f98fd05e..38c2a619 100644 --- a/tests/test_rtn_ed.py +++ b/tests/test_rtn_ed.py @@ -93,3 +93,92 @@ def test_set_load(self): self.ss.ED.run(solver='CLARABEL') pgs_pqt2 = self.ss.ED.pg.v.sum() self.assertLess(pgs_pqt2, pgs_pqt, "Load trip does not take effect!") + + +class TestEDES(unittest.TestCase): + """ + Test routine `EDES`. + """ + + def setUp(self) -> None: + self.ss = ams.load(ams.get_case("5bus/pjm5bus_uced_esd1.xlsx"), + setup=True, default_config=True, no_output=True) + # decrease load first + self.ss.PQ.set(src='p0', attr='v', idx=['PQ_1', 'PQ_2'], value=[0.3, 0.3]) + + def test_init(self): + """ + Test initialization. + """ + self.ss.EDES.init() + self.assertTrue(self.ss.EDES.initialized, "EDES initialization failed!") + + def test_trip_gen(self): + """ + Test generator tripping. + """ + # a) ensure EDTSlot.ug takes effect + # NOTE: manually chang ug.v for testing purpose + stg = 'PV_1' + stg_uid = self.ss.ED.pg.get_idx().index(stg) + loc_offtime = np.array([0, 2]) + self.ss.EDTSlot.ug.v[loc_offtime, stg_uid] = 0 + + self.ss.ED.run(solver='CLARABEL') + self.assertTrue(self.ss.ED.converged, "ED did not converge under generator trip!") + pg_pv1 = self.ss.ED.get(src='pg', attr='v', idx=stg) + np.testing.assert_almost_equal(np.zeros_like(loc_offtime), + pg_pv1[loc_offtime], + decimal=6, + err_msg="Generator trip does not take effect!") + + self.ss.EDTSlot.ug.v[...] = 1 # reset + + # b) ensure StaticGen.u does not take effect + # NOTE: in ED, `EDTSlot.ug` is used instead of `StaticGen.u` + self.ss.StaticGen.set(src='u', attr='v', idx=stg, value=0) + self.ss.ED.update() + + self.ss.ED.run(solver='CLARABEL') + self.assertTrue(self.ss.ED.converged, "ED did not converge under generator trip!") + pg_pv1 = self.ss.ED.get(src='pg', attr='v', idx=stg) + np.testing.assert_array_less(np.zeros_like(pg_pv1), pg_pv1, + err_msg="Generator trip take effect, which is unexpected!") + + self.ss.StaticGen.alter(src='u', idx=stg, value=1) # reset + + def test_line_trip(self): + """ + Test line tripping. + """ + self.ss.Line.set(src='u', attr='v', idx=3, value=0) + self.ss.EDES.run(solver='SCIP') + self.assertTrue(self.ss.EDES.converged, "EDES did not converge!") + plf_l3 = self.ss.EDES.get(src='plf', attr='v', idx=3) + np.testing.assert_almost_equal(np.zeros_like(plf_l3), + plf_l3, decimal=6) + + self.ss.Line.alter(src='u', idx=3, value=1) + + def test_set_load(self): + """ + Test setting and tripping load. + """ + self.ss.EDES.run(solver='SCIP') + pgs = self.ss.EDES.pg.v.sum() + + # --- set load --- + self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0.1) + self.ss.EDES.update() + + self.ss.EDES.run(solver='SCIP') + pgs_pqt = self.ss.EDES.pg.v.sum() + self.assertLess(pgs_pqt, pgs, "Load set does not take effect!") + + # --- trip load --- + self.ss.PQ.alter(src='u', idx='PQ_2', value=0) + self.ss.EDES.update() + + self.ss.EDES.run(solver='SCIP') + pgs_pqt2 = self.ss.EDES.pg.v.sum() + self.assertLess(pgs_pqt2, pgs_pqt, "Load trip does not take effect!") From 7c10c7b8acf0f3c7c516e2a305708e34de673f1e Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 23:58:59 -0400 Subject: [PATCH 090/181] Minor fix --- ams/interop/andes.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ams/interop/andes.py b/ams/interop/andes.py index 7e297b20..ea53cf93 100644 --- a/ams/interop/andes.py +++ b/ams/interop/andes.py @@ -695,8 +695,8 @@ def receive(self, adsys=None, routine=None, no_update=False): idx=link['stg_idx'].values) # NOTE: only update u if changed actually u0_rtn = rtn.get(src=vname_ams, attr='v', idx=link['stg_idx'].values).copy() - rtn.alter(src=vname_ams, idx=link['stg_idx'].values, value=u_stg) - rtn.alter(src=vname_ams, idx=link['stg_idx'].values, value=u_dyg) + rtn.set(src=vname_ams, attr='v', idx=link['stg_idx'].values, value=u_stg) + rtn.set(src=vname_ams, attr='v', idx=link['stg_idx'].values, value=u_dyg) u_rtn = rtn.get(src=vname_ams, attr='v', idx=link['stg_idx'].values).copy() if not np.array_equal(u0_rtn, u_rtn): pname_to_update.append(vname_ams) @@ -733,8 +733,8 @@ def receive(self, adsys=None, routine=None, no_update=False): # Sync StaticGen.p first, then overwrite the ones with dynamic generator p_stg = sa.StaticGen.get(src='p', attr='v', idx=link['stg_idx'].values) - rtn.alter(src=vname_ams, idx=link['stg_idx'].values, value=p_stg) - rtn.alter(src=vname_ams, idx=link['stg_idx'].values, value=p_dyg) + rtn.set(src=vname_ams, attr='v', idx=link['stg_idx'].values, value=p_stg) + rtn.set(src=vname_ams, attr='v', idx=link['stg_idx'].values, value=p_dyg) pname_to_update.append(vname_ams) @@ -749,7 +749,7 @@ def receive(self, adsys=None, routine=None, no_update=False): # --- other scenarios --- if _dest_check(mname=mname_ads, pname=pname_ads, idx=idx_ads, adsys=sa): v_ads = mdl_ads.get(src=pname_ads, attr='v', idx=idx_ads) - rtn.alter(src=vname_ams, idx=idx_ads, value=v_ads) + rtn.set(src=vname_ams, attr='v', idx=idx_ads, value=v_ads) pname_to_update.append(vname_ams) logger.warning(f'Receive <{vname_ams}> from {mname_ads}.{pname_ads}') From a1eb40e312c365632c8a083d8470c857bb55f18c Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 23:59:26 -0400 Subject: [PATCH 091/181] Add a param force_init to Routinebase.init --- ams/routines/dcopf.py | 21 +++++++++++++++++++-- ams/routines/routine.py | 18 ++++++++++++------ ams/routines/uc.py | 4 ++-- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/ams/routines/dcopf.py b/ams/routines/dcopf.py index 4acc80ec..806087c9 100644 --- a/ams/routines/dcopf.py +++ b/ams/routines/dcopf.py @@ -232,7 +232,11 @@ def solve(self, *args, **kwargs): """ return self.om.prob.solve(*args, **kwargs) - def run(self, no_code=True, *args, **kwargs): + def run(self, + no_code=True, force_init=False, + force_mats=False, force_constr=False, + force_parse=False, force_generate=False, + *args, **kwargs): """ Run the routine. *args and **kwargs go to `self.solve()`. @@ -241,6 +245,16 @@ def run(self, no_code=True, *args, **kwargs): ---------- no_code : bool, optional If True, print the generated CVXPY code. Defaults to False. + force_init : bool, optional + If True, force re-initialization. Defaults to False. + force_mats : bool, optional + If True, force re-generating matrices. Defaults to False. + force_constr : bool, optional + If True, force re-generating constraints. Defaults to False. + force_parse : bool, optional + If True, force re-parsing the model. Defaults to False. + force_generate : bool, optional + If True, force re-generating the model. Defaults to False. Other Parameters ---------------- @@ -272,7 +286,10 @@ def run(self, no_code=True, *args, **kwargs): kwargs : keywords, optional Additional solver specific arguments. See CVXPY documentation for details. """ - return RoutineBase.run(self, no_code=no_code, *args, **kwargs) + return super().run(no_code=no_code, force_init=force_init, + force_mats=force_mats, force_constr=force_constr, + force_parse=force_parse, force_generate=force_generate, + *args, **kwargs) def _post_solve(self): """ diff --git a/ams/routines/routine.py b/ams/routines/routine.py index a55f6e48..98a8836d 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -237,7 +237,7 @@ def _data_check(self, info=True): logger.debug(" - Data check passed") return True - def init(self, no_code=True, + def init(self, no_code=True, force_init=False, force_mats=False, force_constr=False, force_parse=False, force_generate=False): """ @@ -247,6 +247,8 @@ def init(self, no_code=True, ---------- no_code: bool Whether to show generated code. + force_init: bool + Whether to force re-initialization, will ignore `self.initialized`. force_mats: bool Whether to force build the system matrices, goes to `self.system.mats.build()`. force_constr: bool @@ -256,7 +258,7 @@ def init(self, no_code=True, force_generate: bool Whether to force generate symbols, goes to `self.om.init()`. """ - skip_all = not (force_mats and force_parse and force_generate) and self.initialized + skip_all = not (force_init and force_mats and force_parse and force_generate) and self.initialized if skip_all: logger.debug(f"{self.class_name} has already been initialized.") @@ -310,9 +312,9 @@ def _post_solve(self): return None def run(self, + no_code=True, force_init=False, force_mats=False, force_constr=False, force_parse=False, force_generate=False, - no_code=True, *args, **kwargs): """ Run the routine. @@ -325,6 +327,10 @@ def run(self, Parameters ---------- + no_code: bool + Whether to show generated code. + force_init: bool + Whether to force re-initialization, will ignore `self.initialized`. force_mats: bool Whether to force build the system matrices, goes to `self.init()`. force_constr: bool @@ -337,9 +343,9 @@ def run(self, Whether to show generated code. """ # --- setup check --- - self.init(force_mats=force_mats, force_constr=force_constr, - force_parse=force_parse, force_generate=force_generate, - no_code=no_code) + self.init(no_code=no_code, force_init=force_init, + force_mats=force_mats, force_constr=force_constr, + force_parse=force_parse, force_generate=force_generate) # --- solve optimization --- t0, _ = elapsed() _ = self.solve(*args, **kwargs) diff --git a/ams/routines/uc.py b/ams/routines/uc.py index 98b083e3..9d068fa1 100644 --- a/ams/routines/uc.py +++ b/ams/routines/uc.py @@ -311,11 +311,11 @@ def _initial_guess(self): logger.warning(f"As initial commitment guess, turn off StaticGen: {off_gen}") return g_idx - def init(self, no_code=True, + def init(self, no_code=True, force_init=False, force_mats=False, force_constr=False, force_parse=False, force_generate=False): self._initial_guess() - return super().init(no_code=no_code, + return super().init(no_code=no_code, force_init=force_init, force_mats=force_mats, force_constr=force_constr, force_parse=force_parse, force_generate=force_generate) From 0d0129d46b8868153523f0e6408769bf173e07ae Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 1 Nov 2024 23:59:44 -0400 Subject: [PATCH 092/181] Format --- ams/routines/dcopf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ams/routines/dcopf.py b/ams/routines/dcopf.py index 806087c9..b4e84bc1 100644 --- a/ams/routines/dcopf.py +++ b/ams/routines/dcopf.py @@ -287,9 +287,9 @@ def run(self, Additional solver specific arguments. See CVXPY documentation for details. """ return super().run(no_code=no_code, force_init=force_init, - force_mats=force_mats, force_constr=force_constr, - force_parse=force_parse, force_generate=force_generate, - *args, **kwargs) + force_mats=force_mats, force_constr=force_constr, + force_parse=force_parse, force_generate=force_generate, + *args, **kwargs) def _post_solve(self): """ From 39c4160e914537142068618ce812e374ffe50cf2 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 2 Nov 2024 00:21:48 -0400 Subject: [PATCH 093/181] [WIP] Refactor OModel to have an evaluate method --- ams/opt/omodel.py | 76 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 64 insertions(+), 12 deletions(-) diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index 9c36e1c2..606a29e4 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -49,6 +49,7 @@ def __init__(self, self.is_disabled = False self.rtn = None self.optz = None # corresponding optimization element + self.code = None def parse(self): """ @@ -56,6 +57,12 @@ def parse(self): """ raise NotImplementedError + def evaluate(self): + """ + Evaluate the object. + """ + raise NotImplementedError + @property def class_name(self): """ @@ -137,8 +144,13 @@ def parse(self, no_code=True): code_expr = "self.optz = " + code_expr if not no_code: logger.info(f"<{self.name}> code: {code_expr}") - # execute the expression - exec(code_expr, globals(), locals()) + return True + + def evaluate(self): + """ + Evaluate the expression. + """ + exec(self.code, globals(), locals()) return True @property @@ -230,14 +242,17 @@ def parse(self): """ Parse the parameter. """ - config = self.config.as_dict() # NOQA sub_map = self.om.rtn.syms.sub_map shape = np.shape(self.v) # NOTE: it seems that there is no need to use re.sub here code_param = f"self.optz=param(shape={shape}, **config)" for pattern, replacement, in sub_map.items(): code_param = re.sub(pattern, replacement, code_param) - exec(code_param, globals(), locals()) + self.code = code_param + + def evaluate(self): + config = self.config.as_dict() # NOQA + exec(self.code, globals(), locals()) try: msg = f"Parameter <{self.name}> is set as sparse, " msg += "but the value is not sparse." @@ -464,8 +479,14 @@ def parse(self): code_var = f"self.optz=var({shape}, **config)" for pattern, replacement, in sub_map.items(): code_var = re.sub(pattern, replacement, code_var) - # build the Var object - exec(code_var, globals(), locals()) + self.code = code_var + return True + + def evaluate(self): + """ + Evaluate the variable. + """ + exec(self.code, globals(), locals()) return True def __repr__(self): @@ -531,15 +552,20 @@ def parse(self, no_code=True): except TypeError as e: logger.error(f"Error in parsing constr <{self.name}>.") raise e - # store the parsed expression str code - self.code = code_constr # parse the constraint type code_constr = "self.optz=" + code_constr code_constr += " == 0" if self.is_eq else " <= 0" + # store the parsed expression str code + self.code = code_constr if not no_code: logger.info(f"<{self.name}> code: {code_constr}") - # set the parsed constraint - exec(code_constr, globals(), locals()) + return True + + def evaluate(self): + """ + Evaluate the constraint. + """ + exec(self.code, globals(), locals()) return True def __repr__(self): @@ -709,8 +735,14 @@ def parse(self, no_code=True): code_obj = f"self.optz={sense}({code_obj})" if not no_code: logger.info(f"Code: {code_obj}") - # set the parsed objective function - exec(code_obj, globals(), locals()) + self.code = code_obj + return True + + def evaluate(self): + """ + Evaluate the objective function. + """ + exec(self.code, globals(), locals()) exec("self.om.obj = self.optz", globals(), locals()) return True @@ -846,6 +878,24 @@ def parse(self, no_code=True, force_generate=False): self.parsed = True return self.parsed + def evaluate(self): + """ + Evaluate the optimization model. + """ + if not self.parsed: + raise ValueError("Model is not parsed yet.") + for key, val in self.rtn.params.items(): + val.evaluate() + for key, val in self.rtn.vars.items(): + val.evaluate() + for key, val in self.rtn.constrs.items(): + val.evaluate() + if self.rtn.type != 'PF': + self.rtn.obj.evaluate() + for key, val in self.rtn.exprs.items(): + val.evaluate() + return True + def init(self, no_code=True, force_parse=False, force_generate=False): """ Set up the optimization model from the symbolic description. @@ -879,6 +929,8 @@ def init(self, no_code=True, force_parse=False, force_generate=False): logger.debug(f"OModel for <{self.rtn.class_name}> initialized in {s_init}.") return self.initialized + self.evaluate() + # --- evaluate the optimziation --- t_eva, _ = elapsed() code_prob = "self.prob = problem(self.obj, " From 25f43e4ef99c2556380c2af5fdf75e06a95cb7c9 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 2 Nov 2024 07:13:59 -0400 Subject: [PATCH 094/181] [WIP] Refactor OModel to have an evaluate method, minor cleanup --- ams/opt/omodel.py | 48 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index 606a29e4..4e896883 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -241,6 +241,11 @@ def __init__(self, def parse(self): """ Parse the parameter. + + Returns + ------- + bool + Returns True if the parsing is successful, False otherwise. """ sub_map = self.om.rtn.syms.sub_map shape = np.shape(self.v) @@ -249,9 +254,10 @@ def parse(self): for pattern, replacement, in sub_map.items(): code_param = re.sub(pattern, replacement, code_param) self.code = code_param + return True def evaluate(self): - config = self.config.as_dict() # NOQA + config = self.config.as_dict() # NOQA, used in `self.code` exec(self.code, globals(), locals()) try: msg = f"Parameter <{self.name}> is set as sparse, " @@ -485,6 +491,11 @@ def parse(self): def evaluate(self): """ Evaluate the variable. + + Returns + ------- + bool + Returns True if the evaluation is successful, False otherwise. """ exec(self.code, globals(), locals()) return True @@ -518,6 +529,12 @@ class Constraint(OptzBase): Flag indicating if the constraint is disabled, False by default. rtn : ams.routines.Routine The owner routine instance. + is_disabled : bool, optional + Flag indicating if the constraint is disabled, False by default. + dual : float, optional + The dual value of the constraint. + code : str, optional + The code string for the constraint """ def __init__(self, @@ -532,7 +549,6 @@ def __init__(self, self.is_disabled = False self.dual = None self.code = None - # TODO: add constraint info from solver, maybe dual? def parse(self, no_code=True): """ @@ -653,6 +669,8 @@ class Objective(OptzBase): computation. rtn : ams.routines.Routine The owner routine instance. + code : str + The code string for the objective function. """ def __init__(self, @@ -721,6 +739,11 @@ def parse(self, no_code=True): ---------- no_code : bool, optional Flag indicating if the code should be shown, True by default. + + Returns + ------- + bool + Returns True if the parsing is successful, False otherwise. """ # parse the expression str sub_map = self.om.rtn.syms.sub_map @@ -741,6 +764,11 @@ def parse(self, no_code=True): def evaluate(self): """ Evaluate the objective function. + + Returns + ------- + bool + Returns True if the evaluation is successful, False otherwise. """ exec(self.code, globals(), locals()) exec("self.om.obj = self.optz", globals(), locals()) @@ -771,6 +799,12 @@ class OModel: Constraints. obj: Objective Objective function. + initialized: bool + Flag indicating if the model is initialized. + parsed: bool + Flag indicating if the model is parsed. + evaluated: bool + Flag indicating if the model is evaluated. """ def __init__(self, routine): @@ -782,6 +816,7 @@ def __init__(self, routine): self.obj = None self.initialized = False self.parsed = False + self.evaluated = False def parse(self, no_code=True, force_generate=False): """ @@ -794,6 +829,11 @@ def parse(self, no_code=True, force_generate=False): True by default. force_generate : bool, optional Flag indicating if the symbols should be generated, goes to `self.rtn.syms.generate_symbols()`. + + Returns + ------- + bool + Returns True if the parsing is successful, False otherwise. """ rtn = self.rtn rtn.syms.generate_symbols(force_generate=force_generate) @@ -894,7 +934,9 @@ def evaluate(self): self.rtn.obj.evaluate() for key, val in self.rtn.exprs.items(): val.evaluate() - return True + + self.evaluated = True + return self.evaluated def init(self, no_code=True, force_parse=False, force_generate=False): """ From 8e08fcd69b8e684f91b82e53ef7b4ab97e2dd3d2 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 2 Nov 2024 08:19:07 -0400 Subject: [PATCH 095/181] [WIP] Refactor OModel to have an evaluate method, known to work for Routine.run --- ams/opt/omodel.py | 113 +++++++++++++++++++++++++--------------------- 1 file changed, 62 insertions(+), 51 deletions(-) diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index 4e896883..c5046c32 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -150,6 +150,7 @@ def evaluate(self): """ Evaluate the expression. """ + logger.debug(f" - Expression <{self.name}>: {self.code}") exec(self.code, globals(), locals()) return True @@ -257,6 +258,9 @@ def parse(self): return True def evaluate(self): + if self.no_parse: + return True + config = self.config.as_dict() # NOQA, used in `self.code` exec(self.code, globals(), locals()) try: @@ -269,10 +273,14 @@ def evaluate(self): exec(f"self.optz.value = {val}", globals(), locals()) except ValueError: msg = f"Parameter <{self.name}> has non-numeric value, " - msg += "no_parse=True is applied." + msg += "set `no_parse=True`." logger.warning(msg) self.no_parse = True return False + except Exception as e: + logger.error(f"Error in evaluating param <{self.name}>.") + logger.error(f"Original error: {e}") + return False return True def update(self): @@ -450,18 +458,6 @@ def parse(self): Parse the variable. """ sub_map = self.om.rtn.syms.sub_map - # only used for CVXPY - # NOTE: Config only allow lower case letters, do a conversion here - config = {} - for k, v in self.config.as_dict().items(): - if k == 'psd': - config['PSD'] = v - elif k == 'nsd': - config['NSD'] = v - elif k == 'bool': - config['boolean'] = v - else: - config[k] = v # NOTE: number of rows is the size of the source variable if self.owner is not None: nr = self.owner.n @@ -497,7 +493,24 @@ def evaluate(self): bool Returns True if the evaluation is successful, False otherwise. """ - exec(self.code, globals(), locals()) + # NOTE: in CVXPY, Config only allow lower case letters + config = {} # used in `self.code` + for k, v in self.config.as_dict().items(): + if k == 'psd': + config['PSD'] = v + elif k == 'nsd': + config['NSD'] = v + elif k == 'bool': + config['boolean'] = v + else: + config[k] = v + logger.debug(f" - Var <{self.name}>: {self.code}") + try: + exec(self.code, globals(), locals()) + except Exception as e: + logger.error(f"Error in evaluating var <{self.name}>.") + logger.error(f"Original error: {e}") + return False return True def __repr__(self): @@ -573,15 +586,19 @@ def parse(self, no_code=True): code_constr += " == 0" if self.is_eq else " <= 0" # store the parsed expression str code self.code = code_constr - if not no_code: - logger.info(f"<{self.name}> code: {code_constr}") return True def evaluate(self): """ Evaluate the constraint. """ - exec(self.code, globals(), locals()) + logger.debug(f" - Constraint <{self.name}>: {self.code}") + try: + exec(self.code, globals(), locals()) + except Exception as e: + logger.error(f"Error in evaluating constr <{self.name}>.") + logger.error(f"Original error: {e}") + return False return True def __repr__(self): @@ -771,7 +788,6 @@ def evaluate(self): Returns True if the evaluation is successful, False otherwise. """ exec(self.code, globals(), locals()) - exec("self.om.obj = self.optz", globals(), locals()) return True def __repr__(self): @@ -821,6 +837,7 @@ def __init__(self, routine): def parse(self, no_code=True, force_generate=False): """ Parse the optimization model from the symbolic description. + Must be called after generating the symbols `self.rtn.syms.generate_symbols()`. Parameters ---------- @@ -835,15 +852,12 @@ def parse(self, no_code=True, force_generate=False): bool Returns True if the parsing is successful, False otherwise. """ - rtn = self.rtn - rtn.syms.generate_symbols(force_generate=force_generate) - + t, _ = elapsed() # --- add RParams and Services as parameters --- - t0, _ = elapsed() - logger.debug(f'Parsing OModel for {rtn.class_name}') - for key, val in rtn.params.items(): + logger.debug(f'Parsing OModel for <{self.rtn.class_name}>') + for key, val in self.rtn.params.items(): if not val.no_parse: - logger.debug(f" - Parsing Param <{key}>") + logger.debug(f" - Param <{key}>") try: val.parse() except Exception as e: @@ -851,27 +865,21 @@ def parse(self, no_code=True, force_generate=False): msg += f"Original error: {e}" raise Exception(msg) setattr(self, key, val.optz) - _, s = elapsed(t0) - logger.debug(f" -> Parse Params in {s}") # --- add decision variables --- - t0, _ = elapsed() - for key, val in rtn.vars.items(): + for key, val in self.rtn.vars.items(): try: - logger.debug(f" - Parsing Var <{key}>") + logger.debug(f" - Var <{key}>") val.parse() except Exception as e: msg = f"Failed to parse Var <{key}>. " msg += f"Original error: {e}" raise Exception(msg) setattr(self, key, val.optz) - _, s = elapsed(t0) - logger.debug(f" -> Parse Vars in {s}") # --- add constraints --- - t0, _ = elapsed() - for key, val in rtn.constrs.items(): - logger.debug(f" - Parsing Constr <{key}>: {val.e_str}") + for key, val in self.rtn.constrs.items(): + logger.debug(f" - Constr <{key}>: {val.e_str}") try: val.parse(no_code=no_code) except Exception as e: @@ -879,32 +887,25 @@ def parse(self, no_code=True, force_generate=False): msg += f"Original error: {e}" raise Exception(msg) setattr(self, key, val.optz) - _, s = elapsed(t0) - logger.debug(f" -> Parse Constrs in {s}") # --- parse objective functions --- - t0, _ = elapsed() - if rtn.type != 'PF': - logger.debug(f" - Parsing Objective <{rtn.obj.name}>") - if rtn.obj is not None: + if self.rtn.type != 'PF': + logger.debug(f" - Objective <{self.rtn.obj.name}>: {self.rtn.obj.e_str}") + if self.rtn.obj is not None: try: - rtn.obj.parse(no_code=no_code) + self.rtn.obj.parse(no_code=no_code) except Exception as e: - msg = f"Failed to parse Objective <{rtn.obj.name}>. " + msg = f"Failed to parse Objective <{self.rtn.obj.name}>. " msg += f"Original error: {e}" raise Exception(msg) else: - logger.warning(f"{rtn.class_name} has no objective function!") - _, s = elapsed(t0) + logger.warning(f"{self.rtn.class_name} has no objective function!") self.parsed = False return self.parsed - _, s = elapsed(t0) - logger.debug(f" -> Parse Objective in {s}") # --- parse expressions --- - t0, _ = elapsed() for key, val in self.rtn.exprs.items(): - msg = f" - Parsing ExpressionCalc <{key}>: {val.e_str} " + msg = f" - ExpressionCalc <{key}>: {val.e_str} " logger.debug(msg) try: val.parse(no_code=no_code) @@ -912,8 +913,8 @@ def parse(self, no_code=True, force_generate=False): msg = f"Failed to parse ExpressionCalc <{key}>. " msg += f"Original error: {e}" raise Exception(msg) - _, s = elapsed(t0) - logger.debug(f" -> Parse Expressions in {s}") + _, s = elapsed(t) + logger.debug(f" -> Parsed in {s}") self.parsed = True return self.parsed @@ -922,20 +923,30 @@ def evaluate(self): """ Evaluate the optimization model. """ + logger.debug(f"Evaluating OModel for <{self.rtn.class_name}>") + t, _ = elapsed() if not self.parsed: raise ValueError("Model is not parsed yet.") for key, val in self.rtn.params.items(): val.evaluate() + setattr(self, key, val.optz) for key, val in self.rtn.vars.items(): val.evaluate() + setattr(self, key, val.optz) for key, val in self.rtn.constrs.items(): val.evaluate() + setattr(self, key, val.optz) if self.rtn.type != 'PF': self.rtn.obj.evaluate() + self.obj = self.rtn.obj.optz + # similar to `setattr` + exec("self.obj = self.rtn.obj.optz", globals(), locals()) for key, val in self.rtn.exprs.items(): val.evaluate() self.evaluated = True + _, s = elapsed(t) + logger.debug(f"OModel for <{self.rtn.class_name}> evaluated in {s}") return self.evaluated def init(self, no_code=True, force_parse=False, force_generate=False): From ee392abc155ced7dee98f660bc8c4352db1b9d43 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 2 Nov 2024 08:21:26 -0400 Subject: [PATCH 096/181] [WIP] Refactor OModel to have an evaluate method, known to work for Routine.run --- ams/opt/omodel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index c5046c32..45188dce 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -938,9 +938,9 @@ def evaluate(self): setattr(self, key, val.optz) if self.rtn.type != 'PF': self.rtn.obj.evaluate() + # NOTE: since we already have the attribute `obj`, + # we can update it rather than setting it self.obj = self.rtn.obj.optz - # similar to `setattr` - exec("self.obj = self.rtn.obj.optz", globals(), locals()) for key, val in self.rtn.exprs.items(): val.evaluate() From 2dc745fd7897b47cc1256ca049e5e14076b00134 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 2 Nov 2024 08:24:04 -0400 Subject: [PATCH 097/181] [WIP] Refactor OModel to have an evaluate method, known to work for Routine.run --- ams/opt/omodel.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index 45188dce..9d960ed3 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -864,7 +864,6 @@ def parse(self, no_code=True, force_generate=False): msg = f"Failed to parse Param <{key}>. " msg += f"Original error: {e}" raise Exception(msg) - setattr(self, key, val.optz) # --- add decision variables --- for key, val in self.rtn.vars.items(): @@ -875,7 +874,6 @@ def parse(self, no_code=True, force_generate=False): msg = f"Failed to parse Var <{key}>. " msg += f"Original error: {e}" raise Exception(msg) - setattr(self, key, val.optz) # --- add constraints --- for key, val in self.rtn.constrs.items(): @@ -886,7 +884,6 @@ def parse(self, no_code=True, force_generate=False): msg = f"Failed to parse Constr <{key}>. " msg += f"Original error: {e}" raise Exception(msg) - setattr(self, key, val.optz) # --- parse objective functions --- if self.rtn.type != 'PF': From f3e586ef538cbff33fc488e2c26fc77e73d560f7 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 2 Nov 2024 08:25:58 -0400 Subject: [PATCH 098/181] [WIP] Refactor OModel to have an evaluate method, known to work for Routine.run --- 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 9d960ed3..aaa6541c 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -943,7 +943,7 @@ def evaluate(self): self.evaluated = True _, s = elapsed(t) - logger.debug(f"OModel for <{self.rtn.class_name}> evaluated in {s}") + logger.debug(f" -> Evaluated in {s}") return self.evaluated def init(self, no_code=True, force_parse=False, force_generate=False): From 811e634293fab9967066240fe624a76fa69d7c69 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 2 Nov 2024 08:30:24 -0400 Subject: [PATCH 099/181] [WIP] Refactor OModel to have an evaluate method, known to work for Routine.run --- ams/opt/omodel.py | 49 +++++++++++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index aaa6541c..f61474f2 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -919,6 +919,11 @@ def parse(self, no_code=True, force_generate=False): def evaluate(self): """ Evaluate the optimization model. + + Returns + ------- + bool + Returns True if the evaluation is successful, False otherwise. """ logger.debug(f"Evaluating OModel for <{self.rtn.class_name}>") t, _ = elapsed() @@ -946,6 +951,31 @@ def evaluate(self): logger.debug(f" -> Evaluated in {s}") return self.evaluated + def finalize(self): + """ + Finalize the optimization model. + + Returns + ------- + """ + logger.debug(f"Finalizing OModel for <{self.rtn.class_name}>") + t, _ = elapsed() + code_prob = "self.prob = problem(self.obj, " + constrs_skip = [] + constrs_add = [] + for key, val in self.rtn.constrs.items(): + if (val.is_disabled) or (val is None): + constrs_skip.append(f'<{key}>') + else: + constrs_add.append(val.optz) + code_prob += "[constr for constr in constrs_add])" + for pattern, replacement in self.rtn.syms.sub_map.items(): + code_prob = re.sub(pattern, replacement, code_prob) + + exec(code_prob, globals(), locals()) + _, s = elapsed(t) + logger.debug(f" -> Finalized in {s}") + def init(self, no_code=True, force_parse=False, force_generate=False): """ Set up the optimization model from the symbolic description. @@ -981,23 +1011,8 @@ def init(self, no_code=True, force_parse=False, force_generate=False): self.evaluate() - # --- evaluate the optimziation --- - t_eva, _ = elapsed() - code_prob = "self.prob = problem(self.obj, " - constrs_skip = [] - constrs_add = [] - for key, val in self.rtn.constrs.items(): - if (val.is_disabled) or (val is None): - constrs_skip.append(f'<{key}>') - else: - constrs_add.append(val.optz) - code_prob += "[constr for constr in constrs_add])" - for pattern, replacement in self.rtn.syms.sub_map.items(): - code_prob = re.sub(pattern, replacement, code_prob) - - exec(code_prob, globals(), locals()) - _, s_eva = elapsed(t_eva) - logger.debug(f"OModel for <{self.rtn.class_name}> evaluated in {s_eva}") + # --- finalize the optimziation --- + self.finalize() _, s_init = elapsed(t_init) self.initialized = True From dfca92aa9d1d94a581da852e5e6e879749349b62 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 2 Nov 2024 08:46:45 -0400 Subject: [PATCH 100/181] [WIP] Refactor OModel to have an evaluate method, remove force_ series params in OModel, known to work for Routine.run --- ams/opt/omodel.py | 71 ++++++++++++++++------------------------- ams/routines/routine.py | 2 +- 2 files changed, 29 insertions(+), 44 deletions(-) diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index f61474f2..1e828680 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -121,14 +121,9 @@ def __init__(self, self.e_str = e_str self.code = None - def parse(self, no_code=True): + def parse(self): """ Parse the Expression. - - Parameters - ---------- - no_code : bool, optional - Flag indicating if the code should be shown, True by default. """ # parse the expression str sub_map = self.om.rtn.syms.sub_map @@ -142,8 +137,6 @@ def parse(self, no_code=True): # store the parsed expression str code self.code = code_expr code_expr = "self.optz = " + code_expr - if not no_code: - logger.info(f"<{self.name}> code: {code_expr}") return True def evaluate(self): @@ -563,14 +556,9 @@ def __init__(self, self.dual = None self.code = None - def parse(self, no_code=True): + def parse(self): """ Parse the constraint. - - Parameters - ---------- - no_code : bool, optional - Flag indicating if the code should be shown, True by default. """ # parse the expression str sub_map = self.om.rtn.syms.sub_map @@ -748,15 +736,10 @@ def v(self): def v(self, value): raise AttributeError("Cannot set the value of the objective function.") - def parse(self, no_code=True): + def parse(self): """ Parse the objective function. - Parameters - ---------- - no_code : bool, optional - Flag indicating if the code should be shown, True by default. - Returns ------- bool @@ -773,8 +756,6 @@ def parse(self, no_code=True): raise ValueError(f'Objective sense {self.sense} is not supported.') sense = 'cp.Minimize' if self.sense == 'min' else 'cp.Maximize' code_obj = f"self.optz={sense}({code_obj})" - if not no_code: - logger.info(f"Code: {code_obj}") self.code = code_obj return True @@ -833,19 +814,16 @@ def __init__(self, routine): self.initialized = False self.parsed = False self.evaluated = False + self.finalized = False - def parse(self, no_code=True, force_generate=False): + def parse(self): """ Parse the optimization model from the symbolic description. - Must be called after generating the symbols `self.rtn.syms.generate_symbols()`. - Parameters - ---------- - no_code : bool, optional - Flag indicating if the parsing code should be displayed, - True by default. - force_generate : bool, optional - Flag indicating if the symbols should be generated, goes to `self.rtn.syms.generate_symbols()`. + This method should be called after the routine symbols are generated + `self.rtn.syms.generate_symbols()`. It parses the following components + of the optimization model: parameters, decision variables, constraints, + objective function, and expressions. Returns ------- @@ -867,8 +845,8 @@ def parse(self, no_code=True, force_generate=False): # --- add decision variables --- for key, val in self.rtn.vars.items(): + logger.debug(f" - Var <{key}>") try: - logger.debug(f" - Var <{key}>") val.parse() except Exception as e: msg = f"Failed to parse Var <{key}>. " @@ -879,7 +857,7 @@ def parse(self, no_code=True, force_generate=False): for key, val in self.rtn.constrs.items(): logger.debug(f" - Constr <{key}>: {val.e_str}") try: - val.parse(no_code=no_code) + val.parse() except Exception as e: msg = f"Failed to parse Constr <{key}>. " msg += f"Original error: {e}" @@ -890,7 +868,7 @@ def parse(self, no_code=True, force_generate=False): logger.debug(f" - Objective <{self.rtn.obj.name}>: {self.rtn.obj.e_str}") if self.rtn.obj is not None: try: - self.rtn.obj.parse(no_code=no_code) + self.rtn.obj.parse() except Exception as e: msg = f"Failed to parse Objective <{self.rtn.obj.name}>. " msg += f"Original error: {e}" @@ -902,12 +880,11 @@ def parse(self, no_code=True, force_generate=False): # --- parse expressions --- for key, val in self.rtn.exprs.items(): - msg = f" - ExpressionCalc <{key}>: {val.e_str} " - logger.debug(msg) + logger.debug(f" - ExpressionCalc <{key}>: {val.e_str}") try: - val.parse(no_code=no_code) + val.parse() except Exception as e: - msg = f"Failed to parse ExpressionCalc <{key}>. " + msg = f"Failed to parse ExpressionCalc <{key}>." msg += f"Original error: {e}" raise Exception(msg) _, s = elapsed(t) @@ -920,6 +897,10 @@ def evaluate(self): """ Evaluate the optimization model. + This method should be called after `self.parse()`. It evaluates the following + components of the optimization model: parameters, decision variables, constraints, + objective function, and expressions. + Returns ------- bool @@ -955,8 +936,13 @@ def finalize(self): """ Finalize the optimization model. + This method should be called after `self.evaluate()`. It assemble the optimization + problem from the evaluated components. + Returns ------- + bool + Returns True if the finalization is successful, False otherwise. """ logger.debug(f"Finalizing OModel for <{self.rtn.class_name}>") t, _ = elapsed() @@ -975,8 +961,10 @@ def finalize(self): exec(code_prob, globals(), locals()) _, s = elapsed(t) logger.debug(f" -> Finalized in {s}") + self.evaluated = True + return self.evaluated - def init(self, no_code=True, force_parse=False, force_generate=False): + def init(self, force_parse=False, force_generate=False): """ Set up the optimization model from the symbolic description. @@ -985,9 +973,6 @@ def init(self, no_code=True, force_parse=False, force_generate=False): Parameters ---------- - no_code : bool, optional - Flag indicating if the parsing code should be displayed, - True by default. force_parse : bool, optional Flag indicating if the parsing should be forced, goes to `self.parse()`. force_generate : bool, optional @@ -1001,7 +986,7 @@ def init(self, no_code=True, force_parse=False, force_generate=False): t_init, _ = elapsed() if force_parse or not self.parsed: - self.parse(no_code=no_code, force_generate=force_generate) + self.parse(force_generate=force_generate) if self.rtn.type == 'PF': _, s_init = elapsed(t_init) diff --git a/ams/routines/routine.py b/ams/routines/routine.py index 98a8836d..19ee9f8a 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -280,7 +280,7 @@ def init(self, no_code=True, force_init=False, _ = self._get_off_constrs() if not self.om.initialized: - self.om.init(no_code=no_code, force_parse=force_parse, force_generate=force_generate) + self.om.init() _, s_init = elapsed(t0) msg = f"<{self.class_name}> " From 08ad9785072e753c0a22507275049b038f8d929f Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 2 Nov 2024 10:24:55 -0400 Subject: [PATCH 101/181] [WIP] Refactor OModel to have an evaluate method, switch to using eval from exec, known to work for Routine.run --- ams/opt/omodel.py | 112 ++++++++++++++++++++++++++++++---------------- 1 file changed, 74 insertions(+), 38 deletions(-) diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index 1e828680..ed219295 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -88,7 +88,7 @@ def shape(self): try: return self.om.__dict__[self.name].shape except KeyError: - logger.warning('Shape info is not ready before initialziation.') + logger.warning('Shape info is not ready before initialization.') return None @property @@ -99,7 +99,7 @@ def size(self): if self.rtn.initialized: return self.om.__dict__[self.name].size else: - logger.warning(f'<{self.rtn.class_name}> is not initialized yet.') + logger.warning(f'Routine <{self.rtn.class_name}> is not initialized yet.') return None @@ -136,7 +136,6 @@ def parse(self): raise e # store the parsed expression str code self.code = code_expr - code_expr = "self.optz = " + code_expr return True def evaluate(self): @@ -144,7 +143,8 @@ def evaluate(self): Evaluate the expression. """ logger.debug(f" - Expression <{self.name}>: {self.code}") - exec(self.code, globals(), locals()) + local_vars = {'self': self} + self.optz = eval(self.code, globals(), local_vars) return True @property @@ -211,7 +211,7 @@ def __init__(self, integer: Optional[bool] = False, pos: Optional[bool] = False, neg: Optional[bool] = False, - sparse: Optional[list] = False, + sparse: Optional[bool] = False, ): OptzBase.__init__(self, name=name, info=info, unit=unit) self.no_parse = no_parse # True to skip parsing the parameter @@ -244,7 +244,7 @@ def parse(self): sub_map = self.om.rtn.syms.sub_map shape = np.shape(self.v) # NOTE: it seems that there is no need to use re.sub here - code_param = f"self.optz=param(shape={shape}, **config)" + code_param = f"param(shape={shape}, **config)" for pattern, replacement, in sub_map.items(): code_param = re.sub(pattern, replacement, code_param) self.code = code_param @@ -255,7 +255,6 @@ def evaluate(self): return True config = self.config.as_dict() # NOQA, used in `self.code` - exec(self.code, globals(), locals()) try: msg = f"Parameter <{self.name}> is set as sparse, " msg += "but the value is not sparse." @@ -263,7 +262,8 @@ def evaluate(self): if self.sparse: if not spr.issparse(self.v): val = "sps.csr_matrix(self.v)" - exec(f"self.optz.value = {val}", globals(), locals()) + local_vars = {'self': self, 'config': config, 'sps': sps} + self.optz = eval(val, globals(), local_vars) except ValueError: msg = f"Parameter <{self.name}> has non-numeric value, " msg += "set `no_parse=True`." @@ -471,7 +471,7 @@ def parse(self): nc = shape[1] if len(shape) > 1 else 0 else: raise ValueError(f"Invalid shape {self._shape}.") - code_var = f"self.optz=var({shape}, **config)" + code_var = f"var({shape}, **config)" for pattern, replacement, in sub_map.items(): code_var = re.sub(pattern, replacement, code_var) self.code = code_var @@ -499,7 +499,8 @@ def evaluate(self): config[k] = v logger.debug(f" - Var <{self.name}>: {self.code}") try: - exec(self.code, globals(), locals()) + local_vars = {'self': self, 'config': config} + self.optz = eval(self.code, globals(), local_vars) except Exception as e: logger.error(f"Error in evaluating var <{self.name}>.") logger.error(f"Original error: {e}") @@ -547,7 +548,7 @@ def __init__(self, name: Optional[str] = None, e_str: Optional[str] = None, info: Optional[str] = None, - is_eq: Optional[str] = False, + is_eq: Optional[bool] = False, ): OptzBase.__init__(self, name=name, info=info) self.e_str = e_str @@ -570,7 +571,6 @@ def parse(self): logger.error(f"Error in parsing constr <{self.name}>.") raise e # parse the constraint type - code_constr = "self.optz=" + code_constr code_constr += " == 0" if self.is_eq else " <= 0" # store the parsed expression str code self.code = code_constr @@ -580,14 +580,14 @@ def evaluate(self): """ Evaluate the constraint. """ - logger.debug(f" - Constraint <{self.name}>: {self.code}") + logger.debug(f" - Constr <{self.name}>: {self.code}") try: - exec(self.code, globals(), locals()) + local_vars = {'self': self} + self.optz = eval(self.code, globals(), local_vars) except Exception as e: logger.error(f"Error in evaluating constr <{self.name}>.") logger.error(f"Original error: {e}") return False - return True def __repr__(self): enabled = 'OFF' if self.is_disabled else 'ON' @@ -755,8 +755,7 @@ def parse(self): if self.sense not in ['min', 'max']: raise ValueError(f'Objective sense {self.sense} is not supported.') sense = 'cp.Minimize' if self.sense == 'min' else 'cp.Maximize' - code_obj = f"self.optz={sense}({code_obj})" - self.code = code_obj + self.code = f"{sense}({code_obj})" return True def evaluate(self): @@ -768,7 +767,8 @@ def evaluate(self): bool Returns True if the evaluation is successful, False otherwise. """ - exec(self.code, globals(), locals()) + logger.debug(f" - Objective <{self.name}>: {self.e_str}") + self.optz = eval(self.code, globals(), locals()) return True def __repr__(self): @@ -893,6 +893,52 @@ def parse(self): self.parsed = True return self.parsed + def _evaluate_params(self): + """ + Evaluate the parameters. + """ + for key, val in self.rtn.params.items(): + val.evaluate() + setattr(self, key, val.optz) + return True + + def _evaluate_vars(self): + """ + Evaluate the decision variables. + """ + for key, val in self.rtn.vars.items(): + val.evaluate() + setattr(self, key, val.optz) + return True + + def _evaluate_constrs(self): + """ + Evaluate the constraints. + """ + for key, val in self.rtn.constrs.items(): + val.evaluate() + setattr(self, key, val.optz) + return True + + def _evaluate_obj(self): + """ + Evaluate the objective function. + """ + # NOTE: since we already have the attribute `obj`, + # we can update it rather than setting it + if self.rtn.type != 'PF': + self.rtn.obj.evaluate() + self.obj = self.rtn.obj.optz + return True + + def _evaluate_exprs(self): + """ + Evaluate the expressions. + """ + for key, val in self.rtn.exprs.items(): + val.evaluate() + return True + def evaluate(self): """ Evaluate the optimization model. @@ -910,22 +956,11 @@ def evaluate(self): t, _ = elapsed() if not self.parsed: raise ValueError("Model is not parsed yet.") - for key, val in self.rtn.params.items(): - val.evaluate() - setattr(self, key, val.optz) - for key, val in self.rtn.vars.items(): - val.evaluate() - setattr(self, key, val.optz) - for key, val in self.rtn.constrs.items(): - val.evaluate() - setattr(self, key, val.optz) - if self.rtn.type != 'PF': - self.rtn.obj.evaluate() - # NOTE: since we already have the attribute `obj`, - # we can update it rather than setting it - self.obj = self.rtn.obj.optz - for key, val in self.rtn.exprs.items(): - val.evaluate() + self._evaluate_params() + self._evaluate_vars() + self._evaluate_constrs() + self._evaluate_obj() + self._evaluate_exprs() self.evaluated = True _, s = elapsed(t) @@ -958,7 +993,8 @@ def finalize(self): for pattern, replacement in self.rtn.syms.sub_map.items(): code_prob = re.sub(pattern, replacement, code_prob) - exec(code_prob, globals(), locals()) + local_vars = {} + self.prob = eval(code_prob, globals(), local_vars) _, s = elapsed(t) logger.debug(f" -> Finalized in {s}") self.evaluated = True @@ -1026,9 +1062,9 @@ def _register_attribute(self, 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 __setattr__(self, name: str, value: Any): + super().__setattr__(name, value) + self._register_attribute(name, value) def update(self, params): """ From debad484628b23c1db690ba514f7c358ae4fc623 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 2 Nov 2024 10:38:48 -0400 Subject: [PATCH 102/181] [WIP] Refactor OModel to have an evaluate method, switch to using eval from exec, known to work for Routine.run --- ams/opt/omodel.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index ed219295..aa89cfb4 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -144,7 +144,7 @@ def evaluate(self): """ logger.debug(f" - Expression <{self.name}>: {self.code}") local_vars = {'self': self} - self.optz = eval(self.code, globals(), local_vars) + self.optz = eval(self.code, {}, local_vars) return True @property @@ -262,8 +262,8 @@ def evaluate(self): if self.sparse: if not spr.issparse(self.v): val = "sps.csr_matrix(self.v)" - local_vars = {'self': self, 'config': config, 'sps': sps} - self.optz = eval(val, globals(), local_vars) + local_vars = {'self': self, 'config': config, 'sps': sps, 'cp': cp} + self.optz = eval(val, {}, local_vars) except ValueError: msg = f"Parameter <{self.name}> has non-numeric value, " msg += "set `no_parse=True`." @@ -499,8 +499,8 @@ def evaluate(self): config[k] = v logger.debug(f" - Var <{self.name}>: {self.code}") try: - local_vars = {'self': self, 'config': config} - self.optz = eval(self.code, globals(), local_vars) + local_vars = {'self': self, 'config': config, 'cp': cp} + self.optz = eval(self.code, {}, local_vars) except Exception as e: logger.error(f"Error in evaluating var <{self.name}>.") logger.error(f"Original error: {e}") @@ -582,8 +582,8 @@ def evaluate(self): """ logger.debug(f" - Constr <{self.name}>: {self.code}") try: - local_vars = {'self': self} - self.optz = eval(self.code, globals(), local_vars) + local_vars = {'self': self, 'cp': cp} + self.optz = eval(self.code, {}, local_vars) except Exception as e: logger.error(f"Error in evaluating constr <{self.name}>.") logger.error(f"Original error: {e}") @@ -768,7 +768,8 @@ def evaluate(self): Returns True if the evaluation is successful, False otherwise. """ logger.debug(f" - Objective <{self.name}>: {self.e_str}") - self.optz = eval(self.code, globals(), locals()) + local_vars = {'self': self, 'cp': cp} + self.optz = eval(self.code, {}, local_vars) return True def __repr__(self): @@ -981,7 +982,7 @@ def finalize(self): """ logger.debug(f"Finalizing OModel for <{self.rtn.class_name}>") t, _ = elapsed() - code_prob = "self.prob = problem(self.obj, " + code_prob = "problem(self.obj, " constrs_skip = [] constrs_add = [] for key, val in self.rtn.constrs.items(): @@ -993,8 +994,8 @@ def finalize(self): for pattern, replacement in self.rtn.syms.sub_map.items(): code_prob = re.sub(pattern, replacement, code_prob) - local_vars = {} - self.prob = eval(code_prob, globals(), local_vars) + local_vars = {'self': self, 'constrs_add': constrs_add, 'cp': cp} + self.prob = eval(code_prob, {}, local_vars) _, s = elapsed(t) logger.debug(f" -> Finalized in {s}") self.evaluated = True From f09414ed6c536777051e7890c043eb2de9280f70 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 2 Nov 2024 11:29:39 -0400 Subject: [PATCH 103/181] [WIP] Refactor OModel to have an evaluate method, improve logging, known to work for Routine.run --- ams/opt/omodel.py | 34 ++++++++++++++++++++++------------ ams/utils/__init__.py | 26 ++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index aa89cfb4..82630d08 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -15,10 +15,14 @@ import cvxpy as cp +from ams.utils import pretty_long_message from ams.shared import sps # NOQA logger = logging.getLogger(__name__) +_prefix = r" - --------------> | " +_max_length = 80 + class OptzBase: """ @@ -136,13 +140,16 @@ def parse(self): raise e # store the parsed expression str code self.code = code_expr + msg = f" - ExpressionCalc <{self.name}>: {self.e_str}" + logger.debug(pretty_long_message(msg, _prefix, max_length=_max_length)) return True def evaluate(self): """ Evaluate the expression. """ - logger.debug(f" - Expression <{self.name}>: {self.code}") + msg = f" - Expression <{self.name}>: {self.code}" + logger.debug(pretty_long_message(msg, _prefix, max_length=_max_length)) local_vars = {'self': self} self.optz = eval(self.code, {}, local_vars) return True @@ -497,7 +504,8 @@ def evaluate(self): config['boolean'] = v else: config[k] = v - logger.debug(f" - Var <{self.name}>: {self.code}") + msg = f" - Var <{self.name}>: {self.code}" + logger.debug(pretty_long_message(msg, _prefix, max_length=_max_length)) try: local_vars = {'self': self, 'config': config, 'cp': cp} self.optz = eval(self.code, {}, local_vars) @@ -574,13 +582,16 @@ def parse(self): code_constr += " == 0" if self.is_eq else " <= 0" # store the parsed expression str code self.code = code_constr + msg = f" - Constr <{self.name}>: {self.e_str}" + logger.debug(pretty_long_message(msg, _prefix, max_length=_max_length)) return True def evaluate(self): """ Evaluate the constraint. """ - logger.debug(f" - Constr <{self.name}>: {self.code}") + msg = f" - Constr <{self.name}>: {self.code}" + logger.debug(pretty_long_message(msg, _prefix, max_length=_max_length)) try: local_vars = {'self': self, 'cp': cp} self.optz = eval(self.code, {}, local_vars) @@ -756,6 +767,8 @@ def parse(self): raise ValueError(f'Objective sense {self.sense} is not supported.') sense = 'cp.Minimize' if self.sense == 'min' else 'cp.Maximize' self.code = f"{sense}({code_obj})" + msg = f" - Objective <{self.name}>: {self.code}" + logger.debug(pretty_long_message(msg, _prefix, max_length=_max_length)) return True def evaluate(self): @@ -767,7 +780,7 @@ def evaluate(self): bool Returns True if the evaluation is successful, False otherwise. """ - logger.debug(f" - Objective <{self.name}>: {self.e_str}") + logger.debug(f" - Objective <{self.name}>: {self.e_str}") local_vars = {'self': self, 'cp': cp} self.optz = eval(self.code, {}, local_vars) return True @@ -833,10 +846,10 @@ def parse(self): """ t, _ = elapsed() # --- add RParams and Services as parameters --- - logger.debug(f'Parsing OModel for <{self.rtn.class_name}>') + logger.warning(f'Parsing OModel for <{self.rtn.class_name}>') for key, val in self.rtn.params.items(): if not val.no_parse: - logger.debug(f" - Param <{key}>") + logger.debug(f" - Param <{key}>") try: val.parse() except Exception as e: @@ -846,7 +859,7 @@ def parse(self): # --- add decision variables --- for key, val in self.rtn.vars.items(): - logger.debug(f" - Var <{key}>") + logger.debug(f" - Var <{key}>") try: val.parse() except Exception as e: @@ -856,7 +869,6 @@ def parse(self): # --- add constraints --- for key, val in self.rtn.constrs.items(): - logger.debug(f" - Constr <{key}>: {val.e_str}") try: val.parse() except Exception as e: @@ -866,7 +878,6 @@ def parse(self): # --- parse objective functions --- if self.rtn.type != 'PF': - logger.debug(f" - Objective <{self.rtn.obj.name}>: {self.rtn.obj.e_str}") if self.rtn.obj is not None: try: self.rtn.obj.parse() @@ -881,7 +892,6 @@ def parse(self): # --- parse expressions --- for key, val in self.rtn.exprs.items(): - logger.debug(f" - ExpressionCalc <{key}>: {val.e_str}") try: val.parse() except Exception as e: @@ -953,7 +963,7 @@ def evaluate(self): bool Returns True if the evaluation is successful, False otherwise. """ - logger.debug(f"Evaluating OModel for <{self.rtn.class_name}>") + logger.warning(f"Evaluating OModel for <{self.rtn.class_name}>") t, _ = elapsed() if not self.parsed: raise ValueError("Model is not parsed yet.") @@ -980,7 +990,7 @@ def finalize(self): bool Returns True if the finalization is successful, False otherwise. """ - logger.debug(f"Finalizing OModel for <{self.rtn.class_name}>") + logger.warning(f"Finalizing OModel for <{self.rtn.class_name}>") t, _ = elapsed() code_prob = "problem(self.obj, " constrs_skip = [] diff --git a/ams/utils/__init__.py b/ams/utils/__init__.py index 99609706..a6877af8 100644 --- a/ams/utils/__init__.py +++ b/ams/utils/__init__.py @@ -31,3 +31,29 @@ def create_entry(*fields, three_params=True): """ base_fields = ['idx', 'u', 'name'] if three_params else [] return base_fields + list(fields) + + +def pretty_long_message(message, prefix="", max_length=80): + """ + Pretty print a long message. + + Parameters + ---------- + message : str + The message to format. + prefix : str, optional + A prefix to add to each line of the message. + max_length : int, optional + The maximum length of each line. + + Returns + ------- + str + The formatted message. + """ + if len(message) <= max_length: + return message + else: + lines = [message[i:i+max_length] for i in range(0, len(message), max_length)] + formatted_message = lines[0] + "\n" + "\n".join([prefix + line for line in lines[1:]]) + return formatted_message From 1bf73fcdf44de3540dc5e908f77fd9247ddbf974 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 2 Nov 2024 11:32:53 -0400 Subject: [PATCH 104/181] Improve logging --- ams/core/matprocessor.py | 3 ++- ams/routines/routine.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ams/core/matprocessor.py b/ams/core/matprocessor.py index 4f34ce25..ed8d551c 100644 --- a/ams/core/matprocessor.py +++ b/ams/core/matprocessor.py @@ -207,6 +207,7 @@ def build(self, force_mats=False): return self.initialized t_mat, _ = elapsed() + logger.debug("Entering system matrix building") # --- connectivity matrices --- _ = self.build_cg() _ = self.build_cl() @@ -220,7 +221,7 @@ def build(self, force_mats=False): _ = self.build_pbusinj() _, s_mat = elapsed(t_mat) - logger.debug(f"Built system matrices in {s_mat}.") + logger.debug(f" -> System matrices built in {s_mat}") self.initialized = True return self.initialized diff --git a/ams/routines/routine.py b/ams/routines/routine.py index 19ee9f8a..697da6bf 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -213,7 +213,7 @@ def _data_check(self, info=True): info: bool Whether to print warning messages. """ - logger.debug(f"Entering data check for <{self.class_name}>.") + logger.debug(f"Entering data check for <{self.class_name}>") no_input = [] owner_list = [] for rname, rparam in self.rparams.items(): @@ -234,7 +234,7 @@ def _data_check(self, info=True): logger.error(msg) return False # TODO: add data validation for RParam, typical range, etc. - logger.debug(" - Data check passed") + logger.debug(" -> Data check passed") return True def init(self, no_code=True, force_init=False, From cf160dcb30177c91b711bc88bb4bd13ad7dc99e2 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 2 Nov 2024 13:04:20 -0400 Subject: [PATCH 105/181] [WIP] Refactor OModel to have an evaluate method, improve try-error in Optz.evaluate and Optz.parse, known to work for Routine.run --- ams/opt/omodel.py | 194 +++++++++++++++++++++++++--------------------- 1 file changed, 105 insertions(+), 89 deletions(-) diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index 82630d08..088a5458 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -135,9 +135,8 @@ def parse(self): for pattern, replacement in sub_map.items(): try: code_expr = re.sub(pattern, replacement, code_expr) - except TypeError as e: - logger.error(f"Error in parsing expr <{self.name}>.") - raise e + except Exception as e: + raise Exception(f"Error in parsing expr <{self.name}>.\n{e}") # store the parsed expression str code self.code = code_expr msg = f" - ExpressionCalc <{self.name}>: {self.e_str}" @@ -150,8 +149,11 @@ def evaluate(self): """ msg = f" - Expression <{self.name}>: {self.code}" logger.debug(pretty_long_message(msg, _prefix, max_length=_max_length)) - local_vars = {'self': self} - self.optz = eval(self.code, {}, local_vars) + try: + local_vars = {'self': self} + self.optz = eval(self.code, {}, local_vars) + except Exception as e: + raise Exception(f"Error in evaluating expr <{self.name}>.\n{e}") return True @property @@ -253,11 +255,17 @@ def parse(self): # NOTE: it seems that there is no need to use re.sub here code_param = f"param(shape={shape}, **config)" for pattern, replacement, in sub_map.items(): - code_param = re.sub(pattern, replacement, code_param) + try: + code_param = re.sub(pattern, replacement, code_param) + except Exception as e: + raise Exception(f"Error in parsing param <{self.name}>.\n{e}") self.code = code_param return True def evaluate(self): + """ + Evaluate the parameter. + """ if self.no_parse: return True @@ -276,11 +284,9 @@ def evaluate(self): msg += "set `no_parse=True`." logger.warning(msg) self.no_parse = True - return False + return True except Exception as e: - logger.error(f"Error in evaluating param <{self.name}>.") - logger.error(f"Original error: {e}") - return False + raise Exception(f"Error in evaluating param <{self.name}>.\n{e}") return True def update(self): @@ -479,8 +485,12 @@ def parse(self): else: raise ValueError(f"Invalid shape {self._shape}.") code_var = f"var({shape}, **config)" + logger.debug(f" - Var <{self.name}>: {self.code}") for pattern, replacement, in sub_map.items(): - code_var = re.sub(pattern, replacement, code_var) + try: + code_var = re.sub(pattern, replacement, code_var) + except Exception as e: + raise Exception(f"Error in parsing var <{self.name}>.\n{e}") self.code = code_var return True @@ -510,9 +520,7 @@ def evaluate(self): local_vars = {'self': self, 'config': config, 'cp': cp} self.optz = eval(self.code, {}, local_vars) except Exception as e: - logger.error(f"Error in evaluating var <{self.name}>.") - logger.error(f"Original error: {e}") - return False + raise Exception(f"Error in evaluating var <{self.name}>.\n{e}") return True def __repr__(self): @@ -576,8 +584,7 @@ def parse(self): try: code_constr = re.sub(pattern, replacement, code_constr) except TypeError as e: - logger.error(f"Error in parsing constr <{self.name}>.") - raise e + raise TypeError(f"Error in parsing constr <{self.name}>.\n{e}") # parse the constraint type code_constr += " == 0" if self.is_eq else " <= 0" # store the parsed expression str code @@ -593,12 +600,10 @@ def evaluate(self): msg = f" - Constr <{self.name}>: {self.code}" logger.debug(pretty_long_message(msg, _prefix, max_length=_max_length)) try: - local_vars = {'self': self, 'cp': cp} + local_vars = {'self': self, 'cp': cp, 'sub_map': self.om.rtn.syms.val_map} self.optz = eval(self.code, {}, local_vars) except Exception as e: - logger.error(f"Error in evaluating constr <{self.name}>.") - logger.error(f"Original error: {e}") - return False + raise Exception(f"Error in evaluating constr <{self.name}>.\n{e}") def __repr__(self): enabled = 'OFF' if self.is_disabled else 'ON' @@ -626,16 +631,14 @@ def e(self): try: code = re.sub(pattern, replacement, code) except TypeError as e: - logger.error(f"Error in parsing value for constr <{self.name}>.") - raise e + raise TypeError(e) try: logger.debug(f"Value code: {code}") - return eval(code) + local_vars = {'self': self, 'cp': cp, 'val_map': val_map} + return eval(code, {}, local_vars) except Exception as e: - logger.error(f"Error in calculating constr <{self.name}>.") - logger.error(f"Original error: {e}") - return None + return Exception(f"Error in calculating constr <{self.name}>.\n{e}") @property def v(self): @@ -760,7 +763,10 @@ def parse(self): sub_map = self.om.rtn.syms.sub_map code_obj = self.e_str for pattern, replacement, in sub_map.items(): - code_obj = re.sub(pattern, replacement, code_obj) + try: + code_obj = re.sub(pattern, replacement, code_obj) + except Exception as e: + raise Exception(f"Error in parsing obj <{self.name}>.\n{e}") # store the parsed expression str code self.code = code_obj if self.sense not in ['min', 'max']: @@ -781,8 +787,11 @@ def evaluate(self): Returns True if the evaluation is successful, False otherwise. """ logger.debug(f" - Objective <{self.name}>: {self.e_str}") - local_vars = {'self': self, 'cp': cp} - self.optz = eval(self.code, {}, local_vars) + try: + local_vars = {'self': self, 'cp': cp} + self.optz = eval(self.code, {}, local_vars) + except Exception as e: + raise Exception(f"Error in evaluating obj <{self.name}>.\n{e}") return True def __repr__(self): @@ -830,7 +839,7 @@ def __init__(self, routine): self.evaluated = False self.finalized = False - def parse(self): + def parse(self, force=False): """ Parse the optimization model from the symbolic description. @@ -839,42 +848,33 @@ def parse(self): of the optimization model: parameters, decision variables, constraints, objective function, and expressions. + Parameters + ---------- + force : bool, optional + Flag indicating if to force the parsing. + Returns ------- bool Returns True if the parsing is successful, False otherwise. """ + if self.parsed and not force: + logger.warning("Model is already parsed.") + return self.parsed t, _ = elapsed() # --- add RParams and Services as parameters --- logger.warning(f'Parsing OModel for <{self.rtn.class_name}>') for key, val in self.rtn.params.items(): if not val.no_parse: - logger.debug(f" - Param <{key}>") - try: - val.parse() - except Exception as e: - msg = f"Failed to parse Param <{key}>. " - msg += f"Original error: {e}" - raise Exception(msg) + val.parse() # --- add decision variables --- for key, val in self.rtn.vars.items(): - logger.debug(f" - Var <{key}>") - try: - val.parse() - except Exception as e: - msg = f"Failed to parse Var <{key}>. " - msg += f"Original error: {e}" - raise Exception(msg) + val.parse() # --- add constraints --- for key, val in self.rtn.constrs.items(): - try: - val.parse() - except Exception as e: - msg = f"Failed to parse Constr <{key}>. " - msg += f"Original error: {e}" - raise Exception(msg) + val.parse() # --- parse objective functions --- if self.rtn.type != 'PF': @@ -882,9 +882,7 @@ def parse(self): try: self.rtn.obj.parse() except Exception as e: - msg = f"Failed to parse Objective <{self.rtn.obj.name}>. " - msg += f"Original error: {e}" - raise Exception(msg) + raise Exception(f"Failed to parse Objective <{self.rtn.obj.name}>.\n{e}") else: logger.warning(f"{self.rtn.class_name} has no objective function!") self.parsed = False @@ -895,9 +893,7 @@ def parse(self): try: val.parse() except Exception as e: - msg = f"Failed to parse ExpressionCalc <{key}>." - msg += f"Original error: {e}" - raise Exception(msg) + raise Exception(f"Failed to parse ExpressionCalc <{key}>.\n{e}") _, s = elapsed(t) logger.debug(f" -> Parsed in {s}") @@ -909,27 +905,33 @@ def _evaluate_params(self): Evaluate the parameters. """ for key, val in self.rtn.params.items(): - val.evaluate() - setattr(self, key, val.optz) - return True + try: + val.evaluate() + setattr(self, key, val.optz) + except Exception as e: + raise Exception(f"Failed to evaluate Param <{key}>.\n{e}") def _evaluate_vars(self): """ Evaluate the decision variables. """ for key, val in self.rtn.vars.items(): - val.evaluate() - setattr(self, key, val.optz) - return True + try: + val.evaluate() + setattr(self, key, val.optz) + except Exception as e: + raise Exception(f"Failed to evaluate Var <{key}>.\n{e}") def _evaluate_constrs(self): """ Evaluate the constraints. """ for key, val in self.rtn.constrs.items(): - val.evaluate() - setattr(self, key, val.optz) - return True + try: + val.evaluate() + setattr(self, key, val.optz) + except Exception as e: + raise Exception(f"Failed to evaluate Constr <{key}>.\n{e}") def _evaluate_obj(self): """ @@ -940,17 +942,18 @@ def _evaluate_obj(self): if self.rtn.type != 'PF': self.rtn.obj.evaluate() self.obj = self.rtn.obj.optz - return True def _evaluate_exprs(self): """ Evaluate the expressions. """ for key, val in self.rtn.exprs.items(): - val.evaluate() - return True + try: + val.evaluate() + except Exception as e: + raise Exception(f"Failed to evaluate ExpressionCalc <{key}>.\n{e}") - def evaluate(self): + def evaluate(self, force=False): """ Evaluate the optimization model. @@ -958,15 +961,22 @@ def evaluate(self): components of the optimization model: parameters, decision variables, constraints, objective function, and expressions. + Parameters + ---------- + force : bool, optional + Flag indicating if to force the evaluation + Returns ------- bool Returns True if the evaluation is successful, False otherwise. """ + if self.evaluated and not force: + logger.warning("Model is already evaluated.") + return self.evaluated logger.warning(f"Evaluating OModel for <{self.rtn.class_name}>") t, _ = elapsed() - if not self.parsed: - raise ValueError("Model is not parsed yet.") + self._evaluate_params() self._evaluate_vars() self._evaluate_constrs() @@ -978,7 +988,7 @@ def evaluate(self): logger.debug(f" -> Evaluated in {s}") return self.evaluated - def finalize(self): + def finalize(self, force=False): """ Finalize the optimization model. @@ -990,6 +1000,9 @@ def finalize(self): bool Returns True if the finalization is successful, False otherwise. """ + if self.finalized and not force: + logger.warning("Model is already finalized.") + return self.finalized logger.warning(f"Finalizing OModel for <{self.rtn.class_name}>") t, _ = elapsed() code_prob = "problem(self.obj, " @@ -1008,10 +1021,10 @@ def finalize(self): self.prob = eval(code_prob, {}, local_vars) _, s = elapsed(t) logger.debug(f" -> Finalized in {s}") - self.evaluated = True - return self.evaluated + self.finalized = True + return self.finalized - def init(self, force_parse=False, force_generate=False): + def init(self, force=False): """ Set up the optimization model from the symbolic description. @@ -1020,35 +1033,38 @@ def init(self, force_parse=False, force_generate=False): Parameters ---------- - force_parse : bool, optional - Flag indicating if the parsing should be forced, goes to `self.parse()`. - force_generate : bool, optional - Flag indicating if the symbols should be generated, goes to `self.parse()`. + force : bool, optional + Flag indicating if to force the OModel initialization. + If True, following methods will be called by force: `self.parse()`, + `self.evaluate()`, `self.finalize()` Returns ------- bool Returns True if the setup is successful, False otherwise. """ - t_init, _ = elapsed() + if self.initialized and not force: + logger.warning("OModel is already initialized.") + return self.initialized + + t, _ = elapsed() - if force_parse or not self.parsed: - self.parse(force_generate=force_generate) + self.parse(force=force) + # NOTE: Hardcoded for using PYPOWER as the PF type routines if self.rtn.type == 'PF': - _, s_init = elapsed(t_init) + _, s = elapsed(t) self.initialized = True - logger.debug(f"OModel for <{self.rtn.class_name}> initialized in {s_init}.") + logger.debug(f"OModel for <{self.rtn.class_name}> initialized in {s}") return self.initialized - self.evaluate() + self.evaluate(force=force) - # --- finalize the optimziation --- - self.finalize() + self.finalize(force=force) - _, s_init = elapsed(t_init) + _, s = elapsed(t) self.initialized = True - logger.debug(f"OModel for <{self.rtn.class_name}> initialized in {s_init}.") + logger.debug(f"OModel for <{self.rtn.class_name}> initialized in {s}") return self.initialized From 4b8c8207d7713addff2f083d628e2dd5757f0844 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 3 Nov 2024 10:34:06 -0500 Subject: [PATCH 106/181] Add a wrapper to ensure symbol generation before parse, in module omodel --- ams/opt/omodel.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index 088a5458..785bc51c 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -24,6 +24,29 @@ _max_length = 80 +def ensure_symbols(func): + """ + Decorator to ensure that symbols are generated before parsing. + If not, it runs self.rtn.syms.generate_symbols(). + + Designed to be used on the `parse` method of the optimization elements (`OptzBase`) + and optimization model (`OModel`), i.e., `Var`, `Param`, `Constraint`, `Objective`, + and `ExpressionCalc`. + + Note: + ----- + Parsing before symbol generation can give wrong results. Ensure that symbols + are generated before calling the `parse` method. + """ + + def wrapper(self, *args, **kwargs): + if not self.rtn._syms: + logger.debug(f"<{self.rtn.class_name}> symbols are not generated yet.") + self.rtn.syms.generate_symbols() + return func(self, *args, **kwargs) + return wrapper + + class OptzBase: """ Base class for optimization elements, e.g., Var and Constraint. @@ -39,6 +62,11 @@ class OptzBase: ---------- rtn : ams.routines.Routine The owner routine instance. + + Note: + ----- + Ensure that symbols are generated before calling the `parse` method. Parsing + before symbol generation can give wrong results. """ def __init__(self, @@ -55,6 +83,7 @@ def __init__(self, self.optz = None # corresponding optimization element self.code = None + @ensure_symbols def parse(self): """ Parse the object. @@ -125,6 +154,7 @@ def __init__(self, self.e_str = e_str self.code = None + @ensure_symbols def parse(self): """ Parse the Expression. @@ -241,6 +271,7 @@ def __init__(self, ('neg', neg), ))) + @ensure_symbols def parse(self): """ Parse the parameter. @@ -459,6 +490,7 @@ def get_idx(self): else: return self.owner.idx.v + @ensure_symbols def parse(self): """ Parse the variable. @@ -573,6 +605,7 @@ def __init__(self, self.dual = None self.code = None + @ensure_symbols def parse(self): """ Parse the constraint. @@ -750,6 +783,7 @@ def v(self): def v(self, value): raise AttributeError("Cannot set the value of the objective function.") + @ensure_symbols def parse(self): """ Parse the objective function. @@ -839,6 +873,7 @@ def __init__(self, routine): self.evaluated = False self.finalized = False + @ensure_symbols def parse(self, force=False): """ Parse the optimization model from the symbolic description. From aeb19d18ec3a24b1f4e99d841853ec91af3cf27c Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 3 Nov 2024 10:58:11 -0500 Subject: [PATCH 107/181] Add a wrapper to ensure mats built and omodel parsed before evaluate, in module omodel --- ams/opt/omodel.py | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index 785bc51c..9fa4fa20 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -41,12 +41,46 @@ def ensure_symbols(func): def wrapper(self, *args, **kwargs): if not self.rtn._syms: - logger.debug(f"<{self.rtn.class_name}> symbols are not generated yet.") + logger.debug(f"<{self.rtn.class_name}> symbols are not generated yet. Generating now...") self.rtn.syms.generate_symbols() return func(self, *args, **kwargs) return wrapper +def ensure_mats_and_parsed(func): + """ + Decorator to ensure that system matrices are built and OModel is parsed + before evaluation. If not, it runs the necessary methods to initialize them. + + Designed to be used on the `evaluate` method of the optimization elements (`OptzBase`) + and optimization model (`OModel`), i.e., `Var`, `Param`, `Constraint`, `Objective`, + and `ExpressionCalc`. + + Note: + ----- + Evaluation before matrices building and parsing can run into errors. Ensure that + system matrices are built and OModel is parsed before calling the `evaluate` method. + """ + def wrapper(self, *args, **kwargs): + try: + if not self.rtn.system.mats.initialized: + logger.debug("System matrices are not built yet. Building now...") + self.rtn.system.mats.build() + if isinstance(self, (OptzBase, Var, Param, Constraint, Objective)): + if not self.om.parsed: + logger.debug("OModel is not parsed yet. Parsing now...") + self.om.parse() + elif isinstance(self, OModel): + if not self.parsed: + logger.debug("OModel is not parsed yet. Parsing now...") + self.parse() + except Exception as e: + logger.error(f"Error during initialization or parsing: {e}") + raise + return func(self, *args, **kwargs) + return wrapper + + class OptzBase: """ Base class for optimization elements, e.g., Var and Constraint. @@ -90,6 +124,7 @@ def parse(self): """ raise NotImplementedError + @ensure_mats_and_parsed def evaluate(self): """ Evaluate the object. @@ -173,6 +208,7 @@ def parse(self): logger.debug(pretty_long_message(msg, _prefix, max_length=_max_length)) return True + @ensure_mats_and_parsed def evaluate(self): """ Evaluate the expression. @@ -293,6 +329,7 @@ def parse(self): self.code = code_param return True + @ensure_mats_and_parsed def evaluate(self): """ Evaluate the parameter. @@ -526,6 +563,7 @@ def parse(self): self.code = code_var return True + @ensure_mats_and_parsed def evaluate(self): """ Evaluate the variable. @@ -626,6 +664,7 @@ def parse(self): logger.debug(pretty_long_message(msg, _prefix, max_length=_max_length)) return True + @ensure_mats_and_parsed def evaluate(self): """ Evaluate the constraint. @@ -811,6 +850,7 @@ def parse(self): logger.debug(pretty_long_message(msg, _prefix, max_length=_max_length)) return True + @ensure_mats_and_parsed def evaluate(self): """ Evaluate the objective function. @@ -988,6 +1028,7 @@ def _evaluate_exprs(self): except Exception as e: raise Exception(f"Failed to evaluate ExpressionCalc <{key}>.\n{e}") + @ensure_mats_and_parsed def evaluate(self, force=False): """ Evaluate the optimization model. From 05b74d6d9dda0547e86eb8d49201d3282bf22f50 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 3 Nov 2024 12:03:36 -0500 Subject: [PATCH 108/181] Rename parameter name force_mats as force of method MatProcessor.build --- ams/core/matprocessor.py | 9 +++++++-- ams/routines/routine.py | 21 ++++++++------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/ams/core/matprocessor.py b/ams/core/matprocessor.py index ed8d551c..9daca3a3 100644 --- a/ams/core/matprocessor.py +++ b/ams/core/matprocessor.py @@ -186,12 +186,17 @@ def __init__(self, system): info='Line outage distribution factor', v=None, sparse=False, owner=self) - def build(self, force_mats=False): + def build(self, force=False): """ Build the system matrices. It build connectivity matrices first: Cg, Cl, Csh, Cft, and CftT. Then build bus matrices: Bf, Bbus, Pfinj, and Pbusinj. + Parameters + ---------- + force : bool, optional + If True, force to rebuild the matrices. Default is False. + Notes ----- Generator online status is NOT considered in its connectivity matrix. @@ -202,7 +207,7 @@ def build(self, force_mats=False): initialized : bool True if the matrices are built successfully. """ - if not force_mats and self.initialized: + if not force and self.initialized: logger.debug("System matrices are already built.") return self.initialized diff --git a/ams/routines/routine.py b/ams/routines/routine.py index 697da6bf..bd753cea 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -237,28 +237,23 @@ def _data_check(self, info=True): logger.debug(" -> Data check passed") return True - def init(self, no_code=True, force_init=False, - force_mats=False, force_constr=False, - force_parse=False, force_generate=False): + def init(self, force_init=False, force_mats=False, + force_constr=False, force_om=False): """ Initialize the routine. - Parameters + Other parameters ---------- - no_code: bool - Whether to show generated code. force_init: bool Whether to force re-initialization, will ignore `self.initialized`. force_mats: bool Whether to force build the system matrices, goes to `self.system.mats.build()`. force_constr: bool Whether to turn on all constraints. - force_parse: bool - Whether to force parse the optimization model, goes to `self.om.init()`. - force_generate: bool - Whether to force generate symbols, goes to `self.om.init()`. + force_om: bool + Whether to force initialize the optimization model. """ - skip_all = not (force_init and force_mats and force_parse and force_generate) and self.initialized + skip_all = not (force_init and force_mats) and self.initialized if skip_all: logger.debug(f"{self.class_name} has already been initialized.") @@ -274,13 +269,13 @@ def init(self, no_code=True, force_init=False, constr.is_disabled = False # --- matrix build --- - self.system.mats.build(force_mats=force_mats) + self.system.mats.build(force=force_mats) # --- constraint check --- _ = self._get_off_constrs() if not self.om.initialized: - self.om.init() + self.om.init(force=force_om) _, s_init = elapsed(t0) msg = f"<{self.class_name}> " From a5761cdbe142e4aff3d73252c39a8a4b4c12bc88 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 3 Nov 2024 15:28:02 -0500 Subject: [PATCH 109/181] Fix property methods e of classes Constraint and Objective in module omodel --- ams/core/symprocessor.py | 2 ++ ams/opt/omodel.py | 17 ++++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/ams/core/symprocessor.py b/ams/core/symprocessor.py index dda7205e..c1bc280e 100644 --- a/ams/core/symprocessor.py +++ b/ams/core/symprocessor.py @@ -86,6 +86,8 @@ def __init__(self, parent): # mapping dict for evaluating expressions self.val_map = OrderedDict([ + (r'(== 0|<= 0)$', ''), # remove the comparison operator + (r'cp\.(Minimize|Maximize)', r'float'), # remove cp.Minimize/Maximize (r'\bcp.\b', 'np.'), ]) diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index 9fa4fa20..2efd0929 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -706,11 +706,13 @@ def e(self): raise TypeError(e) try: - logger.debug(f"Value code: {code}") - local_vars = {'self': self, 'cp': cp, 'val_map': val_map} + logger.debug(pretty_long_message(f"Value code: {code}", + _prefix, max_length=_max_length)) + local_vars = {'self': self, 'np': np, 'cp': cp, 'val_map': val_map} return eval(code, {}, local_vars) except Exception as e: - return Exception(f"Error in calculating constr <{self.name}>.\n{e}") + logger.error(f"Error in calculating constr <{self.name}>.\n{e}") + return None @property def v(self): @@ -801,11 +803,12 @@ def e(self): raise e try: - logger.debug(f"Value code: {code}") - return eval(code) + logger.debug(pretty_long_message(f"Value code: {code}", + _prefix, max_length=_max_length)) + local_vars = {'self': self, 'np': np, 'cp': cp, 'val_map': val_map} + return eval(code, {}, local_vars) except Exception as e: - logger.error(f"Error in calculating obj <{self.name}>.") - logger.error(f"Original error: {e}") + logger.error(f"Error in calculating obj <{self.name}>.\n{e}") return None @property From 3f5c6cc4e81b9bf9e5ab9987793cb841459381a4 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 3 Nov 2024 22:12:41 -0500 Subject: [PATCH 110/181] Fix kwargs in RoutineBase methods init and run --- ams/routines/dcopf.py | 38 ++++++++-------------- ams/routines/routine.py | 71 +++++++++++++++++++++-------------------- 2 files changed, 49 insertions(+), 60 deletions(-) diff --git a/ams/routines/dcopf.py b/ams/routines/dcopf.py index b4e84bc1..fd267a72 100644 --- a/ams/routines/dcopf.py +++ b/ams/routines/dcopf.py @@ -225,39 +225,32 @@ def __init__(self, system, config): info='total cost', unit='$', sense='min', e_str=obj,) - def solve(self, *args, **kwargs): + def solve(self, **kwargs): """ Solve the routine optimization model. *args and **kwargs go to `self.om.prob.solve()` (`cvxpy.Problem.solve()`). """ - return self.om.prob.solve(*args, **kwargs) + return self.om.prob.solve(**kwargs) - def run(self, - no_code=True, force_init=False, - force_mats=False, force_constr=False, - force_parse=False, force_generate=False, - *args, **kwargs): + def run(self, **kwargs): """ Run the routine. - *args and **kwargs go to `self.solve()`. + + Following kwargs go to `self.init()`: `force`, `force_mats`, `force_constr`, `force_om`. + + Following kwargs go to `self.solve()`: `solver`, `verbose`, `gp`, `qcp`, `requires_grad`, + `enforce_dpp`, `ignore_dpp`, `method`, and all rest. Parameters ---------- - no_code : bool, optional - If True, print the generated CVXPY code. Defaults to False. - force_init : bool, optional + force : bool, optional If True, force re-initialization. Defaults to False. force_mats : bool, optional If True, force re-generating matrices. Defaults to False. force_constr : bool, optional - If True, force re-generating constraints. Defaults to False. - force_parse : bool, optional - If True, force re-parsing the model. Defaults to False. - force_generate : bool, optional - If True, force re-generating the model. Defaults to False. - - Other Parameters - ---------------- + Whether to turn on all constraints. + force_om : bool, optional + If True, force re-generating optimization model. Defaults to False. solver: str, optional The solver to use. For example, 'GUROBI', 'ECOS', 'SCS', or 'OSQP'. verbose : bool, optional @@ -283,13 +276,8 @@ def run(self, 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. """ - return super().run(no_code=no_code, force_init=force_init, - force_mats=force_mats, force_constr=force_constr, - force_parse=force_parse, force_generate=force_generate, - *args, **kwargs) + return super().run(**kwargs) def _post_solve(self): """ diff --git a/ams/routines/routine.py b/ams/routines/routine.py index bd753cea..3087ef5b 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -237,15 +237,14 @@ def _data_check(self, info=True): logger.debug(" -> Data check passed") return True - def init(self, force_init=False, force_mats=False, - force_constr=False, force_om=False): + def init(self, **kwargs): """ Initialize the routine. Other parameters ---------- - force_init: bool - Whether to force re-initialization, will ignore `self.initialized`. + force: bool + Whether to force initialization regardless of the current initialization status. force_mats: bool Whether to force build the system matrices, goes to `self.system.mats.build()`. force_constr: bool @@ -253,7 +252,12 @@ def init(self, force_init=False, force_mats=False, force_om: bool Whether to force initialize the optimization model. """ - skip_all = not (force_init and force_mats) and self.initialized + force = kwargs.pop('force', False) + force_mats = kwargs.pop('force_mats', False) + force_constr = kwargs.pop('force_constr', False) + force_om = kwargs.pop('force_om', False) + + skip_all = not (force and force_mats) and self.initialized if skip_all: logger.debug(f"{self.class_name} has already been initialized.") @@ -292,25 +296,21 @@ def solve(self, **kwargs): """ Solve the routine optimization model. """ - return True + raise NotImplementedError def unpack(self, **kwargs): """ Unpack the results. """ - return None + raise NotImplementedError def _post_solve(self): """ Post-solve calculation. """ - return None + raise NotImplementedError - def run(self, - no_code=True, force_init=False, - force_mats=False, force_constr=False, - force_parse=False, force_generate=False, - *args, **kwargs): + def run(self, **kwargs): """ Run the routine. *args and **kwargs go to `self.solve()`. @@ -322,28 +322,26 @@ def run(self, Parameters ---------- - no_code: bool - Whether to show generated code. force_init: bool - Whether to force re-initialization, will ignore `self.initialized`. + Whether to force re-initialize the routine. force_mats: bool - Whether to force build the system matrices, goes to `self.init()`. + Whether to force build the system matrices. force_constr: bool - Whether to turn on all constraints, goes to `self.init()`. - force_parse: bool - Whether to force parse the optimization model, goes to `self.init()`. - force_generate: bool - Whether to force generate symbols, goes to `self.init()` - no_code: bool - Whether to show generated code. + Whether to turn on all constraints. + force_om: bool + Whether to force initialize the OModel. """ # --- setup check --- - self.init(no_code=no_code, force_init=force_init, - force_mats=force_mats, force_constr=force_constr, - force_parse=force_parse, force_generate=force_generate) + force_init = kwargs.pop('force_init', False) + force_mats = kwargs.pop('force_mats', False) + force_constr = kwargs.pop('force_constr', False) + force_om = kwargs.pop('force_om', False) + self.init(force=force_init, force_mats=force_mats, + force_constr=force_constr, force_om=force_om) + # --- solve optimization --- t0, _ = elapsed() - _ = self.solve(*args, **kwargs) + _ = self.solve(**kwargs) status = self.om.prob.status self.exit_code = self.syms.status[status] self.converged = self.exit_code == 0 @@ -528,7 +526,7 @@ def update(self, params=None, build_mats=True,): if no system matrices are changed. """ t0, _ = elapsed() - re_init = False + re_finalize = False # sanitize input sparams = [] if params is None: @@ -542,16 +540,19 @@ def update(self, params=None, build_mats=True,): sparams = [self.params[param] for param in params if isinstance(param, str)] for param in sparams: param.update() + for param in sparams: if param.optz is None: # means no_parse=True - re_init = True + re_finalize = True break - if build_mats: - self.system.mats.build() - if re_init: + + self.system.mats.build(force=build_mats) + + if re_finalize: logger.warning(f"<{self.class_name}> reinit OModel due to non-parametric change.") - self.om.parsed = False - _ = self.om.init(no_code=True) + self.om.evaluate(force=True) + self.om.finalize(force=True) + results = self.om.update(params=sparams) t0, s0 = elapsed(t0) logger.debug(f"Update params in {s0}.") From e8cddc68cabe33b05ef8ce335984eb53ee3875ff Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 3 Nov 2024 22:13:05 -0500 Subject: [PATCH 111/181] Fix RParam.evaluate --- ams/core/param.py | 1 - ams/opt/omodel.py | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ams/core/param.py b/ams/core/param.py index d07eb094..6f170a68 100644 --- a/ams/core/param.py +++ b/ams/core/param.py @@ -139,7 +139,6 @@ def __init__(self, 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 self._v = None # external value if v is not None: diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index 2efd0929..bb3862e7 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -337,7 +337,7 @@ def evaluate(self): if self.no_parse: return True - config = self.config.as_dict() # NOQA, used in `self.code` + config = self.config.as_dict() try: msg = f"Parameter <{self.name}> is set as sparse, " msg += "but the value is not sparse." @@ -346,7 +346,8 @@ def evaluate(self): if not spr.issparse(self.v): val = "sps.csr_matrix(self.v)" local_vars = {'self': self, 'config': config, 'sps': sps, 'cp': cp} - self.optz = eval(val, {}, local_vars) + self.optz = eval(self.code, {}, local_vars) # create the cp.Parameter as self.optz + self.optz.value = eval(val, {}, local_vars) # assign value to self.optz except ValueError: msg = f"Parameter <{self.name}> has non-numeric value, " msg += "set `no_parse=True`." From 21f7e75959c530fb3fcb8a0c4e72367bf47885fe Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 3 Nov 2024 22:15:09 -0500 Subject: [PATCH 112/181] Minor --- 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 bb3862e7..92ce1311 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -16,7 +16,7 @@ import cvxpy as cp from ams.utils import pretty_long_message -from ams.shared import sps # NOQA +from ams.shared import sps logger = logging.getLogger(__name__) From cb4df6c5341198a880b2ddc5871c37e6f645c907 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 3 Nov 2024 22:21:43 -0500 Subject: [PATCH 113/181] In RTED.dc2ac, evaluate the var vBus after add and parse it --- ams/routines/rted.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ams/routines/rted.py b/ams/routines/rted.py index 096d8c06..8b5eb391 100644 --- a/ams/routines/rted.py +++ b/ams/routines/rted.py @@ -227,6 +227,7 @@ def dc2ac(self, kloss=1.0, **kwargs): info='Bus voltage', unit='p.u.', model='Bus', src='v',) self.vBus.parse() + self.vBus.evaluate() self.vBus.optz.value = ACOPF.vBus.v self.aBus.optz.value = ACOPF.aBus.v self.exec_time = exec_time From bd0e975211210567cee74b008e3649f5e5619427 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 3 Nov 2024 22:27:32 -0500 Subject: [PATCH 114/181] Fix UC.init kwargs --- ams/routines/uc.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/ams/routines/uc.py b/ams/routines/uc.py index 9d068fa1..9754b9ab 100644 --- a/ams/routines/uc.py +++ b/ams/routines/uc.py @@ -311,13 +311,9 @@ def _initial_guess(self): logger.warning(f"As initial commitment guess, turn off StaticGen: {off_gen}") return g_idx - def init(self, no_code=True, force_init=False, - force_mats=False, force_constr=False, - force_parse=False, force_generate=False): + def init(self, **kwargs): self._initial_guess() - return super().init(no_code=no_code, force_init=force_init, - force_mats=force_mats, force_constr=force_constr, - force_parse=force_parse, force_generate=force_generate) + return super().init(**kwargs) def dc2ac(self, **kwargs): """ From 091a8a874894face3aed067b464834a90144de00 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 3 Nov 2024 23:42:36 -0500 Subject: [PATCH 115/181] Fix DCPF --- ams/opt/omodel.py | 13 ++++--------- ams/routines/dcpf.py | 14 ++++++-------- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index 92ce1311..c4c17050 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -1080,6 +1080,10 @@ def finalize(self, force=False): bool Returns True if the finalization is successful, False otherwise. """ + # NOTE: for power flow type, we skip the finalization + if self.rtn.type == 'PF': + self.finalized = True + return self.finalized if self.finalized and not force: logger.warning("Model is already finalized.") return self.finalized @@ -1130,16 +1134,7 @@ def init(self, force=False): t, _ = elapsed() self.parse(force=force) - - # NOTE: Hardcoded for using PYPOWER as the PF type routines - if self.rtn.type == 'PF': - _, s = elapsed(t) - self.initialized = True - logger.debug(f"OModel for <{self.rtn.class_name}> initialized in {s}") - return self.initialized - self.evaluate(force=force) - self.finalize(force=force) _, s = elapsed(t) diff --git a/ams/routines/dcpf.py b/ams/routines/dcpf.py index 68e69e30..d0f84735 100644 --- a/ams/routines/dcpf.py +++ b/ams/routines/dcpf.py @@ -127,8 +127,7 @@ def solve(self, method=None): res, sstats = runpf(casedata=ppc, ppopt=ppopt) return res, sstats - def run(self, no_code=True, method=None, - *args, **kwargs): + def run(self, **kwargs): """ Run DC pwoer flow. *args and **kwargs go to `self.solve()`, which are not used yet. @@ -140,8 +139,6 @@ def run(self, no_code=True, method=None, Parameters ---------- - no_code : bool - Disable showing code. method : str Placeholder for future use. @@ -151,9 +148,10 @@ def run(self, no_code=True, method=None, Exit code of the routine. """ if not self.initialized: - self.init(no_code=no_code) + self.init() t0, _ = elapsed() - res, sstats = self.solve(method=method, *args, **kwargs) + + res, sstats = self.solve(**kwargs) self.converged = res['success'] self.exit_code = 0 if res['success'] else 1 _, s = elapsed(t0) @@ -166,8 +164,8 @@ def run(self, no_code=True, method=None, logger.info(msg) try: self.unpack(res) - except Exception: - logger.warning(f"Failed to unpack results from {self.class_name}.") + except Exception as e: + logger.error(f"Failed to unpack results from {self.class_name}.\n{e}") return False return True else: From d6e04f919bf77aa1b8a51c3937ef687be7a79e59 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 3 Nov 2024 23:50:44 -0500 Subject: [PATCH 116/181] [WIP] Add tests for DCPF --- tests/test_rtn_pflow.py | 83 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 tests/test_rtn_pflow.py diff --git a/tests/test_rtn_pflow.py b/tests/test_rtn_pflow.py new file mode 100644 index 00000000..addc9bda --- /dev/null +++ b/tests/test_rtn_pflow.py @@ -0,0 +1,83 @@ +import unittest + +import json +import numpy as np + +from andes.shared import rad2deg + +import ams + + +class TestDCPF(unittest.TestCase): + """ + Test routine `DCPF`. + """ + + def setUp(self) -> None: + with open(ams.get_case('matpower/benchmark.json'), 'r') as file: + self.mpres = json.load(file) + + self.ss = ams.load(ams.get_case('matpower/case14.m'), + setup=True, no_output=True, default_config=True) + + def test_init(self): + """ + Test initialization. + """ + self.ss.DCPF.init() + self.assertTrue(self.ss.DCPF.initialized, "DCPF initialization failed!") + + def test_trip_gen(self): + """ + Test generator tripping. + """ + stg = 2 + self.ss.StaticGen.set(src='u', attr='v', idx=stg, value=0) + + self.ss.DCPF.update() + self.ss.DCPF.run() + self.assertTrue(self.ss.DCPF.converged, "DCPF did not converge under generator trip!") + self.assertAlmostEqual(self.ss.DCPF.get(src='pg', attr='v', idx=stg), + 0, places=6, + msg="Generator trip does not take effect!") + + self.ss.StaticGen.alter(src='u', idx=stg, value=1) # reset + + def test_trip_line(self): + """ + Test line tripping. + """ + self.ss.Line.set(src='u', attr='v', idx='Line_3', value=0) + + self.ss.DCPF.update() + self.ss.DCPF.run() + self.assertTrue(self.ss.DCPF.converged, "DCPF did not converge under line trip!") + self.assertAlmostEqual(self.ss.DCPF.get(src='plf', attr='v', idx='Line_3'), + 0, places=6, + msg="Line trip does not take effect!") + + self.ss.Line.alter(src='u', idx='Line_3', value=1) + + def test_set_load(self): + """ + Test setting and tripping load. + """ + # --- run DCPF --- + self.ss.DCPF.run() + pgs = self.ss.DCPF.pg.v.sum() + + # --- set load --- + self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0.1) + self.ss.DCPF.update() + + self.ss.DCPF.run() + pgs_pqt = self.ss.DCPF.pg.v.sum() + self.assertLess(pgs_pqt, pgs, "Load set does not take effect!") + + # --- trip load --- + self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0) + self.ss.DCPF.update() + + self.ss.DCPF.run() + pgs_pqt2 = self.ss.DCPF.pg.v.sum() + self.assertLess(pgs_pqt2, pgs_pqt, "Load trip does not take effect!") From 58655455f8f2562b1de1552ef71bf64f4aa8f1ee Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 4 Nov 2024 08:32:04 -0500 Subject: [PATCH 117/181] Fix params in ACOPF.run --- ams/routines/acopf.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ams/routines/acopf.py b/ams/routines/acopf.py index 8142e883..3a746ffb 100644 --- a/ams/routines/acopf.py +++ b/ams/routines/acopf.py @@ -93,8 +93,7 @@ def solve(self, method=None, **kwargs): res, sstats = runopf(casedata=ppc, ppopt=ppopt, **kwargs) return res, sstats - def run(self, no_code=True, method=None, - *args, **kwargs): + def run(self, **kwargs): """ Run ACOPF using PYPOWER with PIPS. *args and **kwargs go to `self.solve()`, which are not used yet. @@ -118,5 +117,4 @@ def run(self, no_code=True, method=None, exit_code : int Exit code of the routine. """ - super().run(no_code=no_code, method=method, - *args, **kwargs, ) + super().run(**kwargs) From f2e7e4a13547f80b1a794a7004502afa068b6d4e Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 4 Nov 2024 08:32:17 -0500 Subject: [PATCH 118/181] Add tests for PFlow types --- tests/test_rtn_pflow.py | 152 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 144 insertions(+), 8 deletions(-) diff --git a/tests/test_rtn_pflow.py b/tests/test_rtn_pflow.py index addc9bda..1f7be489 100644 --- a/tests/test_rtn_pflow.py +++ b/tests/test_rtn_pflow.py @@ -1,10 +1,5 @@ import unittest -import json -import numpy as np - -from andes.shared import rad2deg - import ams @@ -14,9 +9,6 @@ class TestDCPF(unittest.TestCase): """ def setUp(self) -> None: - with open(ams.get_case('matpower/benchmark.json'), 'r') as file: - self.mpres = json.load(file) - self.ss = ams.load(ams.get_case('matpower/case14.m'), setup=True, no_output=True, default_config=True) @@ -81,3 +73,147 @@ def test_set_load(self): self.ss.DCPF.run() pgs_pqt2 = self.ss.DCPF.pg.v.sum() self.assertLess(pgs_pqt2, pgs_pqt, "Load trip does not take effect!") + + +class TestPFlow(unittest.TestCase): + """ + Test routine `DCPF`. + """ + + def setUp(self) -> None: + self.ss = ams.load(ams.get_case('matpower/case14.m'), + setup=True, no_output=True, default_config=True) + + def test_init(self): + """ + Test initialization. + """ + self.ss.PFlow.init() + self.assertTrue(self.ss.PFlow.initialized, "PFlow initialization failed!") + + def test_trip_gen(self): + """ + Test generator tripping. + """ + stg = 2 + self.ss.StaticGen.set(src='u', attr='v', idx=stg, value=0) + + self.ss.PFlow.update() + self.ss.PFlow.run() + self.assertTrue(self.ss.PFlow.converged, "PFlow did not converge under generator trip!") + self.assertAlmostEqual(self.ss.PFlow.get(src='pg', attr='v', idx=stg), + 0, places=6, + msg="Generator trip does not take effect!") + + self.ss.StaticGen.alter(src='u', idx=stg, value=1) + + def test_trip_line(self): + """ + Test line tripping. + """ + self.ss.Line.set(src='u', attr='v', idx='Line_3', value=0) + + self.ss.PFlow.update() + self.ss.PFlow.run() + self.assertTrue(self.ss.PFlow.converged, "PFlow did not converge under line trip!") + self.assertAlmostEqual(self.ss.PFlow.get(src='plf', attr='v', idx='Line_3'), + 0, places=6, + msg="Line trip does not take effect!") + + self.ss.Line.alter(src='u', idx='Line_3', value=1) + + def test_set_load(self): + """ + Test setting and tripping load. + """ + # --- run PFlow --- + self.ss.PFlow.run() + pgs = self.ss.PFlow.pg.v.sum() + + # --- set load --- + self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0.1) + self.ss.PFlow.update() + + self.ss.PFlow.run() + pgs_pqt = self.ss.PFlow.pg.v.sum() + self.assertLess(pgs_pqt, pgs, "Load set does not take effect!") + + # --- trip load --- + self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0) + self.ss.PFlow.update() + + self.ss.PFlow.run() + pgs_pqt2 = self.ss.PFlow.pg.v.sum() + self.assertLess(pgs_pqt2, pgs_pqt, "Load trip does not take effect!") + + +class TestACOPF(unittest.TestCase): + """ + Test routine `ACOPF`. + """ + + def setUp(self) -> None: + self.ss = ams.load(ams.get_case('matpower/case14.m'), + setup=True, no_output=True, default_config=True) + + def test_init(self): + """ + Test initialization. + """ + self.ss.ACOPF.init() + self.assertTrue(self.ss.ACOPF.initialized, "ACOPF initialization failed!") + + def test_trip_gen(self): + """ + Test generator tripping. + """ + stg = 2 + self.ss.StaticGen.set(src='u', attr='v', idx=stg, value=0) + + self.ss.ACOPF.update() + self.ss.ACOPF.run() + self.assertTrue(self.ss.ACOPF.converged, "ACOPF did not converge under generator trip!") + self.assertAlmostEqual(self.ss.ACOPF.get(src='pg', attr='v', idx=stg), + 0, places=6, + msg="Generator trip does not take effect!") + + self.ss.StaticGen.alter(src='u', idx=stg, value=1) + + def test_trip_line(self): + """ + Test line tripping. + """ + self.ss.Line.set(src='u', attr='v', idx='Line_3', value=0) + + self.ss.ACOPF.update() + self.ss.ACOPF.run() + self.assertTrue(self.ss.ACOPF.converged, "ACOPF did not converge under line trip!") + self.assertAlmostEqual(self.ss.ACOPF.get(src='plf', attr='v', idx='Line_3'), + 0, places=6, + msg="Line trip does not take effect!") + + self.ss.Line.alter(src='u', idx='Line_3', value=1) + + def test_set_load(self): + """ + Test setting and tripping load. + """ + # --- run ACOPF --- + self.ss.ACOPF.run() + pgs = self.ss.ACOPF.pg.v.sum() + + # --- set load --- + self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0.1) + self.ss.ACOPF.update() + + self.ss.ACOPF.run() + pgs_pqt = self.ss.ACOPF.pg.v.sum() + self.assertLess(pgs_pqt, pgs, "Load set does not take effect!") + + # --- trip load --- + self.ss.PQ.set(src='p0', attr='v', idx='PQ_1', value=0) + self.ss.ACOPF.update() + + self.ss.ACOPF.run() + pgs_pqt2 = self.ss.ACOPF.pg.v.sum() + self.assertLess(pgs_pqt2, pgs_pqt, "Load trip does not take effect!") From e76d23abb14cb7baf62ba1b8b640a841b239e9be Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 4 Nov 2024 09:11:32 -0500 Subject: [PATCH 119/181] Add a README for cases folder --- ams/cases/README.md | 85 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 ams/cases/README.md diff --git a/ams/cases/README.md b/ams/cases/README.md new file mode 100644 index 00000000..919f76b8 --- /dev/null +++ b/ams/cases/README.md @@ -0,0 +1,85 @@ +# Cases + +This folder contains various power system case studies used for scheduling studies. Each subfolder holds a different power system model with multiple variants. + +## 5bus + +PJM 5-bus system [5bus][5bus] + +- `pjm5bus_demo.xlsx`: Demo case for the 5-bus system. +- `pjm5bus_jumper.xlsx`: Jumper case for the 5-bus system. +- `pjm5bus_uced.json`: UCED case in JSON format. +- `pjm5bus_uced.xlsx`: UCED case in Excel format. +- `pjm5bus_uced_esd1.xlsx`: UCED case with energy storage device. +- `pjm5bus_uced_ev.xlsx`: UCED case with electric vehicles. + +## ieee14 + +IEEE 14-bus system [pstca][pstca] + +- `ieee14.json`: JSON format of the IEEE 14-bus system. +- `ieee14.raw`: Raw power flow data for the IEEE 14-bus system. +- `ieee14_uced.xlsx`: UCED case for the IEEE 14-bus system. + +## ieee39 + +IEEE 39-bus system [pstca][pstca] + +- `ieee39.xlsx`: Base case for the IEEE 39-bus system. +- `ieee39_uced.xlsx`: UCED case for the IEEE 39-bus system. +- `ieee39_uced_esd1.xlsx`: UCED case with energy storage device. +- `ieee39_uced_pvd1.xlsx`: UCED case with photovoltaic device. +- `ieee39_uced_vis.xlsx`: Visualization case for the IEEE 39-bus system. + +## ieee123 + +IEEE 123-bus system [pstca][pstca] + +- `ieee123.xlsx`: Base case for the IEEE 123-bus system. +- `ieee123_regcv1.xlsx`: Case with regulator control version 1. + + +## matpower + +Cases from Matpower [matpower][matpower] + +- `case5.m`: Matpower case for the 5-bus system. +- `case14.m`: Matpower case for the 14-bus system. +- `case39.m`: Matpower case for the 39-bus system. +- `case118.m`: Matpower case for the 118-bus system. +- `case300.m`: Matpower case for the 300-bus system. +- `case_ACTIVSg2000.m`: Matpower case for the ACTIVSg2000 system. +- `benchmark.json`: Benchmark results, NOT a power system case. + +## npcc + +Northeast Power Coordinating Council system [SciData][SciData] + +- `npcc.m`: Matpower case for the NPCC system. +- `npcc_uced.xlsx`: UCED case for the NPCC system. + +## wecc + +Western Electricity Coordinating Council system [SciData][SciData] + +- `wecc.m`: Matpower case for the WECC system. +- `wecc_uced.xlsx`: UCED case for the WECC system. + +## pglib + +Cases from the Power Grid Lib - Optimal Power Flow [pglib][pglib] + +- `pglib_opf_case39_epri__api.m`: PGLib case for the 39-bus system. + +--- + +[5bus]: F. Li and R. Bo, "Small test systems for power system economic studies," IEEE PES General Meeting, 2010, pp. 1-4, doi: 10.1109/PES.2010.5589973. + +[pstca]: “Power Systems Test Case Archive - UWEE,” labs.ece.uw.edu. https://labs.ece.uw.edu/pstca/ + +[matpower]: R. D. Zimmerman, C. E. Murillo-Sanchez, and R. J. Thomas, "MATPOWER: Steady-State Operations, Planning and Analysis Tools for Power Systems Research and Education," Power Systems, IEEE Transactions on, vol. 26, no. 1, pp. 12-19, Feb. 2011. doi: 10.1109/TPWRS.2010.2051168 + +[SciData]: Q. Zhang and F. Li, “A Dataset for Electricity Market Studies on Western and Northeastern Power Grids in the United States,” Scientific Data, vol. 10, no. 1, p. 646, Sep. 2023, doi: 10.1038/s41597-023-02448-w. + +[pglib]: S. Babaeinejadsarookolaee et al., “The Power Grid Library for Benchmarking AC Optimal Power Flow Algorithms,” arXiv.org, 2019. https://arxiv.org/abs/1908.02788 +‌ \ No newline at end of file From 60b68001cbf6527154454b31fbd8ca222490fe78 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 4 Nov 2024 09:12:34 -0500 Subject: [PATCH 120/181] Add a citation file --- CITATION.bib | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 CITATION.bib diff --git a/CITATION.bib b/CITATION.bib new file mode 100644 index 00000000..f4b3a333 --- /dev/null +++ b/CITATION.bib @@ -0,0 +1,10 @@ +@article{andes_2021, + author = {Cui, Hantao and Li, Fangxing and Tomsovic, Kevin}, + journal = {IEEE Transactions on Power Systems}, + title = {Hybrid Symbolic-Numeric Framework for Power System Modeling and Analysis}, + year = {2021}, + volume = {36}, + number = {2}, + pages = {1373-1384}, + doi = {10.1109/TPWRS.2020.3017019} +} \ No newline at end of file From 5dbdc4f8c5b1d0cf9512851fc16185c5d1edd2c6 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 4 Nov 2024 09:17:28 -0500 Subject: [PATCH 121/181] Add example usage in repo README --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 1cdf8380..29e6245d 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,19 @@ Install from GitHub source: pip install git+https://github.com/CURENT/ams.git ``` +# Example Usage + +Using AMS to run a Real-Time Economic Dispatch (RTED) simulation: + +```python +import ams + +ss = ams.load(ams.get_case('ieee14_uced.xlsx')) +ss.RTED.run() + +print(ss.RTED.pg.v) +``` + # Sponsors and Contributors AMS is the scheduling simulation engine for the CURENT Largescale Testbed (LTB). More information about CURENT LTB can be found at the [LTB Repository][LTB Repository]. From 5ef0cb7e7dbb1a23d9ac4c655b696d2198d54604 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 4 Nov 2024 09:37:45 -0500 Subject: [PATCH 122/181] Update release-notes --- docs/source/release-notes.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 6d5c31c3..f80c21c9 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -15,6 +15,17 @@ v0.9.11 (2024-xx-xx) - Add pyproject.toml for PEP 517 and PEP 518 compliance - Add model `Jumper` - Fix deprecation warning related to `pandas.fillna` and `newshape` in NumPy +- Minor refactor on solvers information in the module `shared` +- Change default values of minimum ON/OFF duration time of generators to be 1 and 0.5 hours +- Add parameter `uf` for enforced generator on/off status +- In servicee `LoadScale`, consider load online status +- Consider line online status in routine `ED` +- Add methods `evaluate` and `finalize` in the class `OModel` to handle optimization elements generation and assembling +- Refactor `OModel.init()` and `Routine.init()` +- Add ANDES paper as a citation file for now +- Add more routine tests for generator trip, line trip, and load trip +- Add a README to overview built-in cases +- Rename methods `v2` as `e` for classes `Constraint` and `Objective` v0.9.10 (2024-09-03) -------------------- From 9de4baa6b0e46ed6f113d5d351bc3fac962426ed Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 4 Nov 2024 10:30:56 -0500 Subject: [PATCH 123/181] Enhance logging messages --- ams/core/matprocessor.py | 2 +- ams/opt/omodel.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ams/core/matprocessor.py b/ams/core/matprocessor.py index 9daca3a3..57df7f1b 100644 --- a/ams/core/matprocessor.py +++ b/ams/core/matprocessor.py @@ -212,7 +212,7 @@ def build(self, force=False): return self.initialized t_mat, _ = elapsed() - logger.debug("Entering system matrix building") + logger.warning("Building system matrices") # --- connectivity matrices --- _ = self.build_cg() _ = self.build_cl() diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index c4c17050..05fa4bb3 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -938,7 +938,7 @@ def parse(self, force=False): Returns True if the parsing is successful, False otherwise. """ if self.parsed and not force: - logger.warning("Model is already parsed.") + logger.debug("Model is already parsed.") return self.parsed t, _ = elapsed() # --- add RParams and Services as parameters --- @@ -1052,7 +1052,7 @@ def evaluate(self, force=False): Returns True if the evaluation is successful, False otherwise. """ if self.evaluated and not force: - logger.warning("Model is already evaluated.") + logger.debug("Model is already evaluated.") return self.evaluated logger.warning(f"Evaluating OModel for <{self.rtn.class_name}>") t, _ = elapsed() @@ -1085,7 +1085,7 @@ def finalize(self, force=False): self.finalized = True return self.finalized if self.finalized and not force: - logger.warning("Model is already finalized.") + logger.debug("Model is already finalized.") return self.finalized logger.warning(f"Finalizing OModel for <{self.rtn.class_name}>") t, _ = elapsed() @@ -1128,7 +1128,7 @@ def init(self, force=False): Returns True if the setup is successful, False otherwise. """ if self.initialized and not force: - logger.warning("OModel is already initialized.") + logger.debug("OModel is already initialized.") return self.initialized t, _ = elapsed() From ac2e8219ff73e2ad9cde742303c3d458a94edee7 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 4 Nov 2024 22:40:06 -0500 Subject: [PATCH 124/181] Add benchmark functions --- ams/benchmarks.py | 337 +++++++++++++++++++++++++++++++++++++++ tests/test_benchmarks.py | 83 ++++++++++ 2 files changed, 420 insertions(+) create mode 100644 ams/benchmarks.py create mode 100644 tests/test_benchmarks.py diff --git a/ams/benchmarks.py b/ams/benchmarks.py new file mode 100644 index 00000000..8ad617bf --- /dev/null +++ b/ams/benchmarks.py @@ -0,0 +1,337 @@ +""" +Benchmark functions. +""" + +import datetime +import sys +import importlib_metadata +import logging + +import numpy as np + +import pandapower as pdp + +from andes.utils.misc import elapsed + +import ams + + +logger = logging.getLogger(__name__) + +_failed_time = '-1 seconds' +_failed_obj = -1 + +cols_time = ['ams_mats', 'ams_parse', 'ams_eval', 'ams_final', + 'ams_postinit', 'ams_grb', 'ams_mosek', 'ams_piqp', 'pdp'] +cols_obj = ['grb', 'mosek', 'piqp', 'pdp'] + + +def get_tool_versions(tools=None): + """ + Get the current time, Python version, and versions of specified tools. + + Parameters + ---------- + tools : list of str, optional + List of tool names to check versions. If None, a default list of tools is used. + + Returns + ------- + dict + A dictionary containing the tool names and their versions. + """ + if tools is None: + tools = ['ltbams', 'cvxpy', 'pandapower', + 'PYPOWER', 'gurobipy', 'mosek', + 'ecos', 'scs', 'osqp', 'numba'] + + # Get current time and Python version + last_run_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + python_version = sys.version + + # Collect tool versions + tool_versions = {} + for tool in tools: + try: + version = importlib_metadata.version(tool) + tool_versions[tool] = version + except importlib_metadata.PackageNotFoundError: + logger.error(f"Package {tool} not found.") + tool_versions[tool] = "Not installed" + + # Print the results in a formatted way + logger.warning(f"Last run time: {last_run_time}") + logger.warning(f"Python: {python_version}\n") + + # Calculate the width of the columns + max_tool_length = max(len(tool) for tool in tool_versions) + max_version_length = max(len(version) for version in tool_versions.values()) + + # Print the header + logger.warning(f"{'Tool':<{max_tool_length}} {'Version':<{max_version_length}}") + logger.warning(f"{'-' * max_tool_length} {'-' * max_version_length}") + + # Print each tool and its version + for tool, version in tool_versions.items(): + logger.warning(f"{tool:<{max_tool_length}} {version:<{max_version_length}}") + + return tool_versions + + +def run_routine(system, routine='DCOPF', ignore_dpp=True, solver='GUROBI', method=None): + """ + Run the specified routine with the given solver and method. + + Parameters + ---------- + system : ams.System + The system object containing the routine. + routine : str, optional + The name of the routine to run. Defaults to 'DCOPF'. + ignore_dpp : bool, optional + Whether to ignore DPP. Defaults to True. + solver : str, optional + The solver to use. Defaults to 'GUROBI'. + method : function, optional + A custom solve method to use. Defaults to None. + + Returns + ------- + tuple + A tuple containing the elapsed time and the objective value. + """ + rtn = system.routines[routine] + try: + t_start, _ = elapsed() + if method: + rtn.run(solver=solver, reoptimize=True, Method=method, ignore_dpp=ignore_dpp) + else: + rtn.run(solver=solver, ignore_dpp=ignore_dpp) + _, elapsed_time = elapsed(t_start) + obj_value = rtn.obj.v + except Exception as e: + logger.error(f"Error running routine {routine} with solver {solver}: {e}") + elapsed_time = _failed_time + obj_value = _failed_obj + return elapsed_time, obj_value + + +def test_time(case, routine='DCOPF', ignore_dpp=True): + """ + Test the execution time of the specified routine on the given case. + + Parameters + ---------- + case : str + The path to the case file. + routine : str, optional + The name of the routine to test. Defaults to 'DCOPF'. + ignore_dpp : bool, optional + Whether to ignore DPP. Defaults to True. + + Returns + ------- + tuple + A tuple containing the list of times and the list of objective values. + """ + sp = ams.load(case, setup=True, default_config=True, no_output=True) + + # NOTE: the line flow limits are relaxed for the large cases + # otherwise the DCOPF will fail in pandapower and MATPOWER + if sp.Bus.n > 4000: + sp.Line.alter(src='rate_a', idx=sp.Line.idx.v, value=999) + + rtn = sp.routines[routine] + + # Initialize AMS + # --- matrices build --- + t_mats, _ = elapsed() + sp.mats.build() + _, s_mats = elapsed(t_mats) + + # --- code generation --- + t_parse, _ = elapsed() + rtn.om.parse() + _, s_parse = elapsed(t_parse) + + # --- code evaluation --- + t_evaluate, _ = elapsed() + rtn.om.evaluate() + _, s_evaluate = elapsed(t_evaluate) + + # --- problem finalization --- + t_finalize, _ = elapsed() + rtn.om.finalize() + _, s_finalize = elapsed(t_finalize) + + # --- rest init process --- + t_postinit, _ = elapsed() + rtn.init() + _, s_postinit = elapsed(t_postinit) + + # --- run solvers --- + s_ams_grb, obj_grb = run_routine(sp, routine, ignore_dpp, 'GUROBI') + s_ams_mosek, obj_mosek = run_routine(sp, routine, ignore_dpp, 'MOSEK') + s_ams_piqp, obj_piqp = run_routine(sp, routine, ignore_dpp, 'PIQP') + + # Convert to PYPOWER format + ppc = ams.io.pypower.system2ppc(sp) + freq = sp.config.freq + ppn = pdp.converter.from_ppc(ppc, f_hz=freq) + + del sp + + try: + t_pdp, _ = elapsed() + pdp.rundcopp(ppn) + _, s_pdp = elapsed(t_pdp) + obj_pdp = ppn.res_cost + except Exception as e: + logger.error(f"Error running pandapower: {e}") + s_pdp = _failed_time + obj_pdp = _failed_obj + + time = [s_mats, s_parse, s_evaluate, s_finalize, s_postinit, + s_ams_grb, s_ams_mosek, s_ams_piqp, s_pdp] + time = [float(t.split(' ')[0]) for t in time] + obj = [obj_grb, obj_mosek, obj_piqp, obj_pdp] + + return time, obj + + +def run_dcopf_with_load_factors(sp, solver, method=None, load_factors=None, ignore_dpp=False): + """ + Run the specified solver with varying load factors. + + Parameters + ---------- + sp : ams.System + The system object containing the routine. + solver : str + The name of the solver to use. + method : function, optional + A custom solve method to use. Defaults to None. + load_factors : list of float, optional + List of load factors to apply. Defaults to None. + + Returns + ------- + tuple + A tuple containing the elapsed time and the cumulative objective value. + """ + if load_factors is None: + load_factors = [] + + obj_value = 0 + try: + t_start, _ = elapsed() + pq_idx = sp.PQ.idx.v + pd0 = sp.PQ.get(src='p0', attr='v', idx=pq_idx).copy() + for lf_k in load_factors: + sp.PQ.alter(src='p0', idx=pq_idx, value=lf_k * pd0) + sp.DCOPF.update(params=['pd']) + if method: + sp.DCOPF.run(solver=solver, reoptimize=True, Method=method, ignore_dpp=ignore_dpp) + else: + sp.DCOPF.run(solver=solver, ignore_dpp=ignore_dpp) + obj_value += sp.DCOPF.obj.v + _, elapsed_time = elapsed(t_start) + except Exception as e: + logger.error(f"Error running solver {solver} with load factors: {e}") + elapsed_time = _failed_time + obj_value = _failed_obj + return elapsed_time, obj_value + + +def test_mtime(case, load_factors, ignore_dpp=True): + """ + Test the execution time of the specified routine on the given case with varying load factors. + + Parameters + ---------- + case : str + The path to the case file. + load_factors : list of float + List of load factors to apply. + + Returns + ------- + tuple + A tuple containing the list of times and the list of objective values. + """ + sp = ams.load(case, setup=True, default_config=True, no_output=True) + + # Record original load + pq_idx = sp.PQ.idx.v + pd0 = sp.PQ.get(src='p0', attr='v', idx=pq_idx).copy() + + # Initialize AMS + # --- matrices build --- + t_mats, _ = elapsed() + sp.mats.build() + _, s_mats = elapsed(t_mats) + + # --- code generation --- + t_parse, _ = elapsed() + sp.DCOPF.om.parse() + _, s_parse = elapsed(t_parse) + + # --- code evaluation --- + t_evaluate, _ = elapsed() + sp.DCOPF.om.evaluate() + _, s_evaluate = elapsed(t_evaluate) + + # --- problem finalization --- + t_finalize, _ = elapsed() + sp.DCOPF.om.finalize() + _, s_finalize = elapsed(t_finalize) + + # --- rest init process --- + t_postinit, _ = elapsed() + sp.DCOPF.init() + _, s_postinit = elapsed(t_postinit) + + # Run solvers with load factors + s_ams_grb, obj_grb = run_dcopf_with_load_factors( + sp, 'GUROBI', method=3, load_factors=load_factors, ignore_dpp=ignore_dpp) + sp.PQ.alter(src='p0', idx=pq_idx, value=pd0) # Reset the load in AMS + + s_ams_mosek, obj_mosek = run_dcopf_with_load_factors( + sp, 'MOSEK', load_factors=load_factors, ignore_dpp=ignore_dpp) + sp.PQ.alter(src='p0', idx=pq_idx, value=pd0) + + s_ams_piqp, obj_piqp = run_dcopf_with_load_factors( + sp, 'PIQP', load_factors=load_factors, ignore_dpp=ignore_dpp) + sp.PQ.alter(src='p0', idx=pq_idx, value=pd0) + + # --- PANDAPOWER --- + ppc = ams.io.pypower.system2ppc(sp) + freq = sp.config.freq + + del sp + + ppc_pd0 = ppc['bus'][:, 2].copy() + + ppn = pdp.converter.from_ppc(ppc, f_hz=freq) + obj_pdp = 0 + t_pdp_series = ['0 seconds'] * len(load_factors) + try: + for i, lf_k in enumerate(load_factors): + ppc['bus'][:, 2] = lf_k * ppc_pd0 + ppn = pdp.converter.from_ppc(ppc, f_hz=freq) + t0_pdp, _ = elapsed() + pdp.rundcopp(ppn) + obj_pdp += ppn.res_cost + _, t_pdp_series[i] = elapsed(t0_pdp) + t_pdp_series = [float(t.split(' ')[0]) for t in t_pdp_series] + s_pdp = f'{np.sum(t_pdp_series):.4f} seconds' + except Exception: + s_pdp = _failed_time + obj_pdp = _failed_obj + + time = [s_mats, s_parse, s_evaluate, s_finalize, s_postinit, + s_ams_grb, s_ams_mosek, s_ams_piqp, s_pdp] + time = [float(t.split(' ')[0]) for t in time] + obj = [obj_grb, obj_mosek, obj_piqp, obj_pdp] + + return time, obj diff --git a/tests/test_benchmarks.py b/tests/test_benchmarks.py new file mode 100644 index 00000000..efd5c617 --- /dev/null +++ b/tests/test_benchmarks.py @@ -0,0 +1,83 @@ +import unittest +import numpy as np + +import ams +import ams.benchmarks as bp + + +def are_packages_available(*packages): + """ + Check if the specified packages are available. + + Parameters + ---------- + *packages : str + Names of the packages to check. + + Returns + ------- + bool + True if all packages are available, False otherwise. + """ + for package in packages: + try: + __import__(package) + except ImportError: + return False + return True + + +def require_packages(*packages): + """ + Decorator to skip a test if the specified packages are not available. + + Parameters + ---------- + *packages : str + Names of the packages required for the test. + + Returns + ------- + function + The decorated test function. + """ + def decorator(test_func): + return unittest.skipIf( + not are_packages_available(*packages), + f"Skipping test because one or more required packages are not available: {', '.join(packages)}" + )(test_func) + return decorator + + +class TestBenchmarks(unittest.TestCase): + + def setUp(self): + self.case = ams.get_case('matpower/case5.m') + + def test_get_tool_versions(self): + self.assertIsInstance(bp.get_tool_versions(), dict) + + def test_run_routine(self): + ss = ams.load(self.case, setup=True, default_config=True, no_output=True) + _, _obj = bp.run_routine(ss, routine='DCOPF', solver='CLARABEL', ignore_dpp=False) + + np.testing.assert_array_less(np.zeros_like(_obj), _obj) + + def test_run_dcopf_with_load_factors(self): + ss = ams.load(self.case, setup=True, default_config=True, no_output=True) + _, _obj = bp.run_dcopf_with_load_factors(ss, solver='CLARABEL', + load_factors=np.array([1.1, 1.2]), ignore_dpp=True) + + np.testing.assert_array_less(np.zeros_like(_obj), _obj) + + @require_packages('mosek', 'gurobipy', 'pandapower') + def test_test_time(self): + _, _obj = bp.test_time(self.case, routine='DCOPF', ignore_dpp=False) + + np.testing.assert_array_less(np.zeros_like(_obj), _obj) + + @require_packages('mosek', 'gurobipy', 'pandapower') + def test_test_mtime(self): + _, _obj = bp.test_mtime(self.case, load_factors=np.array([1.1, 1.2]), ignore_dpp=False) + + np.testing.assert_array_less(np.zeros_like(_obj), _obj) From 5de4474ddc0190e27fb9f27afa8ba261a4242b20 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 4 Nov 2024 22:40:33 -0500 Subject: [PATCH 125/181] Update release-notes --- docs/source/release-notes.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index f80c21c9..9b09dc71 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -26,6 +26,7 @@ v0.9.11 (2024-xx-xx) - Add more routine tests for generator trip, line trip, and load trip - Add a README to overview built-in cases - Rename methods `v2` as `e` for classes `Constraint` and `Objective` +- Add benchmark functions v0.9.10 (2024-09-03) -------------------- From e4f08418f0b3963de3a519de2d47c0e4c05a50e7 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 4 Nov 2024 23:53:36 -0500 Subject: [PATCH 126/181] Update cases README --- ams/cases/README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/ams/cases/README.md b/ams/cases/README.md index 919f76b8..8ad62666 100644 --- a/ams/cases/README.md +++ b/ams/cases/README.md @@ -4,7 +4,7 @@ This folder contains various power system case studies used for scheduling studi ## 5bus -PJM 5-bus system [5bus][5bus] +PJM 5-bus system [5bus](#references) - `pjm5bus_demo.xlsx`: Demo case for the 5-bus system. - `pjm5bus_jumper.xlsx`: Jumper case for the 5-bus system. @@ -15,7 +15,7 @@ PJM 5-bus system [5bus][5bus] ## ieee14 -IEEE 14-bus system [pstca][pstca] +IEEE 14-bus system [pstca](#references) - `ieee14.json`: JSON format of the IEEE 14-bus system. - `ieee14.raw`: Raw power flow data for the IEEE 14-bus system. @@ -23,7 +23,7 @@ IEEE 14-bus system [pstca][pstca] ## ieee39 -IEEE 39-bus system [pstca][pstca] +IEEE 39-bus system [pstca](#references) - `ieee39.xlsx`: Base case for the IEEE 39-bus system. - `ieee39_uced.xlsx`: UCED case for the IEEE 39-bus system. @@ -33,15 +33,14 @@ IEEE 39-bus system [pstca][pstca] ## ieee123 -IEEE 123-bus system [pstca][pstca] +IEEE 123-bus system [pstca](#references) - `ieee123.xlsx`: Base case for the IEEE 123-bus system. - `ieee123_regcv1.xlsx`: Case with regulator control version 1. - ## matpower -Cases from Matpower [matpower][matpower] +Cases from Matpower [matpower](#references) - `case5.m`: Matpower case for the 5-bus system. - `case14.m`: Matpower case for the 14-bus system. @@ -53,26 +52,28 @@ Cases from Matpower [matpower][matpower] ## npcc -Northeast Power Coordinating Council system [SciData][SciData] +Northeast Power Coordinating Council system [SciData](#references) - `npcc.m`: Matpower case for the NPCC system. - `npcc_uced.xlsx`: UCED case for the NPCC system. ## wecc -Western Electricity Coordinating Council system [SciData][SciData] +Western Electricity Coordinating Council system [SciData](#references) - `wecc.m`: Matpower case for the WECC system. - `wecc_uced.xlsx`: UCED case for the WECC system. ## pglib -Cases from the Power Grid Lib - Optimal Power Flow [pglib][pglib] +Cases from the Power Grid Lib - Optimal Power Flow [pglib](#references) - `pglib_opf_case39_epri__api.m`: PGLib case for the 39-bus system. --- +## References + [5bus]: F. Li and R. Bo, "Small test systems for power system economic studies," IEEE PES General Meeting, 2010, pp. 1-4, doi: 10.1109/PES.2010.5589973. [pstca]: “Power Systems Test Case Archive - UWEE,” labs.ece.uw.edu. https://labs.ece.uw.edu/pstca/ @@ -81,5 +82,4 @@ Cases from the Power Grid Lib - Optimal Power Flow [pglib][pglib] [SciData]: Q. Zhang and F. Li, “A Dataset for Electricity Market Studies on Western and Northeastern Power Grids in the United States,” Scientific Data, vol. 10, no. 1, p. 646, Sep. 2023, doi: 10.1038/s41597-023-02448-w. -[pglib]: S. Babaeinejadsarookolaee et al., “The Power Grid Library for Benchmarking AC Optimal Power Flow Algorithms,” arXiv.org, 2019. https://arxiv.org/abs/1908.02788 -‌ \ No newline at end of file +[pglib]: S. Babaeinejadsarookolaee et al., “The Power Grid Library for Benchmarking AC Optimal Power Flow Algorithms,” arXiv.org, 2019. https://arxiv.org/abs/1908.02788 \ No newline at end of file From f8f94378bb87e311837e6c9d834563f3a9fce708 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 4 Nov 2024 23:57:16 -0500 Subject: [PATCH 127/181] Fix parameters overriding in PFlow.run --- ams/routines/pflow.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ams/routines/pflow.py b/ams/routines/pflow.py index 27318067..68ec9bd2 100644 --- a/ams/routines/pflow.py +++ b/ams/routines/pflow.py @@ -78,7 +78,7 @@ def solve(self, method="newton", **kwargs): res, sstats = runpf(casedata=ppc, ppopt=ppopt) return res, sstats - def run(self, force_init=False, no_code=True, method="newton", **kwargs): + def run(self, **kwargs): """ Run AC power flow using PYPOWER. @@ -108,6 +108,4 @@ def run(self, force_init=False, no_code=True, method="newton", **kwargs): exit_code : int Exit code of the routine. """ - return super().run(force_init=force_init, - no_code=no_code, method=method, - **kwargs,) + return super().run(**kwargs,) From 24f5962d2d15f4701877b93d9c929f8e6cc9afc8 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 4 Nov 2024 23:57:37 -0500 Subject: [PATCH 128/181] Ignore cases README in codacy --- .codacy.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.codacy.yml b/.codacy.yml index 0773411c..9e790d7d 100644 --- a/.codacy.yml +++ b/.codacy.yml @@ -11,7 +11,8 @@ exclude_paths: - "ams/_version.py" - ".github/**" - "README.md" - - "CODE_OF_CONDUCT.md" + - "cases/README.md" + - "CODE_OF_CONDUCT.md"` - "tests/**" - "docs/**" - "examples/**" \ No newline at end of file From ae5b5e7f0c1edee97d14c8111c0b0c2d58360d22 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Tue, 5 Nov 2024 00:06:12 -0500 Subject: [PATCH 129/181] Quit use of eval in OModel.finalize --- ams/opt/omodel.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index 05fa4bb3..dec7debc 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -1089,20 +1089,13 @@ def finalize(self, force=False): return self.finalized logger.warning(f"Finalizing OModel for <{self.rtn.class_name}>") t, _ = elapsed() - code_prob = "problem(self.obj, " - constrs_skip = [] - constrs_add = [] - for key, val in self.rtn.constrs.items(): - if (val.is_disabled) or (val is None): - constrs_skip.append(f'<{key}>') - else: - constrs_add.append(val.optz) - code_prob += "[constr for constr in constrs_add])" - for pattern, replacement in self.rtn.syms.sub_map.items(): - code_prob = re.sub(pattern, replacement, code_prob) - local_vars = {'self': self, 'constrs_add': constrs_add, 'cp': cp} - self.prob = eval(code_prob, {}, local_vars) + # Collect constraints that are not disabled + constrs_add = [val.optz for key, val in self.rtn.constrs.items( + ) if not val.is_disabled and val is not None] + # Construct the problem using cvxpy.Problem + self.prob = cp.Problem(self.obj, constrs_add) + _, s = elapsed(t) logger.debug(f" -> Finalized in {s}") self.finalized = True From 216bd0ce41dffc862189a8e4b568736b88f55ede Mon Sep 17 00:00:00 2001 From: jinningwang Date: Tue, 5 Nov 2024 00:09:38 -0500 Subject: [PATCH 130/181] Quit use of eval in Param.evaluate --- ams/opt/omodel.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index dec7debc..5ddcf944 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -61,6 +61,7 @@ def ensure_mats_and_parsed(func): Evaluation before matrices building and parsing can run into errors. Ensure that system matrices are built and OModel is parsed before calling the `evaluate` method. """ + def wrapper(self, *args, **kwargs): try: if not self.rtn.system.mats.initialized: @@ -343,11 +344,11 @@ def evaluate(self): msg += "but the value is not sparse." val = "self.v" if self.sparse: - if not spr.issparse(self.v): - val = "sps.csr_matrix(self.v)" - local_vars = {'self': self, 'config': config, 'sps': sps, 'cp': cp} - self.optz = eval(self.code, {}, local_vars) # create the cp.Parameter as self.optz - self.optz.value = eval(val, {}, local_vars) # assign value to self.optz + self.v = sps.csr_matrix(self.v) + + # Create the cvxpy.Parameter object + self.optz = cp.Parameter(shape=self.v.shape, **config) + self.optz.value = self.v except ValueError: msg = f"Parameter <{self.name}> has non-numeric value, " msg += "set `no_parse=True`." From 79421da310fd3612449d49f974ad0eddca44b312 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Tue, 5 Nov 2024 00:19:49 -0500 Subject: [PATCH 131/181] Quit use of eval in ExpressionCalc.evaluate, Constraint.evaluate and .e, and Objective.evaluate and .e --- ams/opt/omodel.py | 62 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 8 deletions(-) diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index 5ddcf944..12904f14 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -8,7 +8,6 @@ import re import numpy as np -import scipy.sparse as spr from andes.core.common import Config from andes.utils.misc import elapsed @@ -217,12 +216,28 @@ def evaluate(self): msg = f" - Expression <{self.name}>: {self.code}" logger.debug(pretty_long_message(msg, _prefix, max_length=_max_length)) try: - local_vars = {'self': self} - self.optz = eval(self.code, {}, local_vars) + local_vars = {'self': self, 'np': np, 'cp': cp} + self.optz = self._evaluate_expression(self.code, local_vars=local_vars) except Exception as e: raise Exception(f"Error in evaluating expr <{self.name}>.\n{e}") return True + def _evaluate_expression(self, code, local_vars=None): + """ + Helper method to evaluate the expression code. + + Parameters + ---------- + code : str + The code string representing the expression. + + Returns + ------- + cp.Expression + The evaluated cvxpy expression. + """ + return eval(code, {}, local_vars) + @property def v(self): """ @@ -342,7 +357,6 @@ def evaluate(self): try: msg = f"Parameter <{self.name}> is set as sparse, " msg += "but the value is not sparse." - val = "self.v" if self.sparse: self.v = sps.csr_matrix(self.v) @@ -666,6 +680,22 @@ def parse(self): logger.debug(pretty_long_message(msg, _prefix, max_length=_max_length)) return True + def _evaluate_expression(self, code, local_vars=None): + """ + Helper method to evaluate the expression code. + + Parameters + ---------- + code : str + The code string representing the expression. + + Returns + ------- + cp.Expression + The evaluated cvxpy expression. + """ + return eval(code, {}, local_vars) + @ensure_mats_and_parsed def evaluate(self): """ @@ -675,7 +705,7 @@ def evaluate(self): logger.debug(pretty_long_message(msg, _prefix, max_length=_max_length)) try: local_vars = {'self': self, 'cp': cp, 'sub_map': self.om.rtn.syms.val_map} - self.optz = eval(self.code, {}, local_vars) + self.optz = self._evaluate_expression(self.code, local_vars=local_vars) except Exception as e: raise Exception(f"Error in evaluating constr <{self.name}>.\n{e}") @@ -711,7 +741,7 @@ def e(self): logger.debug(pretty_long_message(f"Value code: {code}", _prefix, max_length=_max_length)) local_vars = {'self': self, 'np': np, 'cp': cp, 'val_map': val_map} - return eval(code, {}, local_vars) + return self._evaluate_expression(code, local_vars) except Exception as e: logger.error(f"Error in calculating constr <{self.name}>.\n{e}") return None @@ -808,7 +838,7 @@ def e(self): logger.debug(pretty_long_message(f"Value code: {code}", _prefix, max_length=_max_length)) local_vars = {'self': self, 'np': np, 'cp': cp, 'val_map': val_map} - return eval(code, {}, local_vars) + return self._evaluate_expression(code, local_vars) except Exception as e: logger.error(f"Error in calculating obj <{self.name}>.\n{e}") return None @@ -868,11 +898,27 @@ def evaluate(self): logger.debug(f" - Objective <{self.name}>: {self.e_str}") try: local_vars = {'self': self, 'cp': cp} - self.optz = eval(self.code, {}, local_vars) + self.optz = self._evaluate_expression(self.code, local_vars=local_vars) except Exception as e: raise Exception(f"Error in evaluating obj <{self.name}>.\n{e}") return True + def _evaluate_expression(self, code, local_vars=None): + """ + Helper method to evaluate the expression code. + + Parameters + ---------- + code : str + The code string representing the expression. + + Returns + ------- + cp.Expression + The evaluated cvxpy expression. + """ + return eval(code, {}, local_vars) + def __repr__(self): return f"{self.class_name}: {self.name} [{self.sense.upper()}]" From 1a4a7937cec95dd877bec620abce641fb26144aa Mon Sep 17 00:00:00 2001 From: jinningwang Date: Tue, 5 Nov 2024 00:20:34 -0500 Subject: [PATCH 132/181] Update release notes --- docs/source/release-notes.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 9b09dc71..5070e83c 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -27,6 +27,7 @@ v0.9.11 (2024-xx-xx) - Add a README to overview built-in cases - Rename methods `v2` as `e` for classes `Constraint` and `Objective` - Add benchmark functions +- Improve using of `eval()` in module `omodel` v0.9.10 (2024-09-03) -------------------- From 8b951761543ff7bf45a42d27082955385a9d024d Mon Sep 17 00:00:00 2001 From: jinningwang Date: Tue, 5 Nov 2024 11:09:09 -0500 Subject: [PATCH 133/181] In get_tool_versions, change OSQP to PIQP --- ams/benchmarks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ams/benchmarks.py b/ams/benchmarks.py index 8ad617bf..7d4312b4 100644 --- a/ams/benchmarks.py +++ b/ams/benchmarks.py @@ -43,7 +43,7 @@ def get_tool_versions(tools=None): if tools is None: tools = ['ltbams', 'cvxpy', 'pandapower', 'PYPOWER', 'gurobipy', 'mosek', - 'ecos', 'scs', 'osqp', 'numba'] + 'ecos', 'scs', 'piqp', 'numba'] # Get current time and Python version last_run_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") From 28c79c1fb8b8b03cc1e7f82edc0619d466aba91a Mon Sep 17 00:00:00 2001 From: jinningwang Date: Tue, 5 Nov 2024 17:51:34 -0500 Subject: [PATCH 134/181] Change from .alter to .set for andes interface --- ams/interop/andes.py | 20 ++++++++++---------- ams/routines/dcopf.py | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/ams/interop/andes.py b/ams/interop/andes.py index ea53cf93..9f883990 100644 --- a/ams/interop/andes.py +++ b/ams/interop/andes.py @@ -317,7 +317,7 @@ def parse_addfile(adsys, amsys, addfile): syg_idx += syg.idx.v syg_bus_idx = adsys.SynGen.get(src='bus', attr='v', idx=syg_idx) syg_bus_vn = adsys.Bus.get(src='Vn', idx=syg_bus_idx) - adsys.SynGen.alter(src='Vn', idx=syg_idx, value=syg_bus_vn) + adsys.SynGen.set(src='Vn', attr='v', idx=syg_idx, value=syg_bus_vn) # --- for debugging --- adsys.df_in = df_models @@ -417,7 +417,7 @@ def _send_tgr(self, sa, sp): if syg_mask.any(): logger.debug('Governor is not complete for SynGen.') # --- pref --- - sa.TurbineGov.alter(value=syg_ams, idx=gov_idx, src='pref0') + sa.TurbineGov.set(value=syg_ams, idx=gov_idx, src='pref0') # --- paux --- # TODO: sync paux, using paux0 @@ -432,7 +432,7 @@ def _send_tgr(self, sa, sp): dg_ams = sp.recent.get(src='pg', attr='v', idx=stg_dg_idx, allow_none=True, default=0) # --- pref --- - sa.DG.alter(src='pref0', idx=dg_idx, value=dg_ams) + sa.DG.set(src='pref0', idx=dg_idx, value=dg_ams) # TODO: paux, using Pext0, this one should be do in other place rather than here # 3) RenGen @@ -486,9 +486,9 @@ def _send_dgu(self, sa, sp): msg += ' Otherwise, unexpected results might occur.' raise ValueError(msg) # FIXME: below code seems to be unnecessary - sa.SynGen.alter(src='u', idx=syg_idx, value=stg_u_ams) - sa.DG.alter(src='u', idx=dg_idx, value=dg_u_ams) - sa.RenGen.alter(src='u', idx=rg_idx, value=rg_u_ams) + sa.SynGen.set(src='u', idx=syg_idx, value=stg_u_ams) + sa.DG.set(src='u', idx=dg_idx, value=dg_u_ams) + sa.RenGen.set(src='u', idx=rg_idx, value=rg_u_ams) return True def _sync_check(self, amsys, adsys): @@ -568,7 +568,7 @@ def send(self, adsys=None, routine=None): stg_idx = sp.StaticGen.get_idx() bus_stg = sp.StaticGen.get(src='bus', attr='v', idx=stg_idx) vBus = rtn.get(src='vBus', attr='v', idx=bus_stg) - sa.StaticGen.alter(src='v0', idx=stg_idx, value=vBus) + sa.StaticGen.set(src='v0', attr='v', idx=stg_idx, value=vBus) logger.info(f'*Send <{vname_ams}> to StaticGen.v0') # 1. gen online status; in TDS running, setting u is invalid @@ -600,7 +600,7 @@ def send(self, adsys=None, routine=None): if syg_mask.any(): logger.debug('Governor is not complete for SynGen.') # --- pref --- - sa.TurbineGov.alter(src='pref0', idx=gov_idx, value=syg_ams) + sa.TurbineGov.set(src='pref0', idx=gov_idx, value=syg_ams) # --- DG: DG.pref0 --- dg_idx = sp.dyn.link['dg_idx'].dropna().tolist() # DG idx @@ -610,7 +610,7 @@ def send(self, adsys=None, routine=None): # corresponding StaticGen pg in AMS dg_ams = rtn.get(src='pg', attr='v', idx=stg_dg_idx) # --- pref --- - sa.DG.alter(src='pref0', idx=dg_idx, value=dg_ams) + sa.DG.set(src='pref0', idx=dg_idx, value=dg_ams) # --- RenGen: seems unnecessary --- # TODO: which models/params are used to control output and auxillary power? @@ -625,7 +625,7 @@ def send(self, adsys=None, routine=None): # --- other scenarios --- if _dest_check(mname=mname_ads, pname=pname_ads, idx=idx_ads, adsys=sa): - mdl_ads.alter(src=pname_ads, idx=idx_ads, value=var_ams.v) + mdl_ads.set(src=pname_ads, attr='v', idx=idx_ads, value=var_ams.v) logger.warning(f'Send <{vname_ams}> to {mname_ads}.{pname_ads}') return True diff --git a/ams/routines/dcopf.py b/ams/routines/dcopf.py index fd267a72..a4397feb 100644 --- a/ams/routines/dcopf.py +++ b/ams/routines/dcopf.py @@ -312,7 +312,7 @@ def unpack(self, **kwargs): pass # NOTE: only unpack the variables that are in the model or group try: - var.owner.alter(src=var.src, idx=idx, value=var.v) + var.owner.set(src=var.src, idx=idx, value=var.v) # failed to find source var in the owner (model or group) except (KeyError, TypeError): pass From 256bdb5ae858ff6354d3de66053253b981b428d9 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 6 Nov 2024 08:34:04 -0500 Subject: [PATCH 135/181] Add dev dependencies toml and pandapower, and re-run genconf --- pyproject.toml | 4 +++- requirements-extra.txt | 4 +++- requirements.txt | 2 +- setup.cfg | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 84cd36d2..aa1bc504 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,9 @@ dev = [ "flake8", "numpydoc", "toml", - "pyscipopt" + "pyscipopt", + "pandapower", + "toml" ] doc = [ "ipython", diff --git a/requirements-extra.txt b/requirements-extra.txt index 7f77c81a..7a59760d 100644 --- a/requirements-extra.txt +++ b/requirements-extra.txt @@ -1,4 +1,4 @@ -# Generated on 2024-09-02. +# Generated on 2024-11-06. pytest # dev pytest-cov # dev coverage # dev @@ -6,6 +6,8 @@ flake8 # dev numpydoc # dev toml # dev pyscipopt # dev +pandapower # dev +toml # dev ipython # doc sphinx # doc pydata-sphinx-theme # doc diff --git a/requirements.txt b/requirements.txt index a47dbeec..b4053049 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -# Generated on 2024-09-02. +# Generated on 2024-11-06. kvxopt>=1.3.2.1 numpy scipy diff --git a/setup.cfg b/setup.cfg index a8c004bd..9a8df768 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,4 +1,4 @@ -# Generated on 2024-09-02. +# Generated on 2024-11-06. [versioneer] VCS = git style = pep440-post From c9d57197e09101f21afb1a8722587e2f230b9cfb Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 6 Nov 2024 08:34:32 -0500 Subject: [PATCH 136/181] Try to fix github action error libso.9 not found --- .github/workflows/codecov.yml | 1 + .github/workflows/pythonapp.yml | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 2a778bb5..93b82e0b 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -24,6 +24,7 @@ jobs: run: | mamba install -y nbmake pytest-xdist line_profiler # add'l packages for notebook tests. mamba install --file requirements.txt --file requirements-extra.txt + python -m pip install pyscipopt python -m pip install -e . - shell: bash -el {0} name: Test with pytest and collect coverage diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index 59b6d71e..ee6bad1a 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -29,13 +29,22 @@ jobs: run: | mamba install -y nbmake pytest-xdist line_profiler # add'l packages for notebook tests. mamba install --file requirements.txt --file requirements-extra.txt + python -m pip install pyscipopt python -m pip install -e . + - shell: bash -el {0} + name: Run pip check + run: | + pip check || true # Allow pip check to fail without failing the job - name: Lint with flake8 for pull requests if: github.event_name == 'pull_request' run: | pip install flake8 # specify flake8 to avoid unknown error # stop the build if there are Python syntax errors or undefined names flake8 . + - shell: bash -el {0} + name: Test with pytest + run: | + pytest - shell: bash -el {0} name: Test notebooks. run: | From cee81d8f4bafc44053604628b0335c6d50369ef3 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 6 Nov 2024 08:39:03 -0500 Subject: [PATCH 137/181] Remove dev dependencies pandapower, and re-run genconf --- pyproject.toml | 1 - requirements-extra.txt | 1 - 2 files changed, 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index aa1bc504..175bbea5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,6 @@ dev = [ "numpydoc", "toml", "pyscipopt", - "pandapower", "toml" ] doc = [ diff --git a/requirements-extra.txt b/requirements-extra.txt index 7a59760d..8839e7dc 100644 --- a/requirements-extra.txt +++ b/requirements-extra.txt @@ -6,7 +6,6 @@ flake8 # dev numpydoc # dev toml # dev pyscipopt # dev -pandapower # dev toml # dev ipython # doc sphinx # doc From bd199c8105cbabd73eaf7b0b66b6205f1c09a04e Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 6 Nov 2024 08:43:47 -0500 Subject: [PATCH 138/181] Fix test_benchmarks --- tests/test_benchmarks.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_benchmarks.py b/tests/test_benchmarks.py index efd5c617..1cf7def3 100644 --- a/tests/test_benchmarks.py +++ b/tests/test_benchmarks.py @@ -57,12 +57,14 @@ def setUp(self): def test_get_tool_versions(self): self.assertIsInstance(bp.get_tool_versions(), dict) + @require_packages('mosek', 'gurobipy', 'pandapower') def test_run_routine(self): ss = ams.load(self.case, setup=True, default_config=True, no_output=True) _, _obj = bp.run_routine(ss, routine='DCOPF', solver='CLARABEL', ignore_dpp=False) np.testing.assert_array_less(np.zeros_like(_obj), _obj) + @require_packages('mosek', 'gurobipy', 'pandapower') def test_run_dcopf_with_load_factors(self): ss = ams.load(self.case, setup=True, default_config=True, no_output=True) _, _obj = bp.run_dcopf_with_load_factors(ss, solver='CLARABEL', From 8c4f1bb7769e8e939143821b907c9193210c633a Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 6 Nov 2024 08:53:20 -0500 Subject: [PATCH 139/181] Fix dependency of pandapower in module benchmarks --- ams/benchmarks.py | 78 +++++++++++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 33 deletions(-) diff --git a/ams/benchmarks.py b/ams/benchmarks.py index 7d4312b4..d6f215cc 100644 --- a/ams/benchmarks.py +++ b/ams/benchmarks.py @@ -9,13 +9,17 @@ import numpy as np -import pandapower as pdp +try: + import pandapower as pdp + PANDAPOWER_AVAILABLE = True +except ImportError: + PANDAPOWER_AVAILABLE = False + logging.warning("pandapower is not available. Some functionalities will be disabled.") from andes.utils.misc import elapsed import ams - logger = logging.getLogger(__name__) _failed_time = '-1 seconds' @@ -174,20 +178,24 @@ def test_time(case, routine='DCOPF', ignore_dpp=True): s_ams_mosek, obj_mosek = run_routine(sp, routine, ignore_dpp, 'MOSEK') s_ams_piqp, obj_piqp = run_routine(sp, routine, ignore_dpp, 'PIQP') - # Convert to PYPOWER format - ppc = ams.io.pypower.system2ppc(sp) - freq = sp.config.freq - ppn = pdp.converter.from_ppc(ppc, f_hz=freq) + if PANDAPOWER_AVAILABLE: + # Convert to PYPOWER format + ppc = ams.io.pypower.system2ppc(sp) + freq = sp.config.freq + ppn = pdp.converter.from_ppc(ppc, f_hz=freq) - del sp + del sp - try: - t_pdp, _ = elapsed() - pdp.rundcopp(ppn) - _, s_pdp = elapsed(t_pdp) - obj_pdp = ppn.res_cost - except Exception as e: - logger.error(f"Error running pandapower: {e}") + try: + t_pdp, _ = elapsed() + pdp.rundcopp(ppn) + _, s_pdp = elapsed(t_pdp) + obj_pdp = ppn.res_cost + except Exception as e: + logger.error(f"Error running pandapower: {e}") + s_pdp = _failed_time + obj_pdp = _failed_obj + else: s_pdp = _failed_time obj_pdp = _failed_obj @@ -304,28 +312,32 @@ def test_mtime(case, load_factors, ignore_dpp=True): sp, 'PIQP', load_factors=load_factors, ignore_dpp=ignore_dpp) sp.PQ.alter(src='p0', idx=pq_idx, value=pd0) - # --- PANDAPOWER --- - ppc = ams.io.pypower.system2ppc(sp) - freq = sp.config.freq + if PANDAPOWER_AVAILABLE: + # --- PANDAPOWER --- + ppc = ams.io.pypower.system2ppc(sp) + freq = sp.config.freq - del sp + del sp - ppc_pd0 = ppc['bus'][:, 2].copy() + ppc_pd0 = ppc['bus'][:, 2].copy() - ppn = pdp.converter.from_ppc(ppc, f_hz=freq) - obj_pdp = 0 - t_pdp_series = ['0 seconds'] * len(load_factors) - try: - for i, lf_k in enumerate(load_factors): - ppc['bus'][:, 2] = lf_k * ppc_pd0 - ppn = pdp.converter.from_ppc(ppc, f_hz=freq) - t0_pdp, _ = elapsed() - pdp.rundcopp(ppn) - obj_pdp += ppn.res_cost - _, t_pdp_series[i] = elapsed(t0_pdp) - t_pdp_series = [float(t.split(' ')[0]) for t in t_pdp_series] - s_pdp = f'{np.sum(t_pdp_series):.4f} seconds' - except Exception: + ppn = pdp.converter.from_ppc(ppc, f_hz=freq) + obj_pdp = 0 + t_pdp_series = ['0 seconds'] * len(load_factors) + try: + for i, lf_k in enumerate(load_factors): + ppc['bus'][:, 2] = lf_k * ppc_pd0 + ppn = pdp.converter.from_ppc(ppc, f_hz=freq) + t0_pdp, _ = elapsed() + pdp.rundcopp(ppn) + obj_pdp += ppn.res_cost + _, t_pdp_series[i] = elapsed(t0_pdp) + t_pdp_series = [float(t.split(' ')[0]) for t in t_pdp_series] + s_pdp = f'{np.sum(t_pdp_series):.4f} seconds' + except Exception: + s_pdp = _failed_time + obj_pdp = _failed_obj + else: s_pdp = _failed_time obj_pdp = _failed_obj From 6ccc59456e37dcd05ae122776548398efdb05d70 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 6 Nov 2024 09:23:20 -0500 Subject: [PATCH 140/181] Fix value set in interop --- ams/interop/andes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ams/interop/andes.py b/ams/interop/andes.py index 9f883990..7ff66bb9 100644 --- a/ams/interop/andes.py +++ b/ams/interop/andes.py @@ -600,7 +600,7 @@ def send(self, adsys=None, routine=None): if syg_mask.any(): logger.debug('Governor is not complete for SynGen.') # --- pref --- - sa.TurbineGov.set(src='pref0', idx=gov_idx, value=syg_ams) + sa.TurbineGov.set(src='pref0', attr='v', idx=gov_idx, value=syg_ams) # --- DG: DG.pref0 --- dg_idx = sp.dyn.link['dg_idx'].dropna().tolist() # DG idx @@ -610,7 +610,7 @@ def send(self, adsys=None, routine=None): # corresponding StaticGen pg in AMS dg_ams = rtn.get(src='pg', attr='v', idx=stg_dg_idx) # --- pref --- - sa.DG.set(src='pref0', idx=dg_idx, value=dg_ams) + sa.DG.set(src='pref0', attr='v', idx=dg_idx, value=dg_ams) # --- RenGen: seems unnecessary --- # TODO: which models/params are used to control output and auxillary power? From a44e622f5d107aadab04c67b10bc3ac9f863db61 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 6 Nov 2024 09:23:35 -0500 Subject: [PATCH 141/181] Fix var unpack in DCOPF --- ams/routines/dcopf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ams/routines/dcopf.py b/ams/routines/dcopf.py index a4397feb..c2d7b9cb 100644 --- a/ams/routines/dcopf.py +++ b/ams/routines/dcopf.py @@ -312,9 +312,9 @@ def unpack(self, **kwargs): pass # NOTE: only unpack the variables that are in the model or group try: - var.owner.set(src=var.src, idx=idx, value=var.v) - # failed to find source var in the owner (model or group) + var.owner.set(src=var.src, attr='v', idx=idx, value=var.v) except (KeyError, TypeError): + logger.error(f'Failed to unpack {var.name} to {var.owner.name}') pass # label the most recent solved routine From 4a777dc64f17085db3574a3e44b03644ca204584 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 6 Nov 2024 09:29:57 -0500 Subject: [PATCH 142/181] Minor fix in UC._initial_guess --- ams/routines/uc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ams/routines/uc.py b/ams/routines/uc.py index 9754b9ab..50465422 100644 --- a/ams/routines/uc.py +++ b/ams/routines/uc.py @@ -307,7 +307,7 @@ def _initial_guess(self): off_gen = f'{g_idx}' else: off_gen = ', '.join(g_idx) - self.system.StaticGen.alter(src='u', idx=g_idx, value=ug0) + self.system.StaticGen.set(src='u', attr='v', idx=g_idx, value=ug0) logger.warning(f"As initial commitment guess, turn off StaticGen: {off_gen}") return g_idx From afc4626dd0db9c6b284acc533626985434900ceb Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 6 Nov 2024 09:57:47 -0500 Subject: [PATCH 143/181] Fix alter and set mis-use issue --- ams/interop/andes.py | 30 ++++++++++---------- ams/routines/dcopf.py | 12 ++++---- ams/routines/routine.py | 2 +- ams/routines/rted.py | 20 +++++++------- ams/routines/uc.py | 2 +- examples/demonstration/demo_AGC.ipynb | 40 +++++++++++++-------------- tests/test_interop.py | 4 +-- tests/test_rtn_dcopf.py | 4 +-- tests/test_rtn_ed.py | 8 +++--- tests/test_rtn_pflow.py | 12 ++++---- tests/test_rtn_rted.py | 8 +++--- 11 files changed, 71 insertions(+), 71 deletions(-) diff --git a/ams/interop/andes.py b/ams/interop/andes.py index 7ff66bb9..b75cfdd2 100644 --- a/ams/interop/andes.py +++ b/ams/interop/andes.py @@ -317,7 +317,7 @@ def parse_addfile(adsys, amsys, addfile): syg_idx += syg.idx.v syg_bus_idx = adsys.SynGen.get(src='bus', attr='v', idx=syg_idx) syg_bus_vn = adsys.Bus.get(src='Vn', idx=syg_bus_idx) - adsys.SynGen.set(src='Vn', attr='v', idx=syg_idx, value=syg_bus_vn) + adsys.SynGen.set(src='Vn', idx=syg_idx, attr='v', value=syg_bus_vn) # --- for debugging --- adsys.df_in = df_models @@ -417,7 +417,7 @@ def _send_tgr(self, sa, sp): if syg_mask.any(): logger.debug('Governor is not complete for SynGen.') # --- pref --- - sa.TurbineGov.set(value=syg_ams, idx=gov_idx, src='pref0') + sa.TurbineGov.set(value=syg_ams, idx=gov_idx, attr='v', src='pref0') # --- paux --- # TODO: sync paux, using paux0 @@ -432,7 +432,7 @@ def _send_tgr(self, sa, sp): dg_ams = sp.recent.get(src='pg', attr='v', idx=stg_dg_idx, allow_none=True, default=0) # --- pref --- - sa.DG.set(src='pref0', idx=dg_idx, value=dg_ams) + sa.DG.set(src='pref0', idx=dg_idx, attr='v', value=dg_ams) # TODO: paux, using Pext0, this one should be do in other place rather than here # 3) RenGen @@ -486,9 +486,9 @@ def _send_dgu(self, sa, sp): msg += ' Otherwise, unexpected results might occur.' raise ValueError(msg) # FIXME: below code seems to be unnecessary - sa.SynGen.set(src='u', idx=syg_idx, value=stg_u_ams) - sa.DG.set(src='u', idx=dg_idx, value=dg_u_ams) - sa.RenGen.set(src='u', idx=rg_idx, value=rg_u_ams) + sa.SynGen.set(src='u', idx=syg_idx, attr='v', value=stg_u_ams) + sa.DG.set(src='u', idx=dg_idx, attr='v', value=dg_u_ams) + sa.RenGen.set(src='u', idx=rg_idx, attr='v', value=rg_u_ams) return True def _sync_check(self, amsys, adsys): @@ -568,7 +568,7 @@ def send(self, adsys=None, routine=None): stg_idx = sp.StaticGen.get_idx() bus_stg = sp.StaticGen.get(src='bus', attr='v', idx=stg_idx) vBus = rtn.get(src='vBus', attr='v', idx=bus_stg) - sa.StaticGen.set(src='v0', attr='v', idx=stg_idx, value=vBus) + sa.StaticGen.set(src='v0', idx=stg_idx, attr='v', value=vBus) logger.info(f'*Send <{vname_ams}> to StaticGen.v0') # 1. gen online status; in TDS running, setting u is invalid @@ -600,7 +600,7 @@ def send(self, adsys=None, routine=None): if syg_mask.any(): logger.debug('Governor is not complete for SynGen.') # --- pref --- - sa.TurbineGov.set(src='pref0', attr='v', idx=gov_idx, value=syg_ams) + sa.TurbineGov.set(src='pref0', idx=gov_idx, attr='v', value=syg_ams) # --- DG: DG.pref0 --- dg_idx = sp.dyn.link['dg_idx'].dropna().tolist() # DG idx @@ -610,7 +610,7 @@ def send(self, adsys=None, routine=None): # corresponding StaticGen pg in AMS dg_ams = rtn.get(src='pg', attr='v', idx=stg_dg_idx) # --- pref --- - sa.DG.set(src='pref0', attr='v', idx=dg_idx, value=dg_ams) + sa.DG.set(src='pref0', idx=dg_idx, attr='v', value=dg_ams) # --- RenGen: seems unnecessary --- # TODO: which models/params are used to control output and auxillary power? @@ -625,7 +625,7 @@ def send(self, adsys=None, routine=None): # --- other scenarios --- if _dest_check(mname=mname_ads, pname=pname_ads, idx=idx_ads, adsys=sa): - mdl_ads.set(src=pname_ads, attr='v', idx=idx_ads, value=var_ams.v) + mdl_ads.set(src=pname_ads, idx=idx_ads, attr='v', value=var_ams.v) logger.warning(f'Send <{vname_ams}> to {mname_ads}.{pname_ads}') return True @@ -695,8 +695,8 @@ def receive(self, adsys=None, routine=None, no_update=False): idx=link['stg_idx'].values) # NOTE: only update u if changed actually u0_rtn = rtn.get(src=vname_ams, attr='v', idx=link['stg_idx'].values).copy() - rtn.set(src=vname_ams, attr='v', idx=link['stg_idx'].values, value=u_stg) - rtn.set(src=vname_ams, attr='v', idx=link['stg_idx'].values, value=u_dyg) + rtn.set(src=vname_ams, idx=link['stg_idx'].values, attr='v', value=u_stg) + rtn.set(src=vname_ams, idx=link['stg_idx'].values, attr='v', value=u_dyg) u_rtn = rtn.get(src=vname_ams, attr='v', idx=link['stg_idx'].values).copy() if not np.array_equal(u0_rtn, u_rtn): pname_to_update.append(vname_ams) @@ -733,8 +733,8 @@ def receive(self, adsys=None, routine=None, no_update=False): # Sync StaticGen.p first, then overwrite the ones with dynamic generator p_stg = sa.StaticGen.get(src='p', attr='v', idx=link['stg_idx'].values) - rtn.set(src=vname_ams, attr='v', idx=link['stg_idx'].values, value=p_stg) - rtn.set(src=vname_ams, attr='v', idx=link['stg_idx'].values, value=p_dyg) + rtn.set(src=vname_ams, idx=link['stg_idx'].values, attr='v', value=p_stg) + rtn.set(src=vname_ams, idx=link['stg_idx'].values, attr='v', value=p_dyg) pname_to_update.append(vname_ams) @@ -749,7 +749,7 @@ def receive(self, adsys=None, routine=None, no_update=False): # --- other scenarios --- if _dest_check(mname=mname_ads, pname=pname_ads, idx=idx_ads, adsys=sa): v_ads = mdl_ads.get(src=pname_ads, attr='v', idx=idx_ads) - rtn.set(src=vname_ams, attr='v', idx=idx_ads, value=v_ads) + rtn.set(src=vname_ams, idx=idx_ads, attr='v', value=v_ads) pname_to_update.append(vname_ams) logger.warning(f'Receive <{vname_ams}> from {mname_ads}.{pname_ads}') diff --git a/ams/routines/dcopf.py b/ams/routines/dcopf.py index c2d7b9cb..7b5cf3b5 100644 --- a/ams/routines/dcopf.py +++ b/ams/routines/dcopf.py @@ -312,9 +312,9 @@ def unpack(self, **kwargs): pass # NOTE: only unpack the variables that are in the model or group try: - var.owner.set(src=var.src, attr='v', idx=idx, value=var.v) + var.owner.set(src=var.src, idx=idx, attr='v', value=var.v) except (KeyError, TypeError): - logger.error(f'Failed to unpack {var.name} to {var.owner.name}') + logger.error(f'Failed to unpack <{var}> to <{var.owner.class_name}>.') pass # label the most recent solved routine @@ -340,16 +340,16 @@ def dc2ac(self, kloss=1.0, **kwargs): pq_idx = self.system.StaticLoad.get_idx() pd0 = self.system.StaticLoad.get(src='p0', attr='v', idx=pq_idx).copy() qd0 = self.system.StaticLoad.get(src='q0', attr='v', idx=pq_idx).copy() - self.system.StaticLoad.alter(src='p0', idx=pq_idx, value=pd0 * kloss) - self.system.StaticLoad.alter(src='q0', idx=pq_idx, value=qd0 * kloss) + self.system.StaticLoad.set(src='p0', idx=pq_idx, attr='v', value=pd0 * kloss) + self.system.StaticLoad.set(src='q0', idx=pq_idx, attr='v', value=qd0 * kloss) ACOPF = self.system.ACOPF # run ACOPF ACOPF.run() # self.exec_time += ACOPF.exec_time # scale load back - self.system.StaticLoad.alter(src='p0', idx=pq_idx, value=pd0) - self.system.StaticLoad.alter(src='q0', idx=pq_idx, value=qd0) + self.system.StaticLoad.set(src='p0', idx=pq_idx, value=pd0) + self.system.StaticLoad.set(src='q0', idx=pq_idx, value=qd0) if not ACOPF.exit_code == 0: logger.warning(' did not converge, conversion failed.') # NOTE: mock results to fit interface with ANDES diff --git a/ams/routines/routine.py b/ams/routines/routine.py index 3087ef5b..55880330 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -173,7 +173,7 @@ def set(self, src: str, idx, attr: str = "v", value=0.0): owner = self.__dict__[src].owner src0 = self.__dict__[src].src try: - res = owner.alter(src=src0, idx=idx, value=value) + res = owner.set(src=src0, idx=idx, attr=attr, value=value) return res except KeyError as e: msg = f"Failed to set <{src0}> in <{owner.class_name}>. " diff --git a/ams/routines/rted.py b/ams/routines/rted.py index 8b5eb391..c04ce86f 100644 --- a/ams/routines/rted.py +++ b/ams/routines/rted.py @@ -199,20 +199,20 @@ def dc2ac(self, kloss=1.0, **kwargs): pq_idx = self.system.StaticLoad.get_idx() pd0 = self.system.StaticLoad.get(src='p0', attr='v', idx=pq_idx).copy() qd0 = self.system.StaticLoad.get(src='q0', attr='v', idx=pq_idx).copy() - self.system.StaticLoad.alter(src='p0', idx=pq_idx, value=pd0 * kloss) - self.system.StaticLoad.alter(src='q0', idx=pq_idx, value=qd0 * kloss) + self.system.StaticLoad.set(src='p0', idx=pq_idx, attr='v', value=pd0 * kloss) + self.system.StaticLoad.set(src='q0', idx=pq_idx, attr='v', value=qd0 * kloss) # preserve generator reserve ACOPF = self.system.ACOPF pmin = pmin0 + self.prd.v pmax = pmax0 - self.pru.v - self.system.StaticGen.alter(src='pmin', idx=pr_idx, value=pmin) - self.system.StaticGen.alter(src='pmax', idx=pr_idx, value=pmax) - self.system.StaticGen.alter(src='p0', idx=pr_idx, value=self.pg.v) + self.system.StaticGen.set(src='pmin', idx=pr_idx, attr='v', value=pmin) + self.system.StaticGen.set(src='pmax', idx=pr_idx, attr='v', value=pmax) + self.system.StaticGen.set(src='p0', idx=pr_idx, attr='v', value=self.pg.v) # run ACOPF ACOPF.run() # scale load back - self.system.StaticLoad.alter(src='p0', idx=pq_idx, value=pd0) - self.system.StaticLoad.alter(src='q0', idx=pq_idx, value=qd0) + self.system.StaticLoad.set(src='p0', idx=pq_idx, attr='v', value=pd0) + self.system.StaticLoad.set(src='q0', idx=pq_idx, attr='v', value=qd0) if not ACOPF.exit_code == 0: logger.warning(' did not converge, conversion failed.') # NOTE: mock results to fit interface with ANDES @@ -233,9 +233,9 @@ def dc2ac(self, kloss=1.0, **kwargs): self.exec_time = exec_time # reset pmin, pmax, p0 - self.system.StaticGen.alter(src='pmin', idx=pr_idx, value=pmin0) - self.system.StaticGen.alter(src='pmax', idx=pr_idx, value=pmax0) - self.system.StaticGen.alter(src='p0', idx=pr_idx, value=p00) + self.system.StaticGen.set(src='pmin', idx=pr_idx, attr='v', value=pmin0) + self.system.StaticGen.set(src='pmax', idx=pr_idx, attr='v', value=pmax0) + self.system.StaticGen.set(src='p0', idx=pr_idx, attr='v', value=p00) # --- set status --- self.system.recent = self diff --git a/ams/routines/uc.py b/ams/routines/uc.py index 50465422..f1b29827 100644 --- a/ams/routines/uc.py +++ b/ams/routines/uc.py @@ -307,7 +307,7 @@ def _initial_guess(self): off_gen = f'{g_idx}' else: off_gen = ', '.join(g_idx) - self.system.StaticGen.set(src='u', attr='v', idx=g_idx, value=ug0) + self.system.StaticGen.set(src='u', idx=g_idx, attr='v', value=ug0) logger.warning(f"As initial commitment guess, turn off StaticGen: {off_gen}") return g_idx diff --git a/examples/demonstration/demo_AGC.ipynb b/examples/demonstration/demo_AGC.ipynb index 08eeffea..445a8db4 100644 --- a/examples/demonstration/demo_AGC.ipynb +++ b/examples/demonstration/demo_AGC.ipynb @@ -450,8 +450,8 @@ " columns=out_cols)\n", "\n", "# --- AMS settings ---\n", - "sp.SFR.alter(src='du', idx=sp.SFR.idx.v, value=0.0015*np.ones(sp.SFR.n))\n", - "sp.SFR.alter(src='dd', idx=sp.SFR.idx.v, value=0.0015*np.ones(sp.SFR.n))\n", + "sp.SFR.set(src='du', idx=sp.SFR.idx.v, value=0.0015*np.ones(sp.SFR.n))\n", + "sp.SFR.set(src='dd', idx=sp.SFR.idx.v, value=0.0015*np.ones(sp.SFR.n))\n", "\n", "# --- ANDES settings ---\n", "sa.TDS.config.no_tqdm = True # turn off ANDES progress bar\n", @@ -464,13 +464,13 @@ "# NOTE: might run into error if there exists a TurbineGov model that does not have \"VMAX\"\n", "tbgov_src = [mdl.idx.v for mdl in sa.TurbineGov.models.values()]\n", "tbgov_idx = list(chain.from_iterable(tbgov_src))\n", - "sa.TurbineGov.alter(src='VMAX', idx=tbgov_idx,\n", + "sa.TurbineGov.set(src='VMAX', idx=tbgov_idx,\n", " value=9999 * np.ones(sa.TurbineGov.n),)\n", - "sa.TurbineGov.alter(src='VMIN', idx=tbgov_idx,\n", + "sa.TurbineGov.set(src='VMIN', idx=tbgov_idx,\n", " value=np.zeros(sa.TurbineGov.n),)\n", "syg_src = [mdl.idx.v for mdl in sa.SynGen.models.values()]\n", "syg_idx = list(chain.from_iterable(syg_src))\n", - "sa.SynGen.alter(src='ra', idx=syg_idx,\n", + "sa.SynGen.set(src='ra', idx=syg_idx,\n", " value=np.zeros(sa.SynGen.n),)\n", "\n", "# use constant power model for PQ\n", @@ -664,8 +664,8 @@ " # use 5-min average load in dispatch solution\n", " load_avg = load_coeff[t:t+RTED_interval].mean()\n", " # set load in to AMS\n", - " sp.PQ.alter(src='p0', idx=pq_idx, value=load_avg * p0_sp)\n", - " sp.PQ.alter(src='q0', idx=pq_idx, value=load_avg * q0_sp)\n", + " sp.PQ.set(src='p0', idx=pq_idx, value=load_avg * p0_sp)\n", + " sp.PQ.set(src='q0', idx=pq_idx, value=load_avg * q0_sp)\n", " print(f\"--AMS: update disaptch load with factor {load_avg:.6f}.\")\n", "\n", " # get dynamic generator output from TDS\n", @@ -703,17 +703,17 @@ "\n", " # set into governor, Exclude NaN values for governor index\n", " gov_to_set = {gov: pgov for gov, pgov in zip(maptab['gov_idx'], maptab['pgov']) if bool(gov)}\n", - " sa.TurbineGov.alter(src='pref0', idx=list(gov_to_set.keys()), value=list(gov_to_set.values()))\n", + " sa.TurbineGov.set(src='pref0', idx=list(gov_to_set.keys()), value=list(gov_to_set.values()))\n", " print(f\"--ANDES: update TurbineGov reference.\")\n", "\n", " # set into dg, Exclude NaN values for dg index\n", " dg_to_set = {dg: pdg for dg, pdg in zip(maptab['dg_idx'], maptab['pdg']) if bool(dg)}\n", - " sa.DG.alter(src='pref0', idx=list(dg_to_set.keys()), value=list(dg_to_set.values()))\n", + " sa.DG.set(src='pref0', idx=list(dg_to_set.keys()), value=list(dg_to_set.values()))\n", " print(f\"--ANDES: update DG reference.\")\n", "\n", " # set into rg, Exclude NaN values for rg index\n", " rg_to_set = {rg: prg for rg, prg in zip(maptab['rg_idx'], maptab['prg']) if bool(rg)}\n", - " sa.RenGen.alter(src='Pref', idx=list(rg_to_set.keys()), value=list(rg_to_set.values()))\n", + " sa.RenGen.set(src='Pref', idx=list(rg_to_set.keys()), value=list(rg_to_set.values()))\n", " print(f\"--ANDES: update RenGen reference.\")\n", "\n", " # record dispatch data\n", @@ -745,22 +745,22 @@ "\n", " # set into governor, Exclude NaN values for governor index\n", " agov_to_set = {gov: agov for gov, agov in zip(maptab['gov_idx'], maptab['agov']) if bool(gov)}\n", - " sa.TurbineGov.alter(src='paux0', idx=list(agov_to_set.keys()), value=list(agov_to_set.values()))\n", + " sa.TurbineGov.set(src='paux0', idx=list(agov_to_set.keys()), value=list(agov_to_set.values()))\n", "\n", " # set into dg, Exclude NaN values for dg index\n", " adg_to_set = {dg: adg for dg, adg in zip(maptab['dg_idx'], maptab['adg']) if bool(dg)}\n", - " sa.DG.alter(src='Pext0', idx=list(adg_to_set.keys()), value=list(adg_to_set.values()))\n", + " sa.DG.set(src='Pext0', idx=list(adg_to_set.keys()), value=list(adg_to_set.values()))\n", "\n", " # set into rg, Exclude NaN values for rg index\n", " arg_to_set = {rg: arg + prg for rg, arg,\n", " prg in zip(maptab['rg_idx'], maptab['arg'], maptab['prg']) if bool(rg)}\n", - " sa.RenGen.alter(src='Pref', idx=list(arg_to_set.keys()), value=list(arg_to_set.values()))\n", + " sa.RenGen.set(src='Pref', idx=list(arg_to_set.keys()), value=list(arg_to_set.values()))\n", "\n", " # --- TDS interval ---\n", " if t > 0: # --- run TDS ---\n", " # set laod into PQ.Ppf and PQ.Qpf\n", - " sa.PQ.alter(src='Ppf', idx=pq_idx, value=load_coeff[t] * p0_sa)\n", - " sa.PQ.alter(src='Qpf', idx=pq_idx, value=load_coeff[t] * q0_sa)\n", + " sa.PQ.set(src='Ppf', idx=pq_idx, value=load_coeff[t] * p0_sa)\n", + " sa.PQ.set(src='Qpf', idx=pq_idx, value=load_coeff[t] * q0_sa)\n", " sa.TDS.config.tf = t\n", " sa.TDS.run()\n", " # Update AGC PI controller\n", @@ -785,16 +785,16 @@ " break\n", " else: # --- init TDS ---\n", " # set pg to StaticGen.p0\n", - " sa.StaticGen.alter(src='p0', idx=sp.RTED.pg.get_idx(), value=sp.RTED.pg.v)\n", + " sa.StaticGen.set(src='p0', idx=sp.RTED.pg.get_idx(), attr='v', value=sp.RTED.pg.v)\n", " # set Bus.v to StaticGen.v\n", " bus_stg = sp.StaticGen.get(src='bus', attr='v', idx=sp.StaticGen.get_idx())\n", " v_stg = sp.Bus.get(src='v', attr='v', idx=bus_stg)\n", - " sa.StaticGen.alter(src='v0', idx=sp.StaticGen.get_idx(), value=v_stg)\n", + " sa.StaticGen.set(src='v0', idx=sp.StaticGen.get_idx(), attr='v', value=v_stg)\n", " # set vBus to Bus\n", - " sa.Bus.alter(src='v0', idx=sp.RTED.vBus.get_idx(), value=sp.RTED.vBus.v)\n", + " sa.Bus.set(src='v0', idx=sp.RTED.vBus.get_idx(), attr='v', value=sp.RTED.vBus.v)\n", " # set load into PQ.p0 and PQ.q0\n", - " sa.PQ.alter(src='p0', idx=pq_idx, value=load_coeff[t] * p0_sa)\n", - " sa.PQ.alter(src='q0', idx=pq_idx, value=load_coeff[t] * q0_sa)\n", + " sa.PQ.set(src='p0', idx=pq_idx, attr='v', value=load_coeff[t] * p0_sa)\n", + " sa.PQ.set(src='q0', idx=pq_idx, attr='v', value=load_coeff[t] * q0_sa)\n", " sa.PFlow.run() # run power flow\n", " sa.TDS.init() # initialize TDS\n", "\n", diff --git a/tests/test_interop.py b/tests/test_interop.py index d1083e3e..5961c8dd 100644 --- a/tests/test_interop.py +++ b/tests/test_interop.py @@ -158,8 +158,8 @@ def test_data_exchange(self): no_output=True, default_config=True,) # alleviate limiter - sa.TGOV1.set(src='VMAX', attr='v', idx=sa.TGOV1.idx.v, value=100*np.ones(sa.TGOV1.n)) - sa.TGOV1.set(src='VMIN', attr='v', idx=sa.TGOV1.idx.v, value=np.zeros(sa.TGOV1.n)) + sa.TGOV1.set(src='VMAX', idx=sa.TGOV1.idx.v, attr='v', value=100*np.ones(sa.TGOV1.n)) + sa.TGOV1.set(src='VMIN', idx=sa.TGOV1.idx.v, attr='v', value=np.zeros(sa.TGOV1.n)) # --- test before PFlow --- self.sp.dyn.send(adsys=sa, routine='RTED') diff --git a/tests/test_rtn_dcopf.py b/tests/test_rtn_dcopf.py index 0d80127f..7903f553 100644 --- a/tests/test_rtn_dcopf.py +++ b/tests/test_rtn_dcopf.py @@ -26,7 +26,7 @@ def test_trip_gen(self): Test generator tripping. """ stg = 'PV_1' - self.ss.StaticGen.set(src='u', attr='v', idx=stg, value=0) + self.ss.StaticGen.set(src='u', idx=stg, attr='v', value=0) self.ss.DCOPF.update() self.ss.DCOPF.run(solver='CLARABEL') @@ -35,7 +35,7 @@ def test_trip_gen(self): 0, places=6, msg="Generator trip does not take effect!") - self.ss.StaticGen.alter(src='u', idx=stg, value=1) # reset + self.ss.StaticGen.set(src='u', idx=stg, attr='v', value=1) # reset def test_trip_line(self): """ diff --git a/tests/test_rtn_ed.py b/tests/test_rtn_ed.py index 38c2a619..cadefd3f 100644 --- a/tests/test_rtn_ed.py +++ b/tests/test_rtn_ed.py @@ -45,7 +45,7 @@ def test_trip_gen(self): # b) ensure StaticGen.u does not take effect # NOTE: in ED, `EDTSlot.ug` is used instead of `StaticGen.u` - self.ss.StaticGen.set(src='u', attr='v', idx=stg, value=0) + self.ss.StaticGen.set(src='u', idx=stg, attr='v', value=0) self.ss.ED.update() self.ss.ED.run(solver='CLARABEL') @@ -54,7 +54,7 @@ def test_trip_gen(self): np.testing.assert_array_less(np.zeros_like(pg_pv1), pg_pv1, err_msg="Generator trip take effect, which is unexpected!") - self.ss.StaticGen.alter(src='u', idx=stg, value=1) # reset + self.ss.StaticGen.set(src='u', idx=stg, attr='v', value=1) # reset def test_trip_line(self): """ @@ -136,7 +136,7 @@ def test_trip_gen(self): # b) ensure StaticGen.u does not take effect # NOTE: in ED, `EDTSlot.ug` is used instead of `StaticGen.u` - self.ss.StaticGen.set(src='u', attr='v', idx=stg, value=0) + self.ss.StaticGen.set(src='u', idx=stg, attr='v', value=0) self.ss.ED.update() self.ss.ED.run(solver='CLARABEL') @@ -145,7 +145,7 @@ def test_trip_gen(self): np.testing.assert_array_less(np.zeros_like(pg_pv1), pg_pv1, err_msg="Generator trip take effect, which is unexpected!") - self.ss.StaticGen.alter(src='u', idx=stg, value=1) # reset + self.ss.StaticGen.set(src='u', idx=stg, attr='v', value=1) # reset def test_line_trip(self): """ diff --git a/tests/test_rtn_pflow.py b/tests/test_rtn_pflow.py index 1f7be489..c89c34fa 100644 --- a/tests/test_rtn_pflow.py +++ b/tests/test_rtn_pflow.py @@ -24,7 +24,7 @@ def test_trip_gen(self): Test generator tripping. """ stg = 2 - self.ss.StaticGen.set(src='u', attr='v', idx=stg, value=0) + self.ss.StaticGen.set(src='u', idx=stg, attr='v', value=0) self.ss.DCPF.update() self.ss.DCPF.run() @@ -33,7 +33,7 @@ def test_trip_gen(self): 0, places=6, msg="Generator trip does not take effect!") - self.ss.StaticGen.alter(src='u', idx=stg, value=1) # reset + self.ss.StaticGen.set(src='u', idx=stg, attr='v', value=1) # reset def test_trip_line(self): """ @@ -96,7 +96,7 @@ def test_trip_gen(self): Test generator tripping. """ stg = 2 - self.ss.StaticGen.set(src='u', attr='v', idx=stg, value=0) + self.ss.StaticGen.set(src='u', idx=stg, attr='v', value=0) self.ss.PFlow.update() self.ss.PFlow.run() @@ -105,7 +105,7 @@ def test_trip_gen(self): 0, places=6, msg="Generator trip does not take effect!") - self.ss.StaticGen.alter(src='u', idx=stg, value=1) + self.ss.StaticGen.set(src='u', idx=stg, attr='v', value=1) def test_trip_line(self): """ @@ -168,7 +168,7 @@ def test_trip_gen(self): Test generator tripping. """ stg = 2 - self.ss.StaticGen.set(src='u', attr='v', idx=stg, value=0) + self.ss.StaticGen.set(src='u', idx=stg, attr='v', value=0) self.ss.ACOPF.update() self.ss.ACOPF.run() @@ -177,7 +177,7 @@ def test_trip_gen(self): 0, places=6, msg="Generator trip does not take effect!") - self.ss.StaticGen.alter(src='u', idx=stg, value=1) + self.ss.StaticGen.set(src='u', idx=stg, attr='v', value=1) def test_trip_line(self): """ diff --git a/tests/test_rtn_rted.py b/tests/test_rtn_rted.py index 33fa1b59..f112ada7 100644 --- a/tests/test_rtn_rted.py +++ b/tests/test_rtn_rted.py @@ -30,7 +30,7 @@ def test_trip_gen(self): Test generator tripping. """ stg = 'PV_1' - self.ss.StaticGen.set(src='u', attr='v', idx=stg, value=0) + self.ss.StaticGen.set(src='u', idx=stg, attr='v', value=0) self.ss.RTED.update() self.ss.RTED.run(solver='CLARABEL') @@ -39,7 +39,7 @@ def test_trip_gen(self): 0, places=6, msg="Generator trip does not take effect!") - self.ss.StaticGen.alter(src='u', idx=stg, value=1) # reset + self.ss.StaticGen.set(src='u', idx=stg, attr='v', value=1) # reset def test_trip_line(self): """ @@ -124,7 +124,7 @@ def test_trip_gen(self): Test generator tripping. """ stg = 'PV_1' - self.ss.StaticGen.set(src='u', attr='v', idx=stg, value=0) + self.ss.StaticGen.set(src='u', idx=stg, attr='v', value=0) self.ss.RTEDES.update() self.ss.RTEDES.run(solver='SCIP') @@ -133,7 +133,7 @@ def test_trip_gen(self): 0, places=6, msg="Generator trip does not take effect!") - self.ss.StaticGen.alter(src='u', idx=stg, value=1) + self.ss.StaticGen.set(src='u', idx=stg, attr='v', value=1) @skip_unittest_without_MIP def test_trip_line(self): From 86cf43e50dc3d8ad9720999d86a55042c499c6fb Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 6 Nov 2024 10:14:28 -0500 Subject: [PATCH 144/181] Fix alter and set mis-use issue --- examples/demonstration/demo_AGC.ipynb | 4 ++-- examples/ex1.ipynb | 6 +++--- examples/ex2.ipynb | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/demonstration/demo_AGC.ipynb b/examples/demonstration/demo_AGC.ipynb index 445a8db4..6e1f0b91 100644 --- a/examples/demonstration/demo_AGC.ipynb +++ b/examples/demonstration/demo_AGC.ipynb @@ -450,8 +450,8 @@ " columns=out_cols)\n", "\n", "# --- AMS settings ---\n", - "sp.SFR.set(src='du', idx=sp.SFR.idx.v, value=0.0015*np.ones(sp.SFR.n))\n", - "sp.SFR.set(src='dd', idx=sp.SFR.idx.v, value=0.0015*np.ones(sp.SFR.n))\n", + "sp.SFR.set(src='du', idx=sp.SFR.idx.v, attr='v', value=0.0015*np.ones(sp.SFR.n))\n", + "sp.SFR.set(src='dd', idx=sp.SFR.idx.v, attr='v', value=0.0015*np.ones(sp.SFR.n))\n", "\n", "# --- ANDES settings ---\n", "sa.TDS.config.no_tqdm = True # turn off ANDES progress bar\n", diff --git a/examples/ex1.ipynb b/examples/ex1.ipynb index b590e8a7..b114b680 100644 --- a/examples/ex1.ipynb +++ b/examples/ex1.ipynb @@ -422,13 +422,13 @@ "Then, one can solve it by calling ``run()``.\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", + "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": 9, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -443,7 +443,7 @@ } ], "source": [ - "ams.shared.INSTALLED_SOLVERS" + "ams.shared.installed_solvers" ] }, { diff --git a/examples/ex2.ipynb b/examples/ex2.ipynb index a40ec62b..9d8beb2d 100644 --- a/examples/ex2.ipynb +++ b/examples/ex2.ipynb @@ -957,7 +957,7 @@ } ], "source": [ - "sp.StaticGen.alter(src='u', idx='PV_1', value=0)" + "sp.StaticGen.set(src='u', idx='PV_1', attr='v', value=0)" ] }, { From 841621c0ec3fb03948e4610456b0388567c2f535 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 6 Nov 2024 10:22:11 -0500 Subject: [PATCH 145/181] Fix alter and set mis-use issue --- examples/demonstration/demo_AGC.ipynb | 32 +++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/examples/demonstration/demo_AGC.ipynb b/examples/demonstration/demo_AGC.ipynb index 6e1f0b91..96f480ff 100644 --- a/examples/demonstration/demo_AGC.ipynb +++ b/examples/demonstration/demo_AGC.ipynb @@ -464,14 +464,14 @@ "# NOTE: might run into error if there exists a TurbineGov model that does not have \"VMAX\"\n", "tbgov_src = [mdl.idx.v for mdl in sa.TurbineGov.models.values()]\n", "tbgov_idx = list(chain.from_iterable(tbgov_src))\n", - "sa.TurbineGov.set(src='VMAX', idx=tbgov_idx,\n", - " value=9999 * np.ones(sa.TurbineGov.n),)\n", - "sa.TurbineGov.set(src='VMIN', idx=tbgov_idx,\n", - " value=np.zeros(sa.TurbineGov.n),)\n", + "sa.TurbineGov.set(src='VMAX', idx=tbgov_idx, attr='v',\n", + " value=9999 * np.ones(sa.TurbineGov.n),)\n", + "sa.TurbineGov.set(src='VMIN', idx=tbgov_idx, attr='v',\n", + " value=np.zeros(sa.TurbineGov.n),)\n", "syg_src = [mdl.idx.v for mdl in sa.SynGen.models.values()]\n", "syg_idx = list(chain.from_iterable(syg_src))\n", - "sa.SynGen.set(src='ra', idx=syg_idx,\n", - " value=np.zeros(sa.SynGen.n),)\n", + "sa.SynGen.set(src='ra', idx=syg_idx, attr='v',\n", + " value=np.zeros(sa.SynGen.n),)\n", "\n", "# use constant power model for PQ\n", "sa.PQ.config.p2p = 1\n", @@ -664,8 +664,8 @@ " # use 5-min average load in dispatch solution\n", " load_avg = load_coeff[t:t+RTED_interval].mean()\n", " # set load in to AMS\n", - " sp.PQ.set(src='p0', idx=pq_idx, value=load_avg * p0_sp)\n", - " sp.PQ.set(src='q0', idx=pq_idx, value=load_avg * q0_sp)\n", + " sp.PQ.set(src='p0', idx=pq_idx, attr='v', value=load_avg * p0_sp)\n", + " sp.PQ.set(src='q0', idx=pq_idx, attr='v', value=load_avg * q0_sp)\n", " print(f\"--AMS: update disaptch load with factor {load_avg:.6f}.\")\n", "\n", " # get dynamic generator output from TDS\n", @@ -703,17 +703,17 @@ "\n", " # set into governor, Exclude NaN values for governor index\n", " gov_to_set = {gov: pgov for gov, pgov in zip(maptab['gov_idx'], maptab['pgov']) if bool(gov)}\n", - " sa.TurbineGov.set(src='pref0', idx=list(gov_to_set.keys()), value=list(gov_to_set.values()))\n", + " sa.TurbineGov.set(src='pref0', idx=list(gov_to_set.keys()), attr='v', value=list(gov_to_set.values()))\n", " print(f\"--ANDES: update TurbineGov reference.\")\n", "\n", " # set into dg, Exclude NaN values for dg index\n", " dg_to_set = {dg: pdg for dg, pdg in zip(maptab['dg_idx'], maptab['pdg']) if bool(dg)}\n", - " sa.DG.set(src='pref0', idx=list(dg_to_set.keys()), value=list(dg_to_set.values()))\n", + " sa.DG.set(src='pref0', idx=list(dg_to_set.keys()), attr='v', value=list(dg_to_set.values()))\n", " print(f\"--ANDES: update DG reference.\")\n", "\n", " # set into rg, Exclude NaN values for rg index\n", " rg_to_set = {rg: prg for rg, prg in zip(maptab['rg_idx'], maptab['prg']) if bool(rg)}\n", - " sa.RenGen.set(src='Pref', idx=list(rg_to_set.keys()), value=list(rg_to_set.values()))\n", + " sa.RenGen.set(src='Pref', idx=list(rg_to_set.keys()), attr='v', value=list(rg_to_set.values()))\n", " print(f\"--ANDES: update RenGen reference.\")\n", "\n", " # record dispatch data\n", @@ -745,22 +745,22 @@ "\n", " # set into governor, Exclude NaN values for governor index\n", " agov_to_set = {gov: agov for gov, agov in zip(maptab['gov_idx'], maptab['agov']) if bool(gov)}\n", - " sa.TurbineGov.set(src='paux0', idx=list(agov_to_set.keys()), value=list(agov_to_set.values()))\n", + " sa.TurbineGov.set(src='paux0', idx=list(agov_to_set.keys()), attr='v', value=list(agov_to_set.values()))\n", "\n", " # set into dg, Exclude NaN values for dg index\n", " adg_to_set = {dg: adg for dg, adg in zip(maptab['dg_idx'], maptab['adg']) if bool(dg)}\n", - " sa.DG.set(src='Pext0', idx=list(adg_to_set.keys()), value=list(adg_to_set.values()))\n", + " sa.DG.set(src='Pext0', idx=list(adg_to_set.keys()), attr='v', value=list(adg_to_set.values()))\n", "\n", " # set into rg, Exclude NaN values for rg index\n", " arg_to_set = {rg: arg + prg for rg, arg,\n", " prg in zip(maptab['rg_idx'], maptab['arg'], maptab['prg']) if bool(rg)}\n", - " sa.RenGen.set(src='Pref', idx=list(arg_to_set.keys()), value=list(arg_to_set.values()))\n", + " sa.RenGen.set(src='Pref', idx=list(arg_to_set.keys()), attr='v', value=list(arg_to_set.values()))\n", "\n", " # --- TDS interval ---\n", " if t > 0: # --- run TDS ---\n", " # set laod into PQ.Ppf and PQ.Qpf\n", - " sa.PQ.set(src='Ppf', idx=pq_idx, value=load_coeff[t] * p0_sa)\n", - " sa.PQ.set(src='Qpf', idx=pq_idx, value=load_coeff[t] * q0_sa)\n", + " sa.PQ.set(src='Ppf', idx=pq_idx, attr='v', value=load_coeff[t] * p0_sa)\n", + " sa.PQ.set(src='Qpf', idx=pq_idx, attr='v', value=load_coeff[t] * q0_sa)\n", " sa.TDS.config.tf = t\n", " sa.TDS.run()\n", " # Update AGC PI controller\n", From 33d52c1924361b4ecea8bff07224594df58e7417 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 6 Nov 2024 10:28:53 -0500 Subject: [PATCH 146/181] Fix alter and set mis-use issue --- examples/ex2.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ex2.ipynb b/examples/ex2.ipynb index 9d8beb2d..7979d9f5 100644 --- a/examples/ex2.ipynb +++ b/examples/ex2.ipynb @@ -1556,7 +1556,7 @@ } ], "source": [ - "spc.RTED.alter(src='rate_a', idx=['Line_3'], value=0.6)" + "spc.RTED.set(src='rate_a', idx=['Line_3'], attr='v', value=0.6)" ] }, { From 1c8e7cd80d49bceda81afd4edd9c7f302bfb1ab2 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 6 Nov 2024 10:49:50 -0500 Subject: [PATCH 147/181] Refactor action pythonapp config --- .github/workflows/pythonapp.yml | 51 ++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index ee6bad1a..cc2ca17b 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -3,12 +3,51 @@ name: Python application on: [push, pull_request] jobs: + lint: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.11] # Only run flake8 on Python 3.11 + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . + + test-notebooks: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.11] # Only test notebooks on Python 3.11 + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install nbmake + - name: Test notebooks + run: | + pytest --nbmake examples --ignore=examples/verification + build: name: AMS Tests strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: [3.9, 3.10, 3.11, 3.12] + python-version: [3.9, 3.10, 3.11, 3.12, 3.13] runs-on: ubuntu-latest @@ -35,20 +74,10 @@ jobs: name: Run pip check run: | pip check || true # Allow pip check to fail without failing the job - - name: Lint with flake8 for pull requests - if: github.event_name == 'pull_request' - run: | - pip install flake8 # specify flake8 to avoid unknown error - # stop the build if there are Python syntax errors or undefined names - flake8 . - shell: bash -el {0} name: Test with pytest run: | pytest - - shell: bash -el {0} - name: Test notebooks. - run: | - pytest --nbmake examples --ignore=examples/verification - name: Build a distribution if tagged if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') && matrix.os == 'ubuntu-latest' && matrix.python-version == '3.9' run: | From c73b7f8a9259183914be652699ecda07857c91cb Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 6 Nov 2024 11:37:07 -0500 Subject: [PATCH 148/181] Improve performacne by remove unnecessary shape access of Param --- ams/core/param.py | 7 ++++++- ams/opt/omodel.py | 4 +--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/ams/core/param.py b/ams/core/param.py index 6f170a68..50c0cc4c 100644 --- a/ams/core/param.py +++ b/ams/core/param.py @@ -196,7 +196,12 @@ def shape(self): """ Return the shape of the parameter. """ - return np.shape(self.v) + if self.is_ext: + return np.shape(self._v) + elif self.no_parse: + return None + else: + return np.shape(self.v) @property def dtype(self): diff --git a/ams/opt/omodel.py b/ams/opt/omodel.py index 12904f14..d5ea65f4 100644 --- a/ams/opt/omodel.py +++ b/ams/opt/omodel.py @@ -334,9 +334,7 @@ def parse(self): Returns True if the parsing is successful, False otherwise. """ sub_map = self.om.rtn.syms.sub_map - shape = np.shape(self.v) - # NOTE: it seems that there is no need to use re.sub here - code_param = f"param(shape={shape}, **config)" + code_param = "param(**config)" for pattern, replacement, in sub_map.items(): try: code_param = re.sub(pattern, replacement, code_param) From 1a996657ff1651aca64b7bb6c3a56b7a63853e35 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 6 Nov 2024 11:46:29 -0500 Subject: [PATCH 149/181] Update years in LICENSE --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 6f4c393f..b9d6018a 100644 --- a/LICENSE +++ b/LICENSE @@ -670,7 +670,7 @@ Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: - AMS Copyright (C) 2023 Jinning Wang + AMS Copyright (C) 2023 - 2024 Jinning Wang This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. From aa796b309b6cf1765e31ebb1ed94719599a2ac56 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 6 Nov 2024 11:46:43 -0500 Subject: [PATCH 150/181] Fix setuptool in projecttoml --- pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 175bbea5..47a81659 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,4 +66,7 @@ VCS = "git" style = "pep440-post" versionfile_source = "ams/_version.py" versionfile_build = "ams/_version.py" -tag_prefix = "v" \ No newline at end of file +tag_prefix = "v" + +[tool.setuptools] +packages = ["ams"] \ No newline at end of file From d60e15c4cc5919610dd46a3c80cd9d0d89df1839 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 6 Nov 2024 14:18:05 -0500 Subject: [PATCH 151/181] Fix pythonapp --- .github/workflows/pythonapp.yml | 60 +++++++++++++++++---------------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index cc2ca17b..11cb14dd 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -3,42 +3,40 @@ name: Python application on: [push, pull_request] jobs: - lint: + pretest: + name: AMS Pretest + runs-on: ubuntu-latest - strategy: - matrix: - python-version: [3.11] # Only run flake8 on Python 3.11 + + if: ${{ github.ref != 'refs/heads/misc' }} + steps: - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + - uses: conda-incubator/setup-miniconda@v3 with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies + python-version: 3.11 + mamba-version: "*" + miniforge-version: "latest" + channels: conda-forge,defaults + channel-priority: true + activate-environment: anaconda-client-env + - shell: bash -el {0} + name: Install dependencies run: | - python -m pip install --upgrade pip - pip install flake8 - - name: Lint with flake8 + mamba install -y nbmake pytest-xdist line_profiler # add'l packages for notebook tests. + mamba install --file requirements.txt --file requirements-extra.txt + python -m pip install pyscipopt + python -m pip install -e . + - shell: bash -el {0} + name: Run pip check run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . - - test-notebooks: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: [3.11] # Only test notebooks on Python 3.11 - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies + pip check || true # Allow pip check to fail without failing the job + - shell: bash -el {0} + name: Test with pytest run: | - python -m pip install --upgrade pip - pip install nbmake - - name: Test notebooks + pytest + - shell: bash -el {0} + name: Test notebooks. run: | pytest --nbmake examples --ignore=examples/verification @@ -78,6 +76,10 @@ jobs: name: Test with pytest run: | pytest + - shell: bash -el {0} + name: Test notebooks. + run: | + pytest --nbmake examples --ignore=examples/verification - name: Build a distribution if tagged if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') && matrix.os == 'ubuntu-latest' && matrix.python-version == '3.9' run: | From f1158bef5261a14bc8a4888ad68c72dd2a5c902e Mon Sep 17 00:00:00 2001 From: jinningwang Date: Wed, 6 Nov 2024 15:02:37 -0500 Subject: [PATCH 152/181] Format --- ams/routines/routine.py | 63 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/ams/routines/routine.py b/ams/routines/routine.py index 55880330..cbd602ce 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -26,9 +26,68 @@ class RoutineBase: """ Class to hold descriptive routine models and data mapping. + + Attributes + ---------- + system : Optional[Type] + The system object associated with the routine. + config : Config + Configuration object for the routine. + info : Optional[str] + Information about the routine. + tex_names : OrderedDict + LaTeX names for the routine parameters. + syms : SymProcessor + Symbolic processor for the routine. + _syms : bool + Flag indicating whether symbols have been generated. + rparams : OrderedDict + Registry for RParam objects. + services : OrderedDict + Registry for service objects. + params : OrderedDict + Registry for Param objects. + vars : OrderedDict + Registry for Var objects. + constrs : OrderedDict + Registry for Constraint objects. + exprs : OrderedDict + Registry for Expression objects. + obj : Optional[Objective] + Objective of the routine. + initialized : bool + Flag indicating whether the routine has been initialized. + type : str + Type of the routine. + docum : RDocumenter + Documentation generator for the routine. + map1 : OrderedDict + Mapping from ANDES. + map2 : OrderedDict + Mapping to ANDES. + om : OModel + Optimization model for the routine. + exec_time : float + Execution time of the routine. + exit_code : int + Exit code of the routine. + converged : bool + Flag indicating whether the routine has converged. + converted : bool + Flag indicating whether AC conversion has been performed. """ def __init__(self, system=None, config=None): + """ + Initialize the routine. + + Parameters + ---------- + system : Optional[Type] + The system object associated with the routine. + config : Optional[dict] + Configuration dictionary for the routine. + """ self.system = system self.config = Config(self.class_name) self.info = None @@ -242,7 +301,7 @@ def init(self, **kwargs): Initialize the routine. Other parameters - ---------- + ---------------- force: bool Whether to force initialization regardless of the current initialization status. force_mats: bool @@ -313,7 +372,7 @@ def _post_solve(self): def run(self, **kwargs): """ Run the routine. - *args and **kwargs go to `self.solve()`. + args and kwargs go to `self.solve()`. Force initialization (`force_init=True`) will do the following: - Rebuild the system matrices From c31bb27f441a6de3b900c7621d1bd90d18dcee4b Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 7 Nov 2024 22:55:11 -0500 Subject: [PATCH 153/181] Add reference in DCOPF docstring --- ams/routines/dcopf.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ams/routines/dcopf.py b/ams/routines/dcopf.py index 7b5cf3b5..01d340ac 100644 --- a/ams/routines/dcopf.py +++ b/ams/routines/dcopf.py @@ -20,6 +20,12 @@ class DCOPF(RoutineBase): DC optimal power flow (DCOPF). The nodal price is calculated as ``pi`` in ``pic``. + + References + ---------- + 1. R. D. Zimmerman, C. E. Murillo-Sanchez, and R. J. Thomas, “MATPOWER: Steady-State Operations, Planning, and + Analysis Tools for Power Systems Research and Education,” IEEE Trans. Power Syst., vol. 26, no. 1, pp. 12–19, + Feb. 2011 """ def __init__(self, system, config): From 47ec81275410a6bfd33b64da4ceb8fa2ee33b206 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 7 Nov 2024 22:55:38 -0500 Subject: [PATCH 154/181] Minor fix --- ams/benchmarks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ams/benchmarks.py b/ams/benchmarks.py index d6f215cc..e898b8ff 100644 --- a/ams/benchmarks.py +++ b/ams/benchmarks.py @@ -143,7 +143,7 @@ def test_time(case, routine='DCOPF', ignore_dpp=True): # NOTE: the line flow limits are relaxed for the large cases # otherwise the DCOPF will fail in pandapower and MATPOWER if sp.Bus.n > 4000: - sp.Line.alter(src='rate_a', idx=sp.Line.idx.v, value=999) + sp.Line.alter(src='rate_a', idx=sp.Line.idx.v, value=99999) rtn = sp.routines[routine] From 81035b5e58ff07b6a7cf4bbbe719671de1379fd5 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 8 Nov 2024 00:29:48 -0500 Subject: [PATCH 155/181] Fix matpower conversion when all are linear cost model --- ams/io/matpower.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/ams/io/matpower.py b/ams/io/matpower.py index 710fd39e..82f2f784 100644 --- a/ams/io/matpower.py +++ b/ams/io/matpower.py @@ -205,11 +205,7 @@ def mpc2system(mpc: dict, system) -> bool: if 'gencost' in mpc: gcost_idx = 0 gen_idx = np.arange(mpc['gen'].shape[0]) + 1 - mpc_cost = np.zeros((mpc['gen'].shape[0], 7)) - if mpc['gencost'].shape[1] < 7: - mpc_cost[:, :mpc['gencost'].shape[1]] = mpc['gencost'] - else: - mpc_cost = mpc['gencost'] + mpc_cost = mpc['gencost'] for data, gen in zip(mpc_cost, gen_idx): # NOTE: only type 2 costs are supported for now # type startup shutdown n c2 c1 c0 @@ -220,9 +216,16 @@ def mpc2system(mpc: dict, system) -> bool: gctype = int(data[0]) startup = data[1] shutdown = data[2] - c2 = data[4] * base_mva ** 2 - c1 = data[5] * base_mva - c0 = data[6] + if data[3] == 3: + c2 = data[4] * base_mva ** 2 + c1 = data[5] * base_mva + c0 = data[6] + elif data[3] == 2: + c2 = 0 + c1 = data[4] * base_mva + c0 = data[5] + else: + raise ValueError('Unrecognized gencost model, please use eighter quadratic or linear cost model') system.add('GCost', gen=int(gen), u=1, type=gctype, idx=gcost_idx, From 5942f198fd641c189ce88cf40675d48fb9f95646 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 8 Nov 2024 00:30:48 -0500 Subject: [PATCH 156/181] Using set instead of alter in module benchmarks --- ams/benchmarks.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ams/benchmarks.py b/ams/benchmarks.py index e898b8ff..55307b53 100644 --- a/ams/benchmarks.py +++ b/ams/benchmarks.py @@ -236,7 +236,7 @@ def run_dcopf_with_load_factors(sp, solver, method=None, load_factors=None, igno pq_idx = sp.PQ.idx.v pd0 = sp.PQ.get(src='p0', attr='v', idx=pq_idx).copy() for lf_k in load_factors: - sp.PQ.alter(src='p0', idx=pq_idx, value=lf_k * pd0) + sp.PQ.set(src='p0', attr='v', idx=pq_idx, value=lf_k * pd0) sp.DCOPF.update(params=['pd']) if method: sp.DCOPF.run(solver=solver, reoptimize=True, Method=method, ignore_dpp=ignore_dpp) @@ -302,15 +302,15 @@ def test_mtime(case, load_factors, ignore_dpp=True): # Run solvers with load factors s_ams_grb, obj_grb = run_dcopf_with_load_factors( sp, 'GUROBI', method=3, load_factors=load_factors, ignore_dpp=ignore_dpp) - sp.PQ.alter(src='p0', idx=pq_idx, value=pd0) # Reset the load in AMS + sp.PQ.set(src='p0', attr='v', idx=pq_idx, value=pd0) # Reset the load in AMS s_ams_mosek, obj_mosek = run_dcopf_with_load_factors( sp, 'MOSEK', load_factors=load_factors, ignore_dpp=ignore_dpp) - sp.PQ.alter(src='p0', idx=pq_idx, value=pd0) + sp.PQ.set(src='p0', attr='v', idx=pq_idx, value=pd0) s_ams_piqp, obj_piqp = run_dcopf_with_load_factors( sp, 'PIQP', load_factors=load_factors, ignore_dpp=ignore_dpp) - sp.PQ.alter(src='p0', idx=pq_idx, value=pd0) + sp.PQ.set(src='p0', attr='v', idx=pq_idx, value=pd0) if PANDAPOWER_AVAILABLE: # --- PANDAPOWER --- From 3a47b1ddd075958aff1d9c9101296ac16e761fb2 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 8 Nov 2024 16:29:13 -0500 Subject: [PATCH 157/181] Typo --- ams/io/matpower.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/ams/io/matpower.py b/ams/io/matpower.py index 82f2f784..5e461e84 100644 --- a/ams/io/matpower.py +++ b/ams/io/matpower.py @@ -1,6 +1,5 @@ """ MATPOWER parser. -This module is revised from the existing module ``andes.io.matpower``. """ import logging import numpy as np @@ -23,7 +22,7 @@ def testlines(infile): def read(system, file): """ - Read a MATPOWER data file into mpc, and build andes device elements. + Read a MATPOWER data file into mpc, and build AMS device elements. """ mpc = m2mpc(file) @@ -34,16 +33,14 @@ def mpc2system(mpc: dict, system) -> bool: """ Load an mpc dict into an empty AMS system. - This function is revised from ``andes.io.matpower.mpc2system``. - - Compared to the original one, this function includes the generator cost data. + Revised from ``andes.io.matpower.mpc2system``. Note that `mbase` in mpc is converted to `Sn`, but it is not actually used in MATPOWER nor AMS. Parameters ---------- - system : andes.system.System + system : ams.system.System Empty system to load the data into. mpc : dict mpc struct names : numpy arrays From 8eb9857d42fd90fe89ec51e64c14a7ef88b81b5f Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 8 Nov 2024 16:30:21 -0500 Subject: [PATCH 158/181] Add test for matpower io when there are 6 rather than 7 columns in the gencost --- tests/test_io.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 tests/test_io.py diff --git a/tests/test_io.py b/tests/test_io.py new file mode 100644 index 00000000..6383e903 --- /dev/null +++ b/tests/test_io.py @@ -0,0 +1,32 @@ +""" +Test file input/output. +""" + +import unittest +import numpy as np + +import ams + + +class TestMATPOWER(unittest.TestCase): + + def setUp(self): + self.mpc5 = ams.io.matpower.m2mpc(ams.get_case('matpower/case5.m')) + self.mpc14 = ams.io.matpower.m2mpc(ams.get_case('matpower/case14.m')) + + def test_m2mpc(self): + self.assertTupleEqual(self.mpc5['gencost'].shape, (5, 6)) + self.assertTupleEqual(self.mpc14['gencost'].shape, (5, 7)) + + def test_mpc2system(self): + system5 = ams.system.System() + ams.io.matpower.mpc2system(self.mpc5, system5) + # In case5.m, the gencost has type 2 cost model, with 2 parameters. + np.testing.assert_array_equal(system5.GCost.c2.v, + np.zeros(system5.StaticGen.n)) + + system14 = ams.system.System() + ams.io.matpower.mpc2system(self.mpc14, system14) + # In case14.m, the gencost has type 2 cost model, with 3 parameters. + np.testing.assert_array_less(np.zeros(system14.StaticGen.n), + system14.GCost.c2.v,) From 113beed00da76117b6329e3b730dfa6a8aabd057 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 8 Nov 2024 16:31:14 -0500 Subject: [PATCH 159/181] Add ANDES in the get_tool_versions --- ams/benchmarks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ams/benchmarks.py b/ams/benchmarks.py index 55307b53..e27906c0 100644 --- a/ams/benchmarks.py +++ b/ams/benchmarks.py @@ -45,7 +45,8 @@ def get_tool_versions(tools=None): A dictionary containing the tool names and their versions. """ if tools is None: - tools = ['ltbams', 'cvxpy', 'pandapower', + tools = ['ltbams', 'andes', + 'cvxpy', 'pandapower', 'PYPOWER', 'gurobipy', 'mosek', 'ecos', 'scs', 'piqp', 'numba'] From 0d5facf82d2d2142b4a778b2aa4adf74241938ca Mon Sep 17 00:00:00 2001 From: jinningwang Date: Fri, 8 Nov 2024 16:31:53 -0500 Subject: [PATCH 160/181] Remove ecos and scs from get_tool_versions --- ams/benchmarks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ams/benchmarks.py b/ams/benchmarks.py index e27906c0..bdcd07c2 100644 --- a/ams/benchmarks.py +++ b/ams/benchmarks.py @@ -48,7 +48,7 @@ def get_tool_versions(tools=None): tools = ['ltbams', 'andes', 'cvxpy', 'pandapower', 'PYPOWER', 'gurobipy', 'mosek', - 'ecos', 'scs', 'piqp', 'numba'] + 'piqp', 'numba'] # Get current time and Python version last_run_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") From 7df6468924897ff48533d71583ddb8fbbdb5c785 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 9 Nov 2024 21:53:20 -0500 Subject: [PATCH 161/181] [WIP] Refactor benchmarks, refactor non-repeative part --- ams/benchmarks.py | 157 +++++++++++++++++++++++++++------------------- 1 file changed, 93 insertions(+), 64 deletions(-) diff --git a/ams/benchmarks.py b/ams/benchmarks.py index bdcd07c2..2a7bdcba 100644 --- a/ams/benchmarks.py +++ b/ams/benchmarks.py @@ -22,7 +22,7 @@ logger = logging.getLogger(__name__) -_failed_time = '-1 seconds' +_failed_time = -1 _failed_obj = -1 cols_time = ['ams_mats', 'ams_parse', 'ams_eval', 'ams_final', @@ -45,10 +45,9 @@ def get_tool_versions(tools=None): A dictionary containing the tool names and their versions. """ if tools is None: - tools = ['ltbams', 'andes', - 'cvxpy', 'pandapower', - 'PYPOWER', 'gurobipy', 'mosek', - 'piqp', 'numba'] + tools = ['ltbams', 'andes', 'cvxpy', + 'gurobipy', 'mosek', 'piqp', + 'pandapower', 'numba'] # Get current time and Python version last_run_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") @@ -83,7 +82,7 @@ def get_tool_versions(tools=None): return tool_versions -def run_routine(system, routine='DCOPF', ignore_dpp=True, solver='GUROBI', method=None): +def time_routine_solve(system, routine='DCOPF', **kwargs): """ Run the specified routine with the given solver and method. @@ -93,26 +92,28 @@ def run_routine(system, routine='DCOPF', ignore_dpp=True, solver='GUROBI', metho The system object containing the routine. routine : str, optional The name of the routine to run. Defaults to 'DCOPF'. + + Other Parameters + ---------------- + solver : str, optional + The solver to use. ignore_dpp : bool, optional Whether to ignore DPP. Defaults to True. - solver : str, optional - The solver to use. Defaults to 'GUROBI'. method : function, optional A custom solve method to use. Defaults to None. Returns ------- tuple - A tuple containing the elapsed time and the objective value. + A tuple containing the elapsed time (s) and the objective value ($). """ rtn = system.routines[routine] + solver = kwargs.get('solver', None) try: - t_start, _ = elapsed() - if method: - rtn.run(solver=solver, reoptimize=True, Method=method, ignore_dpp=ignore_dpp) - else: - rtn.run(solver=solver, ignore_dpp=ignore_dpp) - _, elapsed_time = elapsed(t_start) + t, _ = elapsed() + rtn.run(**kwargs) + _, s0 = elapsed(t) + elapsed_time = float(s0.split(' ')[0]) obj_value = rtn.obj.v except Exception as e: logger.error(f"Error running routine {routine} with solver {solver}: {e}") @@ -121,52 +122,44 @@ def run_routine(system, routine='DCOPF', ignore_dpp=True, solver='GUROBI', metho return elapsed_time, obj_value -def test_time(case, routine='DCOPF', ignore_dpp=True): +def pre_solve(system, routine): """ - Test the execution time of the specified routine on the given case. + Time the routine preparation process. Parameters ---------- - case : str - The path to the case file. - routine : str, optional - The name of the routine to test. Defaults to 'DCOPF'. - ignore_dpp : bool, optional - Whether to ignore DPP. Defaults to True. + system : ams.System + The system object containing the routine. + routine : str + The name of the routine to prepare Returns ------- - tuple - A tuple containing the list of times and the list of objective values. + dict + A dictionary containing the preparation times in seconds for each step: + 'mats', 'parse', 'evaluate', 'finalize', 'postinit'. """ - sp = ams.load(case, setup=True, default_config=True, no_output=True) - - # NOTE: the line flow limits are relaxed for the large cases - # otherwise the DCOPF will fail in pandapower and MATPOWER - if sp.Bus.n > 4000: - sp.Line.alter(src='rate_a', idx=sp.Line.idx.v, value=99999) - - rtn = sp.routines[routine] + rtn = system.routines[routine] # Initialize AMS # --- matrices build --- t_mats, _ = elapsed() - sp.mats.build() + system.mats.build(force=True) _, s_mats = elapsed(t_mats) # --- code generation --- t_parse, _ = elapsed() - rtn.om.parse() + rtn.om.parse(force=True) _, s_parse = elapsed(t_parse) # --- code evaluation --- t_evaluate, _ = elapsed() - rtn.om.evaluate() + rtn.om.evaluate(force=True) _, s_evaluate = elapsed(t_evaluate) # --- problem finalization --- t_finalize, _ = elapsed() - rtn.om.finalize() + rtn.om.finalize(force=True) _, s_finalize = elapsed(t_finalize) # --- rest init process --- @@ -174,38 +167,74 @@ def test_time(case, routine='DCOPF', ignore_dpp=True): rtn.init() _, s_postinit = elapsed(t_postinit) - # --- run solvers --- - s_ams_grb, obj_grb = run_routine(sp, routine, ignore_dpp, 'GUROBI') - s_ams_mosek, obj_mosek = run_routine(sp, routine, ignore_dpp, 'MOSEK') - s_ams_piqp, obj_piqp = run_routine(sp, routine, ignore_dpp, 'PIQP') + pre_time = dict(mats=float(s_mats.split(' ')[0]), + parse=float(s_parse.split(' ')[0]), + evaluate=float(s_evaluate.split(' ')[0]), + finalize=float(s_finalize.split(' ')[0]), + postinit=float(s_postinit.split(' ')[0])) + return pre_time - if PANDAPOWER_AVAILABLE: - # Convert to PYPOWER format - ppc = ams.io.pypower.system2ppc(sp) - freq = sp.config.freq - ppn = pdp.converter.from_ppc(ppc, f_hz=freq) - del sp +def time_pdp_dcopf(system): + """ + Test the execution time of DCOPF using pandapower. - try: - t_pdp, _ = elapsed() - pdp.rundcopp(ppn) - _, s_pdp = elapsed(t_pdp) - obj_pdp = ppn.res_cost - except Exception as e: - logger.error(f"Error running pandapower: {e}") - s_pdp = _failed_time - obj_pdp = _failed_obj - else: - s_pdp = _failed_time - obj_pdp = _failed_obj + Parameters + ---------- + system : ams.System + The system object containing the routine. - time = [s_mats, s_parse, s_evaluate, s_finalize, s_postinit, - s_ams_grb, s_ams_mosek, s_ams_piqp, s_pdp] - time = [float(t.split(' ')[0]) for t in time] - obj = [obj_grb, obj_mosek, obj_piqp, obj_pdp] + Returns + ------- + tuple + A tuple containing the elapsed time (s) and the objective value ($). + """ + ppc = ams.io.pypower.system2ppc(system) + ppn = pdp.converter.from_ppc(ppc, f_hz=system.config.freq) + try: + t_pdp, _ = elapsed() + pdp.rundcopp(ppn) + _, s_pdp = elapsed(t_pdp) + elapsed_time = float(s_pdp.split(' ')[0]) + obj_value = ppn.res_cost + except Exception as e: + logger.error(f"Error running pandapower: {e}") + elapsed_time = _failed_time + obj_value = _failed_obj + return elapsed_time, obj_value - return time, obj + +def time_routine(system, routine='DCOPF', solvers=['CLARABEL']): + """ + Time the specified routine with the given solvers. + + Parameters + ---------- + system : ams.System + The system object containing the routine. + routine : str, optional + The name of the routine to run. Defaults to 'DCOPF'. + solvers : list of str, optional + List of solvers to use. Defaults to ['CLARABEL']. + """ + pre_time = pre_solve(system, routine) + sol_time = {f'{solver}': {'time': 0, 'obj': 0} for solver in solvers} + + for solver in solvers: + if solver != 'pandapower': + kwargs = {'solver': solver} + s, obj = time_routine_solve(system, routine, **kwargs) + sol_time[solver]['time'] = s + sol_time[solver]['obj'] = obj + elif solver == 'pandapower' and PANDAPOWER_AVAILABLE and routine == 'DCOPF': + s, obj = time_pdp_dcopf(system) + sol_time[solver]['time'] = s + sol_time[solver]['obj'] = obj + else: + sol_time[solver]['time'] = _failed_time + sol_time[solver]['obj'] = _failed_obj + + return pre_time, sol_time def run_dcopf_with_load_factors(sp, solver, method=None, load_factors=None, ignore_dpp=False): From 64708fd975ba863190ade07ddc1ca591f6c76fdd Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 9 Nov 2024 21:56:43 -0500 Subject: [PATCH 162/181] [WIP] Refactor benchmarks, minor fix non-repeative part --- ams/benchmarks.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/ams/benchmarks.py b/ams/benchmarks.py index 2a7bdcba..6e9c7391 100644 --- a/ams/benchmarks.py +++ b/ams/benchmarks.py @@ -99,8 +99,6 @@ def time_routine_solve(system, routine='DCOPF', **kwargs): The solver to use. ignore_dpp : bool, optional Whether to ignore DPP. Defaults to True. - method : function, optional - A custom solve method to use. Defaults to None. Returns ------- @@ -204,7 +202,8 @@ def time_pdp_dcopf(system): return elapsed_time, obj_value -def time_routine(system, routine='DCOPF', solvers=['CLARABEL']): +def time_routine(system, routine='DCOPF', solvers=['CLARABEL'], + **kwargs): """ Time the specified routine with the given solvers. @@ -216,14 +215,24 @@ def time_routine(system, routine='DCOPF', solvers=['CLARABEL']): The name of the routine to run. Defaults to 'DCOPF'. solvers : list of str, optional List of solvers to use. Defaults to ['CLARABEL']. + + Other Parameters + ---------------- + ignore_dpp : bool, optional + Whether to ignore DPP. Defaults to True. + + Returns + ------- + tuple + A tuple containing the preparation times and the solution times in + seconds for each solver. """ pre_time = pre_solve(system, routine) sol_time = {f'{solver}': {'time': 0, 'obj': 0} for solver in solvers} for solver in solvers: if solver != 'pandapower': - kwargs = {'solver': solver} - s, obj = time_routine_solve(system, routine, **kwargs) + s, obj = time_routine_solve(system, routine, solver=solver, **kwargs) sol_time[solver]['time'] = s sol_time[solver]['obj'] = obj elif solver == 'pandapower' and PANDAPOWER_AVAILABLE and routine == 'DCOPF': From 1a5848d193ddf7232ae5a096d4e4006a2dfc60f5 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sat, 9 Nov 2024 22:38:00 -0500 Subject: [PATCH 163/181] [WIP] Refactor benchmarks, minor change non-repeative part --- ams/benchmarks.py | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/ams/benchmarks.py b/ams/benchmarks.py index 6e9c7391..06bf4b41 100644 --- a/ams/benchmarks.py +++ b/ams/benchmarks.py @@ -25,9 +25,7 @@ _failed_time = -1 _failed_obj = -1 -cols_time = ['ams_mats', 'ams_parse', 'ams_eval', 'ams_final', - 'ams_postinit', 'ams_grb', 'ams_mosek', 'ams_piqp', 'pdp'] -cols_obj = ['grb', 'mosek', 'piqp', 'pdp'] +cols_pre = ['ams_mats', 'ams_parse', 'ams_eval', 'ams_final', 'ams_postinit'] def get_tool_versions(tools=None): @@ -165,11 +163,9 @@ def pre_solve(system, routine): rtn.init() _, s_postinit = elapsed(t_postinit) - pre_time = dict(mats=float(s_mats.split(' ')[0]), - parse=float(s_parse.split(' ')[0]), - evaluate=float(s_evaluate.split(' ')[0]), - finalize=float(s_finalize.split(' ')[0]), - postinit=float(s_postinit.split(' ')[0])) + s_float = [float(s.split(' ')[0]) for s in [s_mats, s_parse, s_evaluate, s_finalize, s_postinit]] + + pre_time = dict(zip(cols_pre, s_float)) return pre_time @@ -228,22 +224,22 @@ def time_routine(system, routine='DCOPF', solvers=['CLARABEL'], seconds for each solver. """ pre_time = pre_solve(system, routine) - sol_time = {f'{solver}': {'time': 0, 'obj': 0} for solver in solvers} + sol = {f'{solver}': {'time': 0, 'obj': 0} for solver in solvers} for solver in solvers: if solver != 'pandapower': s, obj = time_routine_solve(system, routine, solver=solver, **kwargs) - sol_time[solver]['time'] = s - sol_time[solver]['obj'] = obj + sol[solver]['time'] = s + sol[solver]['obj'] = obj elif solver == 'pandapower' and PANDAPOWER_AVAILABLE and routine == 'DCOPF': s, obj = time_pdp_dcopf(system) - sol_time[solver]['time'] = s - sol_time[solver]['obj'] = obj + sol[solver]['time'] = s + sol[solver]['obj'] = obj else: - sol_time[solver]['time'] = _failed_time - sol_time[solver]['obj'] = _failed_obj + sol[solver]['time'] = _failed_time + sol[solver]['obj'] = _failed_obj - return pre_time, sol_time + return pre_time, sol def run_dcopf_with_load_factors(sp, solver, method=None, load_factors=None, ignore_dpp=False): From 8b86bc02802e42c9864e5749c9038e3f0cc2ca51 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Sun, 10 Nov 2024 12:44:45 -0500 Subject: [PATCH 164/181] Refactor module benchmarks --- ams/benchmarks.py | 182 +++++++++++---------------------------- tests/test_benchmarks.py | 124 +++++++++++++++++++------- 2 files changed, 144 insertions(+), 162 deletions(-) diff --git a/ams/benchmarks.py b/ams/benchmarks.py index 06bf4b41..ded0e7a9 100644 --- a/ams/benchmarks.py +++ b/ams/benchmarks.py @@ -7,8 +7,6 @@ import importlib_metadata import logging -import numpy as np - try: import pandapower as pdp PANDAPOWER_AVAILABLE = True @@ -169,22 +167,20 @@ def pre_solve(system, routine): return pre_time -def time_pdp_dcopf(system): +def time_pdp_dcopf(ppn): """ Test the execution time of DCOPF using pandapower. Parameters ---------- - system : ams.System - The system object containing the routine. + ppn : pandapowerNet + The pandapower network object. Returns ------- tuple A tuple containing the elapsed time (s) and the objective value ($). """ - ppc = ams.io.pypower.system2ppc(system) - ppn = pdp.converter.from_ppc(ppc, f_hz=system.config.freq) try: t_pdp, _ = elapsed() pdp.rundcopp(ppn) @@ -232,7 +228,9 @@ def time_routine(system, routine='DCOPF', solvers=['CLARABEL'], sol[solver]['time'] = s sol[solver]['obj'] = obj elif solver == 'pandapower' and PANDAPOWER_AVAILABLE and routine == 'DCOPF': - s, obj = time_pdp_dcopf(system) + ppc = ams.io.pypower.system2ppc(system) + ppn = pdp.converter.from_ppc(ppc, f_hz=system.config.freq) + s, obj = time_pdp_dcopf(ppn) sol[solver]['time'] = s sol[solver]['obj'] = obj else: @@ -242,143 +240,63 @@ def time_routine(system, routine='DCOPF', solvers=['CLARABEL'], return pre_time, sol -def run_dcopf_with_load_factors(sp, solver, method=None, load_factors=None, ignore_dpp=False): +def time_dcopf_with_lf(system, solvers=['CLARABEL'], load_factors=[1], ignore_dpp=False): """ - Run the specified solver with varying load factors. + Time the execution of DCOPF with varying load factors. Parameters ---------- - sp : ams.System + system : ams.System The system object containing the routine. - solver : str - The name of the solver to use. - method : function, optional - A custom solve method to use. Defaults to None. + solvers : list of str, optional + List of solvers to use. Defaults to ['CLARABEL']. load_factors : list of float, optional List of load factors to apply. Defaults to None. - - Returns - ------- - tuple - A tuple containing the elapsed time and the cumulative objective value. - """ - if load_factors is None: - load_factors = [] - - obj_value = 0 - try: - t_start, _ = elapsed() - pq_idx = sp.PQ.idx.v - pd0 = sp.PQ.get(src='p0', attr='v', idx=pq_idx).copy() - for lf_k in load_factors: - sp.PQ.set(src='p0', attr='v', idx=pq_idx, value=lf_k * pd0) - sp.DCOPF.update(params=['pd']) - if method: - sp.DCOPF.run(solver=solver, reoptimize=True, Method=method, ignore_dpp=ignore_dpp) - else: - sp.DCOPF.run(solver=solver, ignore_dpp=ignore_dpp) - obj_value += sp.DCOPF.obj.v - _, elapsed_time = elapsed(t_start) - except Exception as e: - logger.error(f"Error running solver {solver} with load factors: {e}") - elapsed_time = _failed_time - obj_value = _failed_obj - return elapsed_time, obj_value - - -def test_mtime(case, load_factors, ignore_dpp=True): - """ - Test the execution time of the specified routine on the given case with varying load factors. - - Parameters - ---------- - case : str - The path to the case file. - load_factors : list of float - List of load factors to apply. + ignore_dpp : bool, optional + Whether to ignore DPP. Returns ------- tuple A tuple containing the list of times and the list of objective values. """ - sp = ams.load(case, setup=True, default_config=True, no_output=True) - - # Record original load - pq_idx = sp.PQ.idx.v - pd0 = sp.PQ.get(src='p0', attr='v', idx=pq_idx).copy() - - # Initialize AMS - # --- matrices build --- - t_mats, _ = elapsed() - sp.mats.build() - _, s_mats = elapsed(t_mats) - - # --- code generation --- - t_parse, _ = elapsed() - sp.DCOPF.om.parse() - _, s_parse = elapsed(t_parse) - - # --- code evaluation --- - t_evaluate, _ = elapsed() - sp.DCOPF.om.evaluate() - _, s_evaluate = elapsed(t_evaluate) - - # --- problem finalization --- - t_finalize, _ = elapsed() - sp.DCOPF.om.finalize() - _, s_finalize = elapsed(t_finalize) - - # --- rest init process --- - t_postinit, _ = elapsed() - sp.DCOPF.init() - _, s_postinit = elapsed(t_postinit) - - # Run solvers with load factors - s_ams_grb, obj_grb = run_dcopf_with_load_factors( - sp, 'GUROBI', method=3, load_factors=load_factors, ignore_dpp=ignore_dpp) - sp.PQ.set(src='p0', attr='v', idx=pq_idx, value=pd0) # Reset the load in AMS - - s_ams_mosek, obj_mosek = run_dcopf_with_load_factors( - sp, 'MOSEK', load_factors=load_factors, ignore_dpp=ignore_dpp) - sp.PQ.set(src='p0', attr='v', idx=pq_idx, value=pd0) - - s_ams_piqp, obj_piqp = run_dcopf_with_load_factors( - sp, 'PIQP', load_factors=load_factors, ignore_dpp=ignore_dpp) - sp.PQ.set(src='p0', attr='v', idx=pq_idx, value=pd0) - - if PANDAPOWER_AVAILABLE: - # --- PANDAPOWER --- - ppc = ams.io.pypower.system2ppc(sp) - freq = sp.config.freq + pre_time = pre_solve(system, 'DCOPF') + sol = {f'{solver}': {'time': 0, 'obj': 0} for solver in solvers} - del sp + pd0 = system.PQ.p0.v.copy() + pq_idx = system.PQ.idx.v - ppc_pd0 = ppc['bus'][:, 2].copy() + for solver in solvers: + if solver != 'pandapower': + obj_all = 0 + t_all, _ = elapsed() + for lf_k in load_factors: + system.PQ.set(src='p0', attr='v', idx=pq_idx, value=lf_k * pd0) + system.DCOPF.update(params=['pd']) + _, obj = time_routine_solve(system, 'DCOPF', + solver=solver, ignore_dpp=ignore_dpp) + obj_all += obj + _, s_all = elapsed(t_all) + system.PQ.set(src='p0', attr='v', idx=pq_idx, value=pd0) + s = float(s_all.split(' ')[0]) + sol[solver]['time'] = s + sol[solver]['obj'] = obj_all + elif solver == 'pandapower' and PANDAPOWER_AVAILABLE: + ppc = ams.io.pypower.system2ppc(system) + ppn = pdp.converter.from_ppc(ppc, f_hz=system.config.freq) + p_mw0 = ppn.load['p_mw'].copy() + t_all, _ = elapsed() + obj_all = 0 + for lf_k in load_factors: + ppn.load['p_mw'] = lf_k * p_mw0 + _, obj = time_pdp_dcopf(ppn) + obj_all += obj + _, s_all = elapsed(t_all) + s = float(s_all.split(' ')[0]) + sol[solver]['time'] = s + sol[solver]['obj'] = obj_all + else: + sol[solver]['time'] = _failed_time + sol[solver]['obj'] = _failed_obj - ppn = pdp.converter.from_ppc(ppc, f_hz=freq) - obj_pdp = 0 - t_pdp_series = ['0 seconds'] * len(load_factors) - try: - for i, lf_k in enumerate(load_factors): - ppc['bus'][:, 2] = lf_k * ppc_pd0 - ppn = pdp.converter.from_ppc(ppc, f_hz=freq) - t0_pdp, _ = elapsed() - pdp.rundcopp(ppn) - obj_pdp += ppn.res_cost - _, t_pdp_series[i] = elapsed(t0_pdp) - t_pdp_series = [float(t.split(' ')[0]) for t in t_pdp_series] - s_pdp = f'{np.sum(t_pdp_series):.4f} seconds' - except Exception: - s_pdp = _failed_time - obj_pdp = _failed_obj - else: - s_pdp = _failed_time - obj_pdp = _failed_obj - - time = [s_mats, s_parse, s_evaluate, s_finalize, s_postinit, - s_ams_grb, s_ams_mosek, s_ams_piqp, s_pdp] - time = [float(t.split(' ')[0]) for t in time] - obj = [obj_grb, obj_mosek, obj_piqp, obj_pdp] - - return time, obj + return pre_time, sol diff --git a/tests/test_benchmarks.py b/tests/test_benchmarks.py index 1cf7def3..66c57af8 100644 --- a/tests/test_benchmarks.py +++ b/tests/test_benchmarks.py @@ -1,5 +1,4 @@ import unittest -import numpy as np import ams import ams.benchmarks as bp @@ -50,36 +49,101 @@ def decorator(test_func): class TestBenchmarks(unittest.TestCase): - - def setUp(self): - self.case = ams.get_case('matpower/case5.m') + """ + Test module for benchmarks.py. + """ def test_get_tool_versions(self): self.assertIsInstance(bp.get_tool_versions(), dict) - @require_packages('mosek', 'gurobipy', 'pandapower') - def test_run_routine(self): - ss = ams.load(self.case, setup=True, default_config=True, no_output=True) - _, _obj = bp.run_routine(ss, routine='DCOPF', solver='CLARABEL', ignore_dpp=False) - - np.testing.assert_array_less(np.zeros_like(_obj), _obj) - - @require_packages('mosek', 'gurobipy', 'pandapower') - def test_run_dcopf_with_load_factors(self): - ss = ams.load(self.case, setup=True, default_config=True, no_output=True) - _, _obj = bp.run_dcopf_with_load_factors(ss, solver='CLARABEL', - load_factors=np.array([1.1, 1.2]), ignore_dpp=True) - - np.testing.assert_array_less(np.zeros_like(_obj), _obj) - - @require_packages('mosek', 'gurobipy', 'pandapower') - def test_test_time(self): - _, _obj = bp.test_time(self.case, routine='DCOPF', ignore_dpp=False) - - np.testing.assert_array_less(np.zeros_like(_obj), _obj) - - @require_packages('mosek', 'gurobipy', 'pandapower') - def test_test_mtime(self): - _, _obj = bp.test_mtime(self.case, load_factors=np.array([1.1, 1.2]), ignore_dpp=False) - - np.testing.assert_array_less(np.zeros_like(_obj), _obj) + def test_pre_solve_rted(self): + ss = ams.load(ams.get_case('5bus/pjm5bus_demo.xlsx'), + setup=True, default_config=True, no_output=True) + pre_time = bp.pre_solve(ss, 'RTED') + self.assertIsInstance(pre_time, dict) + self.assertEqual(len(pre_time), len(bp.cols_pre)) + for v in pre_time.values(): + self.assertIsInstance(v, float) + + def test_dcopf_solve(self): + ss = ams.load(ams.get_case('matpower/case5.m'), + setup=True, default_config=True, no_output=True) + s, obj = bp.time_routine_solve(ss, routine='DCOPF', solver='CLARABEL', ignore_dpp=False) + self.assertGreater(s, 0) + self.assertGreater(obj, 0) + + def test_pre_solve_ed(self): + ss = ams.load(ams.get_case('5bus/pjm5bus_demo.xlsx'), + setup=True, default_config=True, no_output=True) + pre_time = bp.pre_solve(ss, 'ED') + self.assertIsInstance(pre_time, dict) + self.assertEqual(len(pre_time), len(bp.cols_pre)) + for v in pre_time.values(): + self.assertIsInstance(v, float) + + def test_ed_solve(self): + ss = ams.load(ams.get_case('5bus/pjm5bus_demo.xlsx'), + setup=True, default_config=True, no_output=True) + s, obj = bp.time_routine_solve(ss, 'ED', solver='CLARABEL', ignore_dpp=True) + self.assertGreater(s, 0) + self.assertGreater(obj, 0) + + @require_packages('pandapower') + def time_pdp_dcopf(self): + ss = ams.load(ams.get_case('matpower/case5.m'), + setup=True, default_config=True, no_output=True) + ppc = ams.io.pypower.system2ppc(ss) + ppn = ams.io.pandapower.converter.from_ppc(ppc, f_hz=ss.config.freq) + s, obj = bp.time_pdp_dcopf(ppn) + self.assertGreater(s, 0) + self.assertGreater(obj, 0) + + def test_time_dcopf(self): + ss = ams.load(ams.get_case('matpower/case5.m'), + setup=True, default_config=True, no_output=True) + pre_time, sol = bp.time_routine(ss, + routine='DCOPF', + solvers=['CLARABEL', 'SCIP', 'pandapower'], ignore_dpp=False) + for v in pre_time.values(): + self.assertGreaterEqual(v, 0) + + self.assertGreater(sol['CLARABEL']['time'], 0) + self.assertGreater(sol['SCIP']['time'], 0) + self.assertAlmostEqual(sol['CLARABEL']['obj'], + sol['SCIP']['obj'], + places=2) + if not are_packages_available('pandapower'): + self.assertEqual(sol['pandapower']['time'], -1) + self.assertEqual(sol['pandapower']['obj'], -1) + else: + self.assertGreater(sol['pandapower']['obj'], 0) + self.assertAlmostEqual(sol['CLARABEL']['obj'], + sol['pandapower']['obj'], + places=2) + + def test_time_rted(self): + ss = ams.load(ams.get_case('5bus/pjm5bus_demo.xlsx'), + setup=True, default_config=True, no_output=True) + pre_time, sol = bp.time_routine(ss, + routine='RTED', + solvers=['CLARABEL', 'SCIP', 'pandapower'], ignore_dpp=False) + for v in pre_time.values(): + self.assertGreaterEqual(v, 0) + + self.assertGreater(sol['CLARABEL']['time'], 0) + self.assertGreater(sol['SCIP']['time'], 0) + self.assertAlmostEqual(sol['CLARABEL']['obj'], + sol['SCIP']['obj'], + places=2) + self.assertEqual(sol['pandapower']['time'], -1) + self.assertEqual(sol['pandapower']['obj'], -1) + + def test_time_dcopf_with_lf(self): + ss = ams.load(ams.get_case('matpower/case5.m'), + setup=True, default_config=True, no_output=True) + pre_time, sol = bp.time_dcopf_with_lf(ss, solvers=['CLARABEL', 'SCIP'], load_factors=[ + 1, 0.5, 0.25], ignore_dpp=False) + self.assertEqual(len(pre_time), len(bp.cols_pre)) + self.assertAlmostEqual(sol['CLARABEL']['obj'], + sol['SCIP']['obj'], + places=2) From 7f73d2bd66c2e316bfd15940819684e0899b42c3 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 11 Nov 2024 11:37:47 -0500 Subject: [PATCH 165/181] Add pandoc to doc dependencies --- ams/__init__.py | 12 +----------- pyproject.toml | 1 + requirements-extra.txt | 3 ++- requirements.txt | 2 +- setup.cfg | 2 +- 5 files changed, 6 insertions(+), 14 deletions(-) diff --git a/ams/__init__.py b/ams/__init__.py index 1b86b2ba..0918653c 100644 --- a/ams/__init__.py +++ b/ams/__init__.py @@ -1,20 +1,10 @@ from . import _version __version__ = _version.get_versions()['version'] -from ams import io # NOQA -from ams import utils # NOQA -from ams import models # NOQA -from ams import system # NOQA -from ams import routines # NOQA -from ams import opt # NOQA -from ams import pypower # NOQA -from ams import report # NOQA -from ams import extension # NOQA - from ams.main import config_logger, load, run # NOQA from ams.utils.paths import get_case # NOQA from ams.shared import ppc2df # NOQA __author__ = 'Jining Wang' -__all__ = ['io', 'utils', 'models', 'system', 'extension'] +__all__ = ['system'] diff --git a/pyproject.toml b/pyproject.toml index 47a81659..dd12bf9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,7 @@ dev = [ "toml" ] doc = [ + "pandoc", "ipython", "sphinx", "pydata-sphinx-theme", diff --git a/requirements-extra.txt b/requirements-extra.txt index 8839e7dc..2b4798bd 100644 --- a/requirements-extra.txt +++ b/requirements-extra.txt @@ -1,4 +1,4 @@ -# Generated on 2024-11-06. +# Generated on 2024-11-11. pytest # dev pytest-cov # dev coverage # dev @@ -7,6 +7,7 @@ numpydoc # dev toml # dev pyscipopt # dev toml # dev +pandoc # doc ipython # doc sphinx # doc pydata-sphinx-theme # doc diff --git a/requirements.txt b/requirements.txt index b4053049..5ceafe9f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -# Generated on 2024-11-06. +# Generated on 2024-11-11. kvxopt>=1.3.2.1 numpy scipy diff --git a/setup.cfg b/setup.cfg index 9a8df768..be71e22f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,4 +1,4 @@ -# Generated on 2024-11-06. +# Generated on 2024-11-11. [versioneer] VCS = git style = pep440-post From aa78b737f7875d8e202b8b925c6dc67a9ac0fe45 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Mon, 11 Nov 2024 14:46:35 -0500 Subject: [PATCH 166/181] Try to fix RTD --- .readthedocs.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index c732d0ac..2fcec7aa 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -18,6 +18,4 @@ python: - method: pip path: . extra_requirements: - - doc - - method: setuptools - path: . \ No newline at end of file + - doc \ No newline at end of file From 1a082a53f274b78adc0756127197cf8484ec2ff6 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 14 Nov 2024 08:49:43 -0500 Subject: [PATCH 167/181] Add included packages in setup.py --- docs/source/conf.py | 2 +- setup.py | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index e99a8545..b332c5c9 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -164,7 +164,7 @@ # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'ams', 'AMS Manual', - author, 'ams', 'Python Software for Scheduling Modeling and Co-Simulation with Dynanic', + author, 'ams', 'Python Software for Scheduling Modeling and Co-Simulation with Dynanics', 'Miscellaneous'), ] diff --git a/setup.py b/setup.py index 08f0e6aa..60ac0388 100644 --- a/setup.py +++ b/setup.py @@ -4,5 +4,12 @@ setup( version=versioneer.get_version(), - cmdclass=versioneer.get_cmdclass() + cmdclass=versioneer.get_cmdclass(), + package_data={ + 'ams': [ + # When adding files here, remember to update MANIFEST.in as well, + # or else they will not be included in the distribution on PyPI! + # 'path/to/data_file', + ] + }, ) From 4cc2045933e3e8a13bfb09cf96563237c71f0d19 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 14 Nov 2024 09:00:18 -0500 Subject: [PATCH 168/181] Add import routines in package init --- ams/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ams/__init__.py b/ams/__init__.py index 0918653c..f985e63f 100644 --- a/ams/__init__.py +++ b/ams/__init__.py @@ -1,10 +1,13 @@ from . import _version __version__ = _version.get_versions()['version'] +from ams import routines # NOQA +from ams import benchmarks # NOQA + from ams.main import config_logger, load, run # NOQA from ams.utils.paths import get_case # NOQA from ams.shared import ppc2df # NOQA __author__ = 'Jining Wang' -__all__ = ['system'] +__all__ = ['main', 'system', 'cli'] From 01686c73a01014090bd43ea8f221e44feafab1bd Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 14 Nov 2024 11:06:21 -0500 Subject: [PATCH 169/181] Add an empty ANDES system and list ad_models into module shared --- ams/io/json.py | 11 ++--------- ams/io/xlsx.py | 13 +++---------- ams/shared.py | 9 ++++++++- 3 files changed, 13 insertions(+), 20 deletions(-) diff --git a/ams/io/json.py b/ams/io/json.py index 9ea4d9ac..7a8ab107 100644 --- a/ams/io/json.py +++ b/ams/io/json.py @@ -11,7 +11,7 @@ from andes.io.json import (testlines, read) # NOQA from andes.utils.paths import confirm_overwrite -from andes.system import System as andes_system +from ams.shared import empty_adsys, ad_models logger = logging.getLogger(__name__) @@ -62,13 +62,6 @@ def _dump_system(system, skip_empty, orient='records', to_andes=False): Dump parameters of each model into a json string and return them all in an OrderedDict. """ - ad_models = [] - if to_andes: - # Instantiate an ANDES system - sa = andes_system(setup=False, default_config=True, - codegen=False, autogen_stale=False, - no_undill=True,) - ad_models = list(sa.models.keys()) out = OrderedDict() for name, instance in system.models.items(): if skip_empty and instance.n == 0: @@ -79,7 +72,7 @@ def _dump_system(system, skip_empty, orient='records', to_andes=False): # NOTE: ommit parameters that are not in ANDES skip_params = [] ams_params = list(instance.params.keys()) - andes_params = list(sa.models[name].params.keys()) + andes_params = list(empty_adsys.models[name].params.keys()) skip_params = list(set(ams_params) - set(andes_params)) df = instance.cache.df_in.drop(skip_params, axis=1, errors='ignore') out[name] = df.to_dict(orient=orient) diff --git a/ams/io/xlsx.py b/ams/io/xlsx.py index bc7826c4..12d40863 100644 --- a/ams/io/xlsx.py +++ b/ams/io/xlsx.py @@ -6,8 +6,8 @@ import logging from andes.io.xlsx import (read, testlines, confirm_overwrite, _add_book) # NOQA -from andes.shared import pd -from andes.system import System as andes_system + +from ams.shared import pd, empty_adsys, ad_models logger = logging.getLogger(__name__) @@ -61,13 +61,6 @@ def _write_system(system, writer, skip_empty, to_andes=False): Rewrite function ``andes.io.xlsx._write_system`` to skip non-andes sheets. """ - ad_models = [] - if to_andes: - # Instantiate an ANDES system - sa = andes_system(setup=False, default_config=True, - codegen=False, autogen_stale=False, - no_undill=True,) - ad_models = list(sa.models.keys()) for name, instance in system.models.items(): if skip_empty and instance.n == 0: continue @@ -78,7 +71,7 @@ def _write_system(system, writer, skip_empty, to_andes=False): # NOTE: ommit parameters that are not in ANDES skip_params = [] ams_params = list(instance.params.keys()) - andes_params = list(sa.models[name].params.keys()) + andes_params = list(empty_adsys.models[name].params.keys()) skip_params = list(set(ams_params) - set(andes_params)) df = instance.cache.df_in.drop(skip_params, axis=1, errors='ignore') else: diff --git a/ams/shared.py b/ams/shared.py index 9ab0fa9a..afe3c92e 100644 --- a/ams/shared.py +++ b/ams/shared.py @@ -11,13 +11,20 @@ import cvxpy as cp -from andes.shared import pd from andes.utils.lazyimport import LazyImport +from andes.system import System as adSystem + + logger = logging.getLogger(__name__) sps = LazyImport('import scipy.sparse as sps') np = LazyImport('import numpy as np') +pd = LazyImport('import pandas as pd') + +# --- an empty ANDES system --- +empty_adsys = adSystem() +ad_models = list(empty_adsys.models.keys()) # --- NumPy constants --- # NOTE: In NumPy 2.0, np.Inf and np.NaN are deprecated. From 4da152411bc4c63671a2a75baf9ea947fefefaf6 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 14 Nov 2024 11:09:25 -0500 Subject: [PATCH 170/181] Switching andes.shared.pd to ams.shared.pd --- ams/core/matprocessor.py | 4 ++-- ams/interop/andes.py | 3 +-- ams/routines/routine.py | 2 +- tests/test_routine.py | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/ams/core/matprocessor.py b/ams/core/matprocessor.py index 57df7f1b..151dac3d 100644 --- a/ams/core/matprocessor.py +++ b/ams/core/matprocessor.py @@ -10,11 +10,11 @@ import numpy as np from andes.thirdparty.npfunc import safe_div -from andes.shared import pd, tqdm, tqdm_nb +from andes.shared import tqdm, tqdm_nb from andes.utils.misc import elapsed, is_notebook from ams.opt.omodel import Param -from ams.shared import sps +from ams.shared import pd, sps logger = logging.getLogger(__name__) diff --git a/ams/interop/andes.py b/ams/interop/andes.py index b75cfdd2..8409eb73 100644 --- a/ams/interop/andes.py +++ b/ams/interop/andes.py @@ -6,13 +6,12 @@ import logging from collections import OrderedDict, Counter -from andes.shared import pd, np from andes.utils.misc import elapsed from andes.system import System as andes_System from ams.utils import create_entry from ams.io import input_formats -from ams.shared import nan +from ams.shared import nan, pd, np logger = logging.getLogger(__name__) diff --git a/ams/routines/routine.py b/ams/routines/routine.py index cbd602ce..60b04785 100644 --- a/ams/routines/routine.py +++ b/ams/routines/routine.py @@ -10,7 +10,6 @@ import numpy as np from andes.core import Config -from andes.shared import pd from andes.utils.misc import elapsed from ams.core.param import RParam @@ -19,6 +18,7 @@ from ams.core.service import RBaseService, ValueService from ams.opt.omodel import OModel, Param, Var, Constraint, Objective, ExpressionCalc +from ams.shared import pd logger = logging.getLogger(__name__) diff --git a/tests/test_routine.py b/tests/test_routine.py index 86ce37be..64b8cd70 100644 --- a/tests/test_routine.py +++ b/tests/test_routine.py @@ -1,9 +1,9 @@ import unittest import numpy as np -from andes.shared import pd import ams +from ams.shared import pd class TestRoutineMethods(unittest.TestCase): From fab93f5b1bf67e8d58fd3d50c5c3c6e1fd42622d Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 14 Nov 2024 11:12:50 -0500 Subject: [PATCH 171/181] Typo --- ams/io/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ams/io/__init__.py b/ams/io/__init__.py index c24061cb..997de88a 100644 --- a/ams/io/__init__.py +++ b/ams/io/__init__.py @@ -19,7 +19,6 @@ # The first file will be parsed by read() function and the addfile will be parsed by read_add() # Typically, column based formats, such as IEEE CDF and PSS/E RAW, are faster to parse -# TODO: add support for json I/O input_formats = { 'xlsx': ('xlsx',), 'json': ('json',), From b295868fb0feb5583d3c4ee1f71d525f408ddeff Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 14 Nov 2024 11:22:52 -0500 Subject: [PATCH 172/181] Refactor module interop as a folder into a file interface --- ams/{interop/andes.py => interface.py} | 2 +- ams/interop/__init__.py | 18 ------------------ ams/system.py | 2 +- tests/test_interop.py | 4 ++-- 4 files changed, 4 insertions(+), 22 deletions(-) rename ams/{interop/andes.py => interface.py} (99%) delete mode 100644 ams/interop/__init__.py diff --git a/ams/interop/andes.py b/ams/interface.py similarity index 99% rename from ams/interop/andes.py rename to ams/interface.py index 8409eb73..fe93dd32 100644 --- a/ams/interop/andes.py +++ b/ams/interface.py @@ -1,5 +1,5 @@ """ -Interface with ANDES +Module for interfacing ANDES """ import os diff --git a/ams/interop/__init__.py b/ams/interop/__init__.py deleted file mode 100644 index 041597ad..00000000 --- a/ams/interop/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -""" -Interopability package between AMS and other software. - -To install dependencies, do: - -.. code:: bash - - pip install ams[interop] - -To install dependencies for *development*, in the AMS source code folder, do: - -.. code:: bash - - pip install -e .[interop] - -""" - -from ams.interop import andes # NOQA diff --git a/ams/system.py b/ams/system.py index f0601d46..d8c6b07b 100644 --- a/ams/system.py +++ b/ams/system.py @@ -24,7 +24,7 @@ from ams.routines import all_routines from ams.utils.paths import get_config_path from ams.core.matprocessor import MatProcessor -from ams.interop.andes import to_andes +from ams.interface import to_andes from ams.report import Report logger = logging.getLogger(__name__) diff --git a/tests/test_interop.py b/tests/test_interop.py index 5961c8dd..11070f21 100644 --- a/tests/test_interop.py +++ b/tests/test_interop.py @@ -8,8 +8,8 @@ import andes import ams -from ams.interop.andes import (build_group_table, make_link_table, to_andes, - parse_addfile, verify_pf) +from ams.interface import (build_group_table, make_link_table, to_andes, + parse_addfile, verify_pf) class TestAndesConversion(unittest.TestCase): From 7b1489c3d9df4fea65520ae199b357a9c0738df9 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 14 Nov 2024 11:45:56 -0500 Subject: [PATCH 173/181] Fix project __init__ --- ams/__init__.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/ams/__init__.py b/ams/__init__.py index f985e63f..b0941301 100644 --- a/ams/__init__.py +++ b/ams/__init__.py @@ -1,13 +1,19 @@ from . import _version __version__ = _version.get_versions()['version'] -from ams import routines # NOQA +from ams import io # NOQA +from ams import core # NOQA +from ams import models # NOQA +from ams import routines # NOQA +from ams import utils # NOQA +from ams import interface # NOQA from ams import benchmarks # NOQA from ams.main import config_logger, load, run # NOQA -from ams.utils.paths import get_case # NOQA +from ams.system import System # NOQA +from ams.utils.paths import get_case, list_cases # NOQA from ams.shared import ppc2df # NOQA __author__ = 'Jining Wang' -__all__ = ['main', 'system', 'cli'] +__all__ = ['system', 'ppc2df', 'System'] From 0b6d9ba9181a688d0daa018e3729ed26981399db Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 14 Nov 2024 12:34:42 -0500 Subject: [PATCH 174/181] Update docstring with notes if copied from ANDES --- ams/cli.py | 6 ++++ ams/main.py | 81 +++++++++++++++++++++++++++++++++++++++++++++++++-- ams/report.py | 6 ++++ 3 files changed, 91 insertions(+), 2 deletions(-) diff --git a/ams/cli.py b/ams/cli.py index 6b383f16..80721420 100644 --- a/ams/cli.py +++ b/ams/cli.py @@ -29,6 +29,12 @@ def create_parser(): ------- argparse.ArgumentParser Parser with all AMS options + + Notes + ----- + Revised from the ANDES project (https://github.com/CURENT/andes). + Original author: Hantao Cui + License: GPL3 """ parser = argparse.ArgumentParser() diff --git a/ams/main.py b/ams/main.py index ad4e600f..58fc1f9c 100644 --- a/ams/main.py +++ b/ams/main.py @@ -60,9 +60,16 @@ def config_logger(stream_level=logging.INFO, *, `StreamHandler` verbosity level. file_level : {10, 20, 30, 40, 50}, optional `FileHandler` verbosity level. + Returns ------- None + + Notes + ----- + Copied from the ANDES project (https://github.com/CURENT/andes). + Original author: Hantao Cui + License: GPL3 """ lg = logging.getLogger('ams') lg.setLevel(logging.DEBUG) @@ -105,7 +112,6 @@ def config_logger(stream_level=logging.INFO, *, coloredlogs.install(logger=lg, level=stream_level, fmt=sh_formatter_str) -# TODO: check ``load`` later on to see if some of them can be removed def load(case, setup=True, use_input_path=True, **kwargs): @@ -131,6 +137,12 @@ def load(case, setup=True, If one need to add devices in addition to these from the case file, do ``setup=False`` and call ``System.add()`` to add devices. When done, manually invoke ``setup()`` to set up the system. + + Notes + ----- + Revised from the ANDES project (https://github.com/CURENT/andes). + Original author: Hantao Cui + License: GPL3 """ if use_input_path: input_path = kwargs.get('input_path', '') @@ -180,6 +192,12 @@ def run_case(case, *, routine='pflow', profile=False, add_book : str, optional Name of the device to be added to an excel case as a new sheet. + + Notes + ----- + Revised from the ANDES project (https://github.com/CURENT/andes). + Original author: Hantao Cui + License: GPL3 """ pr = cProfile.Profile() @@ -249,6 +267,12 @@ def _run_mp_proc(cases, ncpu=NCPUS_PHYSICAL, **kwargs): Run multiprocessing with `Process`. Return values from `run_case` are not preserved. Always return `True` when done. + + Notes + ----- + Copied from the ANDES project (https://github.com/CURENT/andes). + Original author: Hantao Cui + License: GPL3 """ # start processes @@ -283,6 +307,12 @@ def _run_mp_pool(cases, ncpu=NCPUS_PHYSICAL, verbose=logging.INFO, **kwargs): Verbosity level during multiprocessing verbose : 10, 20, 30, 40, 50 Verbosity level outside multiprocessing + + Notes + ----- + Copied from the ANDES project (https://github.com/CURENT/andes). + Original author: Hantao Cui + License: GPL3 """ pool = Pool(ncpu) @@ -337,6 +367,11 @@ def run(filename, input_path='', verbose=20, mp_verbose=30, System or exit_code An instance of system (if `cli == False`) or an exit code otherwise.. + Notes + ----- + Copied from the ANDES project (https://github.com/CURENT/andes). + Original author: Hantao Cui + License: GPL3 """ if is_interactive() and len(logger.handlers) == 0: @@ -459,6 +494,12 @@ def misc(edit_config='', save_config='', show_license=False, clean=True, recursi overwrite=None, version=False, **kwargs): """ Miscellaneous commands. + + Notes + ----- + Copied from the ANDES project (https://github.com/CURENT/andes). + Original author: Hantao Cui + License: GPL3 """ if edit_conf(edit_config): @@ -487,6 +528,12 @@ def misc(edit_config='', save_config='', show_license=False, clean=True, recursi def doc(attribute=None, list_supported=False, config=False, **kwargs): """ Quick documentation from command-line. + + Notes + ----- + Revised from the ANDES project (https://github.com/CURENT/andes). + Original author: Hantao Cui + License: GPL3 """ system = System() if attribute is not None: @@ -512,6 +559,12 @@ def demo(**kwargs): def versioninfo(): """ Print version info for AMS and dependencies. + + Notes + ----- + Revised from the ANDES project (https://github.com/CURENT/andes). + Original author: Hantao Cui + License: GPL3 """ import numpy as np @@ -574,6 +627,12 @@ def edit_conf(edit_config: Optional[Union[str, bool]] = ''): ------- bool ``True`` is a config file is found and an editor is opened. ``False`` if ``edit_config`` is False. + + Notes + ----- + Copied from the ANDES project (https://github.com/CURENT/andes). + Original author: Hantao Cui + License: GPL3 """ ret = False @@ -627,6 +686,12 @@ def save_conf(config_path=None, overwrite=None, **kwargs): ------- bool ``True`` is the save action is run. ``False`` otherwise. + + Notes + ----- + Copied from the ANDES project (https://github.com/CURENT/andes). + Original author: Hantao Cui + License: GPL3 """ ret = False @@ -644,7 +709,7 @@ def save_conf(config_path=None, overwrite=None, **kwargs): return ret -# TODO: list AMS output files here +# TODO: change to AMS output types def remove_output(recursive=False): """ Remove the outputs generated by AMS, including power flow reports @@ -661,6 +726,12 @@ def remove_output(recursive=False): bool ``True`` is the function body executes with success. ``False`` otherwise. + + Notes + ----- + Copied from the ANDES project (https://github.com/CURENT/andes). + Original author: Hantao Cui + License: GPL3 """ found = False cwd = os.getcwd() @@ -690,6 +761,12 @@ def remove_output(recursive=False): def selftest(quick=False, extra=False, **kwargs): """ Run unit tests. + + Notes + ----- + Copied from the ANDES project (https://github.com/CURENT/andes). + Original author: Hantao Cui + License: GPL3 """ # map verbosity level from logging to unittest diff --git a/ams/report.py b/ams/report.py index 0442cd6e..ea3e62f6 100644 --- a/ams/report.py +++ b/ams/report.py @@ -28,6 +28,12 @@ def report_info(system) -> list: class Report: """ Report class to store routine analysis reports. + + Notes + ----- + Revised from the ANDES project (https://github.com/CURENT/andes). + Original author: Hantao Cui + License: GPL3 """ def __init__(self, system): From ef1a3f454ba9f1c10aac52e4463bf81bc2c2cb5b Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 14 Nov 2024 12:44:30 -0500 Subject: [PATCH 175/181] Minor fix --- ams/system.py | 2 +- tests/test_addressing.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ams/system.py b/ams/system.py index d8c6b07b..f14603b9 100644 --- a/ams/system.py +++ b/ams/system.py @@ -17,7 +17,7 @@ from andes.utils.misc import elapsed from andes.utils.tab import Tab -import ams.io +import ams from ams.models.group import GroupBase from ams.routines.type import TypeBase from ams.models import file_classes diff --git a/tests/test_addressing.py b/tests/test_addressing.py index 1c7d3f1d..674f8458 100644 --- a/tests/test_addressing.py +++ b/tests/test_addressing.py @@ -1,7 +1,7 @@ import unittest +import numpy as np import ams -import numpy as np ams.config_logger(stream_level=40) From 21065d841f94d26c130fafd8357636b06b016686 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 14 Nov 2024 12:45:32 -0500 Subject: [PATCH 176/181] Minor fix --- ams/__init__.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/ams/__init__.py b/ams/__init__.py index b0941301..ae078bea 100644 --- a/ams/__init__.py +++ b/ams/__init__.py @@ -1,12 +1,6 @@ from . import _version __version__ = _version.get_versions()['version'] -from ams import io # NOQA -from ams import core # NOQA -from ams import models # NOQA -from ams import routines # NOQA -from ams import utils # NOQA -from ams import interface # NOQA from ams import benchmarks # NOQA from ams.main import config_logger, load, run # NOQA @@ -16,4 +10,4 @@ __author__ = 'Jining Wang' -__all__ = ['system', 'ppc2df', 'System'] +__all__ = ['System', 'get_case', 'System'] From 78f598a2ff7e13ff7f19a39a85e1dfa285384374 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 14 Nov 2024 12:51:00 -0500 Subject: [PATCH 177/181] Switch to importlib from importlib_metadata --- ams/benchmarks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ams/benchmarks.py b/ams/benchmarks.py index ded0e7a9..b80bf6cd 100644 --- a/ams/benchmarks.py +++ b/ams/benchmarks.py @@ -4,7 +4,7 @@ import datetime import sys -import importlib_metadata +import importlib.metadata as importlib_metadata import logging try: From a640d5a40e63ca3760a88d563fcabbcccd7cef90 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 14 Nov 2024 14:19:24 -0500 Subject: [PATCH 178/181] Remove unused import --- ams/routines/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ams/routines/__init__.py b/ams/routines/__init__.py index c3dd4b06..15f15565 100644 --- a/ams/routines/__init__.py +++ b/ams/routines/__init__.py @@ -4,7 +4,6 @@ from collections import OrderedDict from andes.utils.func import list_flatten -from ams.routines.routine import RoutineBase # NOQA all_routines = OrderedDict([ ('dcpf', ['DCPF']), From 4b8fde17b899f21a9ce572a902b63b55997d9238 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 14 Nov 2024 15:15:07 -0500 Subject: [PATCH 179/181] Fix doc build --- ams/routines/dcopf.py | 2 +- docs/source/api.rst | 6 ++++-- docs/source/getting_started/install.rst | 4 ++-- docs/source/modeling/routine.rst | 15 ++++----------- 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/ams/routines/dcopf.py b/ams/routines/dcopf.py index 01d340ac..f2220cdf 100644 --- a/ams/routines/dcopf.py +++ b/ams/routines/dcopf.py @@ -234,7 +234,7 @@ def __init__(self, system, config): def solve(self, **kwargs): """ Solve the routine optimization model. - *args and **kwargs go to `self.om.prob.solve()` (`cvxpy.Problem.solve()`). + args and kwargs go to `self.om.prob.solve()` (`cvxpy.Problem.solve()`). """ return self.om.prob.solve(**kwargs) diff --git a/docs/source/api.rst b/docs/source/api.rst index 9fabf3cd..ba11937c 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -44,6 +44,8 @@ Routines :template: autosummary/module_toctree.rst ams.routines.routine + ams.routines.dcopf + ams.routines.pflow Optimization @@ -56,7 +58,7 @@ Optimization :caption: Optimization :template: autosummary/module_toctree.rst - ams.opt.omodel + ams.opt I/O @@ -82,7 +84,7 @@ Interoperability :caption: Interoperability :template: autosummary/module_toctree.rst - ams.interop + ams.interface Others diff --git a/docs/source/getting_started/install.rst b/docs/source/getting_started/install.rst index 7c02f449..21f6d138 100644 --- a/docs/source/getting_started/install.rst +++ b/docs/source/getting_started/install.rst @@ -172,8 +172,8 @@ Note the dot at the end. Pip will take care of the rest. .. note:: To install extra support packages, one can append ``[NAME_OF_EXTRA]`` to - ``pip install -e .``. For example, ``pip install -e .[interop]`` will - install packages to support interoperability when installing AMS in the + ``pip install -e .``. For example, ``pip install -e .[doc]`` will + install packages to support documentation when installing AMS in the development, editable mode. Updating AMS diff --git a/docs/source/modeling/routine.rst b/docs/source/modeling/routine.rst index 6cf8dbd4..a257bebe 100644 --- a/docs/source/modeling/routine.rst +++ b/docs/source/modeling/routine.rst @@ -58,13 +58,6 @@ Further, to facilitate the routine definition, AMS developed a class .. autoclass:: ams.core.param.RParam :noindex: -.. currentmodule:: ams.routines -.. autosummary:: - :recursive: - :toctree: _generated - - RoutineBase - Numerical Optimization ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -87,7 +80,7 @@ Interoperation with ANDES ----------------------------------- The interoperation with dynamic simulator invovles both file conversion and data exchange. -In AMS, the built-in interface with ANDES is implemented in :py:mod:`ams.interop.andes`. +In AMS, the built-in interface with ANDES is implemented in :py:mod:`ams.interface`. File Format Converter @@ -98,7 +91,7 @@ where it defines grid topology and power flow. An AMS case can be converted to an ANDES case, with the option to supply additional dynamic data. -.. autofunction:: ams.interop.andes.to_andes +.. autofunction:: ams.interface.to_andes :noindex: @@ -112,10 +105,10 @@ The maping relationship for a specific routine is defined in the routine class a ``map2``. Additionally, a link table for the ANDES case is used for the controller connections. -Module :py:mod:`ams.interop.andes.Dynamic`, contains the necessary functions and classes for +Module :py:mod:`ams.interface.Dynamic`, contains the necessary functions and classes for file conversion and data exchange. -.. autoclass:: ams.interop.andes.Dynamic +.. autoclass:: ams.interface.Dynamic :noindex: :members: send, receive From b61b075e9b46dc56a9b22f403ea781f6e25e6e54 Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 14 Nov 2024 15:52:06 -0500 Subject: [PATCH 180/181] Fix pyproject setups --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index dd12bf9f..9758d91f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,5 +69,5 @@ versionfile_source = "ams/_version.py" versionfile_build = "ams/_version.py" tag_prefix = "v" -[tool.setuptools] -packages = ["ams"] \ No newline at end of file +[tool.setuptools.packages.find] +where = ["."] \ No newline at end of file From 68c930e6260b5b92b51b0957e61b48a44296d60c Mon Sep 17 00:00:00 2001 From: jinningwang Date: Thu, 14 Nov 2024 15:59:30 -0500 Subject: [PATCH 181/181] Update release notes --- docs/source/release-notes.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 5070e83c..8bcdb1fa 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -9,7 +9,7 @@ The APIs before v3.0.0 are in beta and may change without prior notice. Pre-v1.0.0 ========== -v0.9.11 (2024-xx-xx) +v0.9.11 (2024-11-14) -------------------- - Add pyproject.toml for PEP 517 and PEP 518 compliance @@ -28,6 +28,7 @@ v0.9.11 (2024-xx-xx) - Rename methods `v2` as `e` for classes `Constraint` and `Objective` - Add benchmark functions - Improve using of `eval()` in module `omodel` +- Refactor module `interop.andes` as module `interface` for simplicity v0.9.10 (2024-09-03) --------------------