From 060c42085e04a35225696e6c20985d6dc3985a44 Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Wed, 11 Jun 2025 17:34:30 -0700 Subject: [PATCH 01/36] Replace Inf with inf and Nan with nan --- pypower/ipopt_options.py | 4 ++-- pypower/mosek_options.py | 2 +- pypower/opf_model.py | 4 ++-- pypower/pips.py | 8 ++++---- pypower/qps_cplex.py | 4 ++-- pypower/qps_gurobi.py | 6 +++--- pypower/qps_ipopt.py | 6 +++--- pypower/qps_mosek.py | 4 ++-- pypower/qps_pips.py | 12 ++++++------ pypower/qps_pypower.py | 8 ++++---- pypower/t/t_opf_ipopt.py | 4 ++-- pypower/toggle_reserves.py | 4 ++-- 12 files changed, 33 insertions(+), 33 deletions(-) diff --git a/pypower/ipopt_options.py b/pypower/ipopt_options.py index 7a5561b..979cec2 100644 --- a/pypower/ipopt_options.py +++ b/pypower/ipopt_options.py @@ -471,14 +471,14 @@ def ipopt_user_options_3(opt, ppopt): # - yes [Project final point back into original bounds] # # check_derivatives_for_naninf ("no") -# Indicates whether it is desired to check for Nan/Inf in derivative matrices +# Indicates whether it is desired to check for nan/inf in derivative matrices # Activating this option will cause an error if an invalid number is # detected in the constraint Jacobians or the Lagrangian Hessian. If this # is not activated, the test is skipped, and the algorithm might proceed # with invalid numbers and fail. # Possible values: # - no [Don't check (faster).] -# - yes [Check Jacobians and Hessian for Nan and Inf.] +# - yes [Check Jacobians and Hessian for nan and inf.] # # jac_c_constant ("no") # Indicates whether all equality constraints are linear diff --git a/pypower/mosek_options.py b/pypower/mosek_options.py index 749ac0a..9b31d25 100644 --- a/pypower/mosek_options.py +++ b/pypower/mosek_options.py @@ -133,7 +133,7 @@ def mosek_user_options_3(opt, ppopt): # opt['MSK_DPAR_INTPNT_TOL_REL_GAP'] = 1e-8 ## relative gap termination tol # opt['MSK_IPAR_INTPNT_MAX_ITERATIONS'] = 400 ## max iterations for int point # opt['MSK_IPAR_SIM_MAX_ITERATIONS'] = 10000000 ## max iterations for simplex - # opt['MSK_DPAR_OPTIMIZER_MAX_TIME'] = -1 ## max time allowed (< 0 --> Inf) + # opt['MSK_DPAR_OPTIMIZER_MAX_TIME'] = -1 ## max time allowed (< 0 --> inf) # opt['MSK_IPAR_INTPNT_NUM_THREADS'] = 1 ## number of threads # opt['MSK_IPAR_PRESOLVE_USE'] = sc['MSK_PRESOLVE_MODE_OFF'] diff --git a/pypower/opf_model.py b/pypower/opf_model.py index 0190c95..861c455 100644 --- a/pypower/opf_model.py +++ b/pypower/opf_model.py @@ -185,7 +185,7 @@ def add_constraints(self, name, AorN, l, u=None, varsets=None): create a lot of zero columns. If C{varsets} is empty, C{x} is taken to be the full vector of all optimization variables. If C{l} or C{u} are empty, they are assumed to be appropriately sized vectors - of C{-Inf} and C{Inf}, respectively. + of C{-inf} and C{inf}, respectively. For nonlinear constraints, the 3rd argument, C{N}, is the number of constraints in the set. Currently, this is used internally @@ -385,7 +385,7 @@ def add_vars(self, name, N, v0=None, vl=None, vu=None): and C{vl} and C{vu} are the lower and upper bounds on the variables. The defaults for the last three arguments, which are optional, are for all values to be initialized to zero (C{v0 = 0}) and unbounded - (C{VL = -Inf, VU = Inf}). + (C{VL = -inf, VU = inf}). """ ## prevent duplicate named var sets if name in self.var["idx"]["N"]: diff --git a/pypower/pips.py b/pypower/pips.py index 7153d30..a94c5c8 100644 --- a/pypower/pips.py +++ b/pypower/pips.py @@ -99,15 +99,15 @@ def pips(f_fcn, x0=None, A=None, l=None, u=None, xmin=None, xmax=None, @type x0: array @param A: Optional linear constraints. @type A: csr_matrix - @param l: Optional linear constraints. Default values are M{-Inf}. + @param l: Optional linear constraints. Default values are M{-inf}. @type l: array - @param u: Optional linear constraints. Default values are M{Inf}. + @param u: Optional linear constraints. Default values are M{inf}. @type u: array @param xmin: Optional lower bounds on the M{x} variables, defaults are - M{-Inf}. + M{-inf}. @type xmin: array @param xmax: Optional upper bounds on the M{x} variables, defaults are - M{Inf}. + M{inf}. @type xmax: array @param gh_fcn: Function that evaluates the optional nonlinear constraints and their gradients for a given value of M{x}. diff --git a/pypower/qps_cplex.py b/pypower/qps_cplex.py index bd7cbd9..222d6ee 100644 --- a/pypower/qps_cplex.py +++ b/pypower/qps_cplex.py @@ -45,9 +45,9 @@ def qps_cplex(H, c, A, l, u, xmin, xmax, x0, opt): - C{H} : matrix (possibly sparse) of quadratic cost coefficients - C{c} : vector of linear cost coefficients - C{A, l, u} : define the optional linear constraints. Default - values for the elements of L and U are -Inf and Inf, respectively. + values for the elements of L and U are -inf and inf, respectively. - C{xmin, xmax} : optional lower and upper bounds on the - C{x} variables, defaults are -Inf and Inf, respectively. + C{x} variables, defaults are -inf and inf, respectively. - C{x0} : optional starting value of optimization vector C{x} - C{opt} : optional options structure with the following fields, all of which are also optional (default values shown in parentheses) diff --git a/pypower/qps_gurobi.py b/pypower/qps_gurobi.py index 7f3d29f..902cade 100644 --- a/pypower/qps_gurobi.py +++ b/pypower/qps_gurobi.py @@ -37,10 +37,10 @@ def qps_gurobi(H, c, A, l, u, xmin, xmax, x0, opt): H : matrix (possibly sparse) of quadratic cost coefficients c : vector of linear cost coefficients A, l, u : define the optional linear constraints. Default - values for the elements of l and u are -Inf and Inf, + values for the elements of l and u are -inf and inf, respectively. xmin, xmax : optional lower and upper bounds on the - C{x} variables, defaults are -Inf and Inf, respectively. + C{x} variables, defaults are -inf and inf, respectively. x0 : optional starting value of optimization vector C{x} opt : optional options structure with the following fields, all of which are also optional (default values shown in @@ -97,7 +97,7 @@ def qps_gurobi(H, c, A, l, u, xmin, xmax, x0, opt): A = [ [1 1 1 1] [0.17 0.11 0.10 0.18] ] l = [1; 0.10] - u = [1; Inf] + u = [1; inf] xmin = zeros((4, 1)) x0 = [1; 0; 0; 1] opt = {'verbose': 2} diff --git a/pypower/qps_ipopt.py b/pypower/qps_ipopt.py index eb39ffb..59bdb8f 100644 --- a/pypower/qps_ipopt.py +++ b/pypower/qps_ipopt.py @@ -37,10 +37,10 @@ def qps_ipopt(H, c, A, l, u, xmin, xmax, x0, opt): - C{H} : matrix (possibly sparse) of quadratic cost coefficients - C{C} : vector of linear cost coefficients - C{A, l, u} : define the optional linear constraints. Default - values for the elements of C{l} and C{u} are -Inf and Inf, + values for the elements of C{l} and C{u} are -inf and inf, respectively. - C{xmin, xmax} : optional lower and upper bounds on the - C{x} variables, defaults are -Inf and Inf, respectively. + C{x} variables, defaults are -inf and inf, respectively. - C{x0} : optional starting value of optimization vector C{x} - C{opt} : optional options structure with the following fields, all of which are also optional (default values shown in parentheses) @@ -102,7 +102,7 @@ def qps_ipopt(H, c, A, l, u, xmin, xmax, x0, opt): A = [ 1 1 1 1 0.17 0.11 0.10 0.18 ] l = [1, 0.10] - u = [1, Inf] + u = [1, inf] xmin = zeros((4, 1)) x0 = [1, 0, 0, 1] opt = {'verbose': 2) diff --git a/pypower/qps_mosek.py b/pypower/qps_mosek.py index 3ab8e84..284e165 100644 --- a/pypower/qps_mosek.py +++ b/pypower/qps_mosek.py @@ -41,9 +41,9 @@ def qps_mosek(H, c=None, A=None, l=None, u=None, xmin=None, xmax=None, - C{H} : matrix (possibly sparse) of quadratic cost coefficients - C{C} : vector of linear cost coefficients - C{A, l, u} : define the optional linear constraints. Default - values for the elements of L and U are -Inf and Inf, respectively. + values for the elements of L and U are -inf and inf, respectively. - xmin, xmax : optional lower and upper bounds on the - C{x} variables, defaults are -Inf and Inf, respectively. + C{x} variables, defaults are -inf and inf, respectively. - C{x0} : optional starting value of optimization vector C{x} - C{opt} : optional options structure with the following fields, all of which are also optional (default values shown in parentheses) diff --git a/pypower/qps_pips.py b/pypower/qps_pips.py index 3de5337..4c90520 100644 --- a/pypower/qps_pips.py +++ b/pypower/qps_pips.py @@ -33,7 +33,7 @@ def qps_pips(H, c, A, l, u, xmin=None, xmax=None, x0=None, opt=None): Example from U{http://www.uc.edu/sashtml/iml/chap8/sect12.htm}: - >>> from numpy import array, zeros, Inf + >>> from numpy import array, zeros, inf >>> from scipy.sparse import csr_matrix >>> H = csr_matrix(array([[1003.1, 4.3, 6.3, 5.9], ... [4.3, 2.2, 2.1, 3.9], @@ -43,7 +43,7 @@ def qps_pips(H, c, A, l, u, xmin=None, xmax=None, x0=None, opt=None): >>> A = csr_matrix(array([[1, 1, 1, 1 ], ... [0.17, 0.11, 0.10, 0.18]])) >>> l = array([1, 0.10]) - >>> u = array([1, Inf]) + >>> u = array([1, inf]) >>> xmin = zeros(4) >>> xmax = None >>> x0 = array([1, 0, 0, 1]) @@ -62,15 +62,15 @@ def qps_pips(H, c, A, l, u, xmin=None, xmax=None, x0=None, opt=None): @type c: array @param A: Optional linear constraints. @type A: csr_matrix - @param l: Optional linear constraints. Default values are M{-Inf}. + @param l: Optional linear constraints. Default values are M{-inf}. @type l: array - @param u: Optional linear constraints. Default values are M{Inf}. + @param u: Optional linear constraints. Default values are M{inf}. @type u: array @param xmin: Optional lower bounds on the M{x} variables, defaults are - M{-Inf}. + M{-inf}. @type xmin: array @param xmax: Optional upper bounds on the M{x} variables, defaults are - M{Inf}. + M{inf}. @type xmax: array @param x0: Starting value of optimization vector M{x}. @type x0: array diff --git a/pypower/qps_pypower.py b/pypower/qps_pypower.py index b20327a..8ece5e7 100644 --- a/pypower/qps_pypower.py +++ b/pypower/qps_pypower.py @@ -35,10 +35,10 @@ def qps_pypower(H, c=None, A=None, l=None, u=None, xmin=None, xmax=None, - C{H} : matrix (possibly sparse) of quadratic cost coefficients - C{c} : vector of linear cost coefficients - C{A, l, u} : define the optional linear constraints. Default - values for the elements of C{l} and C{u} are -Inf and Inf, + values for the elements of C{l} and C{u} are -inf and inf, respectively. - C{xmin}, C{xmax} : optional lower and upper bounds on the - C{x} variables, defaults are -Inf and Inf, respectively. + C{x} variables, defaults are -inf and inf, respectively. - C{x0} : optional starting value of optimization vector C{x} - C{opt} : optional options structure with the following fields, all of which are also optional (default values shown in parentheses) @@ -91,7 +91,7 @@ def qps_pypower(H, c=None, A=None, l=None, u=None, xmin=None, xmax=None, Example from U{http://www.uc.edu/sashtml/iml/chap8/sect12.htm}: - >>> from numpy import array, zeros, Inf + >>> from numpy import array, zeros, inf >>> from scipy.sparse import csr_matrix >>> H = csr_matrix(array([[1003.1, 4.3, 6.3, 5.9], ... [4.3, 2.2, 2.1, 3.9], @@ -101,7 +101,7 @@ def qps_pypower(H, c=None, A=None, l=None, u=None, xmin=None, xmax=None, >>> A = csr_matrix(array([[1, 1, 1, 1 ], ... [0.17, 0.11, 0.10, 0.18]])) >>> l = array([1, 0.10]) - >>> u = array([1, Inf]) + >>> u = array([1, inf]) >>> xmin = zeros(4) >>> xmax = None >>> x0 = array([1, 0, 0, 1]) diff --git a/pypower/t/t_opf_ipopt.py b/pypower/t/t_opf_ipopt.py index 7117b61..6ab0879 100644 --- a/pypower/t/t_opf_ipopt.py +++ b/pypower/t/t_opf_ipopt.py @@ -7,7 +7,7 @@ from os.path import dirname, join -from numpy import array, ones, zeros, Inf, r_, ix_, argsort, arange +from numpy import array, ones, zeros, inf, r_, ix_, argsort, arange from scipy.io import loadmat from scipy.sparse import spdiags, csr_matrix as sparse @@ -204,7 +204,7 @@ def t_opf_ipopt(quiet=False): row = [0, 0, 1, 1] col = [9, 24, 9, 24] A = sparse(([-1, 1, 1, 1], (row, col)), (2, 25)) - u = array([Inf, Inf]) + u = array([inf, inf]) l = array([-1, 1]) N = sparse(([1], ([0], [24])), (1, 25)) ## new z variable only diff --git a/pypower/toggle_reserves.py b/pypower/toggle_reserves.py index 0c9f094..0269cab 100644 --- a/pypower/toggle_reserves.py +++ b/pypower/toggle_reserves.py @@ -12,7 +12,7 @@ from pprint import pprint -from numpy import zeros, ones, arange, Inf, any, flatnonzero as find +from numpy import zeros, ones, arange, inf, any, flatnonzero as find from scipy.sparse import eye as speye from scipy.sparse import csr_matrix as sparse @@ -173,7 +173,7 @@ def userfcn_reserves_formulation(om, *args): ## variable bounds Rmin = zeros(ngr) ## bound below by 0 - Rmax = Inf * ones(ngr) ## bound above by ... + Rmax = inf * ones(ngr) ## bound above by ... k = find(ppc['gen'][igr, RAMP_10]) Rmax[k] = ppc['gen'][igr[k], RAMP_10] ## ... ramp rate and ... if 'qty' in r: From c20cca933402b50feb7a3f14f2d95b6cfc77b559 Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Wed, 11 Jun 2025 17:43:31 -0700 Subject: [PATCH 02/36] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 42f30c4..dcf5a9c 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='PYPOWER', - version='5.1.18', + version='5.1.19a', author='Richard Lincoln', author_email='r.w.lincoln@gmail.com', description='Solves power flow and optimal power flow problems', From 84797ed8286f7da7886d30d8dcf792c4652afd64 Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Thu, 12 Jun 2025 09:16:06 -0700 Subject: [PATCH 03/36] Create tests folder and add test_cases.py to check all cases in pypower module --- .gitignore | 2 +- requirements.txt | 6 +++--- tests/test_cases.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 tests/test_cases.py diff --git a/.gitignore b/.gitignore index 900916c..3586b19 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,4 @@ dist/ PYPOWER.egg-info/ doc/api/ doc/.build/* -/venv/ +*venv/ diff --git a/requirements.txt b/requirements.txt index 37ba251..8ce107c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,14 +1,14 @@ appdirs==1.4.4 distlib==0.3.1 filelock==3.0.12 -numpy==1.22.0 +numpy==2.3.0 packaging==20.9 -pip==21.1 +pip==25.1.1 pluggy==0.13.1 py==1.10.0 pyparsing==2.4.7 pyrlu==0.2.1 -scipy==1.6.1 +scipy==1.15.3 setuptools==65.5.1 six==1.15.0 toml==0.10.2 diff --git a/tests/test_cases.py b/tests/test_cases.py new file mode 100644 index 0000000..b059d2e --- /dev/null +++ b/tests/test_cases.py @@ -0,0 +1,31 @@ +"""PyPOWER Module testing""" + +import os +import sys +import importlib +MODULEDIR = ".." +CASEDIR = "../pypower" +sys.path.extend([MODULEDIR,CASEDIR]) +from pypower.api import runpf, runopf, ppoption +tested = 0 +failed = 0 +print(f"Testing all cases in {CASEDIR}...") +for case in os.listdir("../pypower"): + if case.startswith("case") and case.endswith(".py"): + name = os.path.splitext(case)[0] + module = importlib.__import__(name) + try: + if hasattr(module,name): + tested += 1 + print(f"Running {case} pf and opf",end="...",flush=True,file=sys.stdout) + model = getattr(module,name) + casedata = model() + ppopt = ppoption(VERBOSE=0,OUT_ALL=0) + assert runpf(casedata,ppopt)[1] == 1, "runpf failed" + assert runopf(casedata,ppopt)["success"], "runopf failed" + print("ok",file=sys.stdout) + except Exception as err: + print(f"ERROR [{name}]: {err}",file=sys.stderr) + failed += 1 +print(f"Testing completed: {tested=}, {failed=}") +exit(1 if failed > 0 else 0) \ No newline at end of file From 56f968003e37391b08aca3faf82510f43e2bdc6e Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Thu, 12 Jun 2025 09:20:47 -0700 Subject: [PATCH 04/36] Create run-test-cases.yml --- .github/workflows/run-test-cases.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/run-test-cases.yml diff --git a/.github/workflows/run-test-cases.yml b/.github/workflows/run-test-cases.yml new file mode 100644 index 0000000..04ed1ce --- /dev/null +++ b/.github/workflows/run-test-cases.yml @@ -0,0 +1,25 @@ +# This workflow run all the cases found in the pypower folder + +name: Run test cases + +on: + pull: + - "*" + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: '3.x' + - name: Install dependencies + run: python3 -m pip install --upgrade pip -r requirements.txt + - name: Run test cases + run: | + cd tests + python3 test_cases.py From 57e64789ec617772baab65f7d0a08872366267d5 Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Thu, 12 Jun 2025 09:22:15 -0700 Subject: [PATCH 05/36] Update run-test-cases.yml --- .github/workflows/run-test-cases.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/run-test-cases.yml b/.github/workflows/run-test-cases.yml index 04ed1ce..53e2174 100644 --- a/.github/workflows/run-test-cases.yml +++ b/.github/workflows/run-test-cases.yml @@ -3,22 +3,27 @@ name: Run test cases on: - pull: - - "*" + push: + branches: [ main ] + pull_request: + branches: [ main ] jobs: - deploy: + test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - name: Set up Python uses: actions/setup-python@v3 with: python-version: '3.x' + - name: Install dependencies run: python3 -m pip install --upgrade pip -r requirements.txt + - name: Run test cases run: | cd tests From f2ea035aecf1bbd8446cf0d7e0d9bede12b5f86a Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Thu, 12 Jun 2025 09:33:57 -0700 Subject: [PATCH 06/36] Add output of tests to json --- tests/.gitignore | 1 + tests/test_cases.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 tests/.gitignore diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..94a2dd1 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +*.json \ No newline at end of file diff --git a/tests/test_cases.py b/tests/test_cases.py index b059d2e..597a332 100644 --- a/tests/test_cases.py +++ b/tests/test_cases.py @@ -3,12 +3,24 @@ import os import sys import importlib +import json +import numpy as np + MODULEDIR = ".." CASEDIR = "../pypower" + sys.path.extend([MODULEDIR,CASEDIR]) from pypower.api import runpf, runopf, ppoption + tested = 0 failed = 0 + +class NumpyEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, np.ndarray): + return obj.tolist() + return super().default(obj) + print(f"Testing all cases in {CASEDIR}...") for case in os.listdir("../pypower"): if case.startswith("case") and case.endswith(".py"): @@ -20,6 +32,9 @@ print(f"Running {case} pf and opf",end="...",flush=True,file=sys.stdout) model = getattr(module,name) casedata = model() + json.dump(casedata,open(f"{name}.json","w"), + cls=NumpyEncoder, + indent=4) ppopt = ppoption(VERBOSE=0,OUT_ALL=0) assert runpf(casedata,ppopt)[1] == 1, "runpf failed" assert runopf(casedata,ppopt)["success"], "runopf failed" @@ -28,4 +43,5 @@ print(f"ERROR [{name}]: {err}",file=sys.stderr) failed += 1 print(f"Testing completed: {tested=}, {failed=}") + exit(1 if failed > 0 else 0) \ No newline at end of file From 506a3f47ab581b3967265679de740a3599fdc0fa Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Thu, 12 Jun 2025 10:20:57 -0700 Subject: [PATCH 07/36] Added output of results for diagnostic purposes --- tests/.gitignore | 3 ++- tests/test_cases.py | 18 +++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/tests/.gitignore b/tests/.gitignore index 94a2dd1..035424a 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -1 +1,2 @@ -*.json \ No newline at end of file +*.json +*.out \ No newline at end of file diff --git a/tests/test_cases.py b/tests/test_cases.py index 597a332..2b2d588 100644 --- a/tests/test_cases.py +++ b/tests/test_cases.py @@ -30,18 +30,26 @@ def default(self, obj): if hasattr(module,name): tested += 1 print(f"Running {case} pf and opf",end="...",flush=True,file=sys.stdout) - model = getattr(module,name) - casedata = model() + + casedata = getattr(module,name)() json.dump(casedata,open(f"{name}.json","w"), cls=NumpyEncoder, indent=4) + ppopt = ppoption(VERBOSE=0,OUT_ALL=0) - assert runpf(casedata,ppopt)[1] == 1, "runpf failed" - assert runopf(casedata,ppopt)["success"], "runopf failed" + + result = runpf(casedata,ppopt) + print(result,file=open(f"{name}_pf.out","w")) + assert result[1] == 1, "runpf failed" + + result = runopf(casedata,ppopt) + print(result,file=open(f"{name}_opf.out","w")) + assert result["success"], "runopf failed" + print("ok",file=sys.stdout) except Exception as err: print(f"ERROR [{name}]: {err}",file=sys.stderr) failed += 1 print(f"Testing completed: {tested=}, {failed=}") -exit(1 if failed > 0 else 0) \ No newline at end of file +exit(1 if failed > 0 else 0) From 8fc43c5d8f8971220aa76515b2883b342e24f7a1 Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Thu, 12 Jun 2025 21:35:37 -0700 Subject: [PATCH 08/36] Fixed case4gs test failure (OPF is not possible without gencost data) --- tests/.gitignore | 3 ++- tests/test_cases.py | 28 +++++++++++++++++++++------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/tests/.gitignore b/tests/.gitignore index 035424a..f9f321d 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -1,2 +1,3 @@ *.json -*.out \ No newline at end of file +*.out +*.err diff --git a/tests/test_cases.py b/tests/test_cases.py index 2b2d588..4fe2684 100644 --- a/tests/test_cases.py +++ b/tests/test_cases.py @@ -1,10 +1,18 @@ -"""PyPOWER Module testing""" +"""PyPOWER Module testing + +This script run all the cases in the pypower folder for both PF and OPF solutions. +All results are stored in the `case*_pf.out` and `case*_opf.out` unless there is an +exception generated by the solver itself, in which case the traceback is stored +in the file `case*.err`. If the PF fails, the OPF is not attempted. + +""" import os import sys import importlib import json import numpy as np +import traceback MODULEDIR = ".." CASEDIR = "../pypower" @@ -29,7 +37,7 @@ def default(self, obj): try: if hasattr(module,name): tested += 1 - print(f"Running {case} pf and opf",end="...",flush=True,file=sys.stdout) + print(f"Running {case} pf and opf",end="... ",flush=True,file=sys.stdout) casedata = getattr(module,name)() json.dump(casedata,open(f"{name}.json","w"), @@ -41,14 +49,20 @@ def default(self, obj): result = runpf(casedata,ppopt) print(result,file=open(f"{name}_pf.out","w")) assert result[1] == 1, "runpf failed" - - result = runopf(casedata,ppopt) - print(result,file=open(f"{name}_opf.out","w")) - assert result["success"], "runopf failed" - print("ok",file=sys.stdout) + if 'gencost' in casedata: + result = runopf(casedata,ppopt) + print(result,file=open(f"{name}_opf.out","w")) + assert result["success"], "runopf failed" + + print("ok.",file=sys.stdout) + except Exception as err: print(f"ERROR [{name}]: {err}",file=sys.stderr) + e_type,e_value,e_trace = sys.exc_info() + with open(f"{name}.err","w") as fh: + trace = '\n'.join(traceback.format_tb(e_trace)) + print(f"EXCEPTION [{e_type.__name__}]: {e_value}\n\n{trace}",file=fh) failed += 1 print(f"Testing completed: {tested=}, {failed=}") From 56059a2ba132ae912326803a63d668a2cfcd0abc Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Fri, 13 Jun 2025 08:31:02 -0700 Subject: [PATCH 09/36] Update .gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3586b19..6da2c6c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,8 @@ *~ build/ dist/ -PYPOWER.egg-info/ +*.egg-info/ doc/api/ doc/.build/* *venv/ +__pycache__/ From 7a0f7b0954ad4cfb61033e3a9999f40ec5671631 Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Fri, 13 Jun 2025 08:31:10 -0700 Subject: [PATCH 10/36] Update requirements.txt --- requirements.txt | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8ce107c..0ddfc4e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,20 @@ appdirs==1.4.4 -distlib==0.3.1 -filelock==3.0.12 +cachetools==6.0.0 +chardet==5.2.0 +colorama==0.4.6 +distlib==0.3.9 +filelock==3.18.0 numpy==2.3.0 -packaging==20.9 -pip==25.1.1 -pluggy==0.13.1 -py==1.10.0 -pyparsing==2.4.7 +packaging==25.0 +platformdirs==4.3.8 +pluggy==1.6.0 +py==1.11.0 +pyparsing==3.2.3 +pyproject-api==1.9.1 pyrlu==0.2.1 scipy==1.15.3 -setuptools==65.5.1 -six==1.15.0 +setuptools<81 +six==1.17.0 toml==0.10.2 -tox==3.23.0 -virtualenv==20.4.3 +tox==4.26.0 +virtualenv==20.31.2 From 5b1b302ba7bdb38bab1c3b96be00c2df90058365 Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Fri, 13 Jun 2025 08:31:15 -0700 Subject: [PATCH 11/36] Update tox.ini --- tox.ini | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index ee4b670..c75c489 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,9 @@ [tox] -envlist = py37,py38,py39 +env_list = + 3.9 + 3.10 + 3.11 + 3.12 [testenv] deps = From e4f880448c30a87f287b93b4e349b40882bb2123 Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Fri, 13 Jun 2025 08:31:25 -0700 Subject: [PATCH 12/36] Update test_cases.py --- tests/test_cases.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tests/test_cases.py b/tests/test_cases.py index 4fe2684..b800234 100644 --- a/tests/test_cases.py +++ b/tests/test_cases.py @@ -18,6 +18,7 @@ CASEDIR = "../pypower" sys.path.extend([MODULEDIR,CASEDIR]) +import pkg_resources from pypower.api import runpf, runopf, ppoption tested = 0 @@ -29,12 +30,27 @@ def default(self, obj): return obj.tolist() return super().default(obj) -print(f"Testing all cases in {CASEDIR}...") -for case in os.listdir("../pypower"): +def delete(files): + for file in files: + try: + os.remove(file) + except: + pass + +version = pkg_resources.require('pypower')[0].version + +# first run tox testing of pypower +os.system(f"{os.environ['_']} {CASEDIR}/t/test_pypower.py") + +# now run pypower cases +print(f"Testing all pypower v{version} cases in {CASEDIR}...") +for case in os.listdir(CASEDIR): if case.startswith("case") and case.endswith(".py"): name = os.path.splitext(case)[0] module = importlib.__import__(name) try: + + delete([f"{name}_pf.out",f"{name}_opf.out",f"{name}.err"]) if hasattr(module,name): tested += 1 print(f"Running {case} pf and opf",end="... ",flush=True,file=sys.stdout) @@ -58,12 +74,14 @@ def default(self, obj): print("ok.",file=sys.stdout) except Exception as err: + print(f"ERROR [{name}]: {err}",file=sys.stderr) e_type,e_value,e_trace = sys.exc_info() with open(f"{name}.err","w") as fh: trace = '\n'.join(traceback.format_tb(e_trace)) print(f"EXCEPTION [{e_type.__name__}]: {e_value}\n\n{trace}",file=fh) failed += 1 + print(f"Testing completed: {tested=}, {failed=}") exit(1 if failed > 0 else 0) From c900ec385cad2cc19df50d8a917679b8eb5c820a Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Fri, 13 Jun 2025 08:31:30 -0700 Subject: [PATCH 13/36] Update polycost.py --- pypower/polycost.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pypower/polycost.py b/pypower/polycost.py index fd57359..bb2ee83 100644 --- a/pypower/polycost.py +++ b/pypower/polycost.py @@ -30,7 +30,7 @@ def polycost(gencost, Pg, der=0): """ if gencost.size == 0: #User has a purely linear piecewise problem, exit early with empty array - return [] + return zeros(0) if any(gencost[:, MODEL] == PW_LINEAR): sys.stderr.write('polycost: all costs must be polynomial\n') From f202870dc818905e032d46b57fb8e3b76a3fb6dc Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Fri, 13 Jun 2025 08:31:33 -0700 Subject: [PATCH 14/36] Update opf_hessfcn.py --- pypower/opf_hessfcn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pypower/opf_hessfcn.py b/pypower/opf_hessfcn.py index de842f5..c940e09 100644 --- a/pypower/opf_hessfcn.py +++ b/pypower/opf_hessfcn.py @@ -103,7 +103,7 @@ def opf_hessfcn(x, lmbda, om, Ybus, Yf, Yt, ppopt, il=None, cost_mult=1.0): ipolp = find(pcost[:, MODEL] == POLYNOMIAL) d2f_dPg2[ipolp] = \ baseMVA**2 * polycost(pcost[ipolp, :], Pg[ipolp] * baseMVA, 2) - if any(qcost): ## Qg is not free + if qcost.any(): ## Qg is not free ipolq = find(qcost[:, MODEL] == POLYNOMIAL) d2f_dQg2[ipolq] = \ baseMVA**2 * polycost(qcost[ipolq, :], Qg[ipolq] * baseMVA, 2) From 7bfef236787a687934fecd7380599e0d81e9746a Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Fri, 13 Jun 2025 08:37:38 -0700 Subject: [PATCH 15/36] Update run-test-cases.yml --- .github/workflows/run-test-cases.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/run-test-cases.yml b/.github/workflows/run-test-cases.yml index 53e2174..8a9814a 100644 --- a/.github/workflows/run-test-cases.yml +++ b/.github/workflows/run-test-cases.yml @@ -28,3 +28,11 @@ jobs: run: | cd tests python3 test_cases.py + + - name: Upload artifacts on failure + uses: actions/upload-artifact@v4 + if: failure() + with: + name: test-results + path: | + tests/ From 25ba80898f3f1709c0faf095e3690699a624089b Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Fri, 13 Jun 2025 09:47:40 -0700 Subject: [PATCH 16/36] Update requirements.txt --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0ddfc4e..342e5d2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +# only exact versions allowed (<, >, <=, >= not supported by testing) appdirs==1.4.4 cachetools==6.0.0 chardet==5.2.0 @@ -13,7 +14,7 @@ pyparsing==3.2.3 pyproject-api==1.9.1 pyrlu==0.2.1 scipy==1.15.3 -setuptools<81 +setuptools==80.9.0 six==1.17.0 toml==0.10.2 tox==4.26.0 From 95e338f753e675bb998c30fb48ebda3f75233fd3 Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Fri, 13 Jun 2025 09:47:46 -0700 Subject: [PATCH 17/36] Update test_cases.py --- tests/test_cases.py | 61 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 7 deletions(-) diff --git a/tests/test_cases.py b/tests/test_cases.py index b800234..4316f6b 100644 --- a/tests/test_cases.py +++ b/tests/test_cases.py @@ -13,52 +13,99 @@ import json import numpy as np import traceback +import pkg_resources +import types + +version = pkg_resources.require('pypower')[0].version MODULEDIR = ".." -CASEDIR = "../pypower" +TESTDIR = f"{MODULEDIR}/pypower/t" +CASEDIR = f"{MODULEDIR}/pypower" +IGNORE = [ # module that should be ignored by modules() + "appdirs", + "cachetools", + "chardet", + "colorama", + "distlib", + "filelock", + "packaging", + "platformdirs", + "pluggy", + "py", + "pyparsing", + "pyproject-api", + "setuptools", + "six", + "toml", + "tox", + "virtualenv", +] sys.path.extend([MODULEDIR,CASEDIR]) -import pkg_resources from pypower.api import runpf, runopf, ppoption tested = 0 failed = 0 +def modules(): + """Get pypower runtime required modules in appjson format""" + with open(f"{MODULEDIR}/requirements.txt","r") as fh: + reqs = dict([x.strip().split("==",1) for x in fh.readlines() + if not x.strip().startswith("#")]) + return {x:{"version":y} for x,y in reqs.items() if x not in IGNORE} + class NumpyEncoder(json.JSONEncoder): + """JSON encoder for numpy arrays""" def default(self, obj): if isinstance(obj, np.ndarray): return obj.tolist() return super().default(obj) def delete(files): + """Delete files if found""" for file in files: try: os.remove(file) except: pass -version = pkg_resources.require('pypower')[0].version +def savejson(casedata,fh,**kwargs): + """Save casedata as a JSON application file""" + if "indent" not in kwargs: + kwargs["indent"] = 4 + json.dump({ + "application": "pypower", + "version": version, + "modules" : modules(), + "casedata" : casedata, + },fh,cls=NumpyEncoder,**kwargs) # first run tox testing of pypower -os.system(f"{os.environ['_']} {CASEDIR}/t/test_pypower.py") +print(f"Testing all pypower v{version} tests in {TESTDIR}...") +os.system(f"{os.environ['_']} {TESTDIR}/test_pypower.py") # now run pypower cases print(f"Testing all pypower v{version} cases in {CASEDIR}...") for case in os.listdir(CASEDIR): + + # only test files that start with "case" and end in ".py" if case.startswith("case") and case.endswith(".py"): name = os.path.splitext(case)[0] module = importlib.__import__(name) try: + # clean-up any old output files delete([f"{name}_pf.out",f"{name}_opf.out",f"{name}.err"]) + + # only test modules that contain a function by the same name per convention if hasattr(module,name): + tested += 1 print(f"Running {case} pf and opf",end="... ",flush=True,file=sys.stdout) + # get case data from file casedata = getattr(module,name)() - json.dump(casedata,open(f"{name}.json","w"), - cls=NumpyEncoder, - indent=4) + savejson(casedata,open(f"{name}.json","w")) ppopt = ppoption(VERBOSE=0,OUT_ALL=0) From 89946bae19589c271c6778b9cc824fedbdf8f5f8 Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Fri, 13 Jun 2025 09:48:15 -0700 Subject: [PATCH 18/36] Update test_cases.py --- tests/test_cases.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cases.py b/tests/test_cases.py index 4316f6b..cea0b4d 100644 --- a/tests/test_cases.py +++ b/tests/test_cases.py @@ -21,7 +21,7 @@ MODULEDIR = ".." TESTDIR = f"{MODULEDIR}/pypower/t" CASEDIR = f"{MODULEDIR}/pypower" -IGNORE = [ # module that should be ignored by modules() +IGNORE = [ # modules that should be ignored by modules() "appdirs", "cachetools", "chardet", From 6c220feb48cfc85fbbf69eb9f2c3ec06583c1ed9 Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Fri, 13 Jun 2025 11:03:50 -0700 Subject: [PATCH 19/36] Fixed CPF error --- pypower/cpf_p_jac.py | 4 ++-- tests/test_cases.py | 49 +++++++++++++++++++++++++++++++++----------- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/pypower/cpf_p_jac.py b/pypower/cpf_p_jac.py index 025f819..54a6d73 100644 --- a/pypower/cpf_p_jac.py +++ b/pypower/cpf_p_jac.py @@ -1,7 +1,7 @@ '''Computes partial derivatives of CPF parameterization function. ''' -from numpy import r_, zeros, angle +from numpy import r_, zeros, angle, array def cpf_p_jac(parameterization, z, V, lam, Vprv, lamprv, pv, pq): @@ -33,4 +33,4 @@ def cpf_p_jac(parameterization, z, V, lam, Vprv, lamprv, pv, pq): dP_dV = z[r_[pv, pq, nb+pq]] dP_dlam = z[2 * nb] - return dP_dV, dP_dlam \ No newline at end of file + return dP_dV, array([dP_dlam]) \ No newline at end of file diff --git a/tests/test_cases.py b/tests/test_cases.py index cea0b4d..223daf0 100644 --- a/tests/test_cases.py +++ b/tests/test_cases.py @@ -42,7 +42,7 @@ ] sys.path.extend([MODULEDIR,CASEDIR]) -from pypower.api import runpf, runopf, ppoption +from pypower.api import runpf, runcpf, runopf, ppoption, opf_model tested = 0 failed = 0 @@ -59,6 +59,10 @@ class NumpyEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, np.ndarray): return obj.tolist() + if isinstance(obj, opf_model): + return {x:(y.tolist() if hasattr(y,"tolist") else y) for x,y in obj.user_data.items()} + if isinstance(obj, complex): + return f"{obj.real:g}{obj.imag:+g}j" return super().default(obj) def delete(files): @@ -69,7 +73,7 @@ def delete(files): except: pass -def savejson(casedata,fh,**kwargs): +def savejson(casedata,fh,result=None,**kwargs): """Save casedata as a JSON application file""" if "indent" not in kwargs: kwargs["indent"] = 4 @@ -77,7 +81,8 @@ def savejson(casedata,fh,**kwargs): "application": "pypower", "version": version, "modules" : modules(), - "casedata" : casedata, + "problem" : casedata, + "solution" : result, },fh,cls=NumpyEncoder,**kwargs) # first run tox testing of pypower @@ -101,20 +106,33 @@ def savejson(casedata,fh,**kwargs): if hasattr(module,name): tested += 1 - print(f"Running {case} pf and opf",end="... ",flush=True,file=sys.stdout) + print(f"Solving {case} problems",end="... ",flush=True,file=sys.stdout) # get case data from file - casedata = getattr(module,name)() - savejson(casedata,open(f"{name}.json","w")) - + casedata = getattr(module,name)() ppopt = ppoption(VERBOSE=0,OUT_ALL=0) - result = runpf(casedata,ppopt) - print(result,file=open(f"{name}_pf.out","w")) - assert result[1] == 1, "runpf failed" + if "target" in name: + + print("CPF",end="... ",flush=True,file=sys.stdout) + result = runcpf(f"{CASEDIR}/{name}.py".replace("target",""),casedata,ppopt) + savejson({"basecase":result,"target":casedata},open(f"{name}.json","w"),result) + print(result,file=open(f"{name}_cpf.out","w")) + assert result[1] == 1, "runcpf failed" + + else: + + print("PF",end="... ",flush=True,file=sys.stdout) + result = runpf(casedata,ppopt) + savejson(casedata,open(f"{name}.json","w"),result) + print(result,file=open(f"{name}_pf.out","w")) + assert result[1] == 1, "runpf failed" + + if "gencost" in casedata and "target" not in name: - if 'gencost' in casedata: + print("OPF",end="... ",flush=True,file=sys.stdout) result = runopf(casedata,ppopt) + savejson(casedata,open(f"{name}.json","w"),result) print(result,file=open(f"{name}_opf.out","w")) assert result["success"], "runopf failed" @@ -124,7 +142,14 @@ def savejson(casedata,fh,**kwargs): print(f"ERROR [{name}]: {err}",file=sys.stderr) e_type,e_value,e_trace = sys.exc_info() - with open(f"{name}.err","w") as fh: + e_file = f"{name}.err" + savejson(casedata,open(f"{name}.json","w"), + result={ + "exception":e_type.__name__, + "value":str(e_value), + "traceback":e_file + }) + with open(e_file,"w") as fh: trace = '\n'.join(traceback.format_tb(e_trace)) print(f"EXCEPTION [{e_type.__name__}]: {e_value}\n\n{trace}",file=fh) failed += 1 From 4e83138676e21bd738075a3187b68de4189f383d Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Fri, 13 Jun 2025 11:07:20 -0700 Subject: [PATCH 20/36] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index dcf5a9c..3156057 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='PYPOWER', - version='5.1.19a', + version='5.1.19', author='Richard Lincoln', author_email='r.w.lincoln@gmail.com', description='Solves power flow and optimal power flow problems', From 71efeae139d743f8da8d71f5ae57594eee79c939 Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Fri, 13 Jun 2025 11:18:57 -0700 Subject: [PATCH 21/36] Update test_cases.py --- tests/test_cases.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_cases.py b/tests/test_cases.py index 223daf0..0de2f2b 100644 --- a/tests/test_cases.py +++ b/tests/test_cases.py @@ -128,13 +128,13 @@ def savejson(casedata,fh,result=None,**kwargs): print(result,file=open(f"{name}_pf.out","w")) assert result[1] == 1, "runpf failed" - if "gencost" in casedata and "target" not in name: + if "gencost" in casedata: - print("OPF",end="... ",flush=True,file=sys.stdout) - result = runopf(casedata,ppopt) - savejson(casedata,open(f"{name}.json","w"),result) - print(result,file=open(f"{name}_opf.out","w")) - assert result["success"], "runopf failed" + print("OPF",end="... ",flush=True,file=sys.stdout) + result = runopf(casedata,ppopt) + savejson(casedata,open(f"{name}.json","w"),result) + print(result,file=open(f"{name}_opf.out","w")) + assert result["success"], "runopf failed" print("ok.",file=sys.stdout) From 66c5e2116ed6bc319f9897903f3b4757f2a3a804 Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Fri, 13 Jun 2025 11:37:18 -0700 Subject: [PATCH 22/36] Update test_cases.py --- tests/test_cases.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/test_cases.py b/tests/test_cases.py index 0de2f2b..a1c82f6 100644 --- a/tests/test_cases.py +++ b/tests/test_cases.py @@ -5,6 +5,27 @@ exception generated by the solver itself, in which case the traceback is stored in the file `case*.err`. If the PF fails, the OPF is not attempted. +Case files with the string "target" in them will be run as CPF problems instead of +PF/OPF problems. + +The output JSON file includes both the problem and solution, as well as supporting +information about the test environment. + +Before running the test cases, the script also runs the tox tests in the pypower +folder. + +To run the test procedure, use the following command: + +~~~ +pip install . +python3 tests/test_cases.py +~~~ + +The following exit codes are used: + +* `0`: all tests passed ok +* `1`: one or more test cases failed +* `2`: one of more unit tests failed """ import os @@ -16,6 +37,8 @@ import pkg_resources import types +os.chdir(os.path.split(sys.argv[0])[0]) + version = pkg_resources.require('pypower')[0].version MODULEDIR = ".." @@ -87,7 +110,10 @@ def savejson(casedata,fh,result=None,**kwargs): # first run tox testing of pypower print(f"Testing all pypower v{version} tests in {TESTDIR}...") -os.system(f"{os.environ['_']} {TESTDIR}/test_pypower.py") +if os.system(f"{os.environ['_']} {TESTDIR}/test_pypower.py") != 0: + + print(f"ERROR [{os.path.basename(sys.argv[0])}]: pypower unit tests failed--case testing cannot continue",file= sys.stderr) + exit(2) # now run pypower cases print(f"Testing all pypower v{version} cases in {CASEDIR}...") From a91eb495097488bf7d13646e6f6ccef94aaed97e Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Fri, 13 Jun 2025 11:40:37 -0700 Subject: [PATCH 23/36] cleanup --- .github/workflows/run-test-cases.yml | 4 +--- tests/test_cases.py | 9 +++++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/run-test-cases.yml b/.github/workflows/run-test-cases.yml index 8a9814a..095d49b 100644 --- a/.github/workflows/run-test-cases.yml +++ b/.github/workflows/run-test-cases.yml @@ -25,9 +25,7 @@ jobs: run: python3 -m pip install --upgrade pip -r requirements.txt - name: Run test cases - run: | - cd tests - python3 test_cases.py + run: python3 tests/test_cases.py - name: Upload artifacts on failure uses: actions/upload-artifact@v4 diff --git a/tests/test_cases.py b/tests/test_cases.py index a1c82f6..0608ae7 100644 --- a/tests/test_cases.py +++ b/tests/test_cases.py @@ -37,7 +37,8 @@ import pkg_resources import types -os.chdir(os.path.split(sys.argv[0])[0]) +EXEDIR,EXENAME = os.path.split(sys.argv[0]) +os.chdir(EXEDIR if EXEDIR else ".") version = pkg_resources.require('pypower')[0].version @@ -112,7 +113,7 @@ def savejson(casedata,fh,result=None,**kwargs): print(f"Testing all pypower v{version} tests in {TESTDIR}...") if os.system(f"{os.environ['_']} {TESTDIR}/test_pypower.py") != 0: - print(f"ERROR [{os.path.basename(sys.argv[0])}]: pypower unit tests failed--case testing cannot continue",file= sys.stderr) + print(f"ERROR [{EXENAME}]: pypower unit tests failed--case testing cannot continue",file= sys.stderr) exit(2) # now run pypower cases @@ -166,7 +167,7 @@ def savejson(casedata,fh,result=None,**kwargs): except Exception as err: - print(f"ERROR [{name}]: {err}",file=sys.stderr) + print(f"ERROR [{EXENAME}]: {name} -- {err}",file=sys.stderr) e_type,e_value,e_trace = sys.exc_info() e_file = f"{name}.err" savejson(casedata,open(f"{name}.json","w"), @@ -177,7 +178,7 @@ def savejson(casedata,fh,result=None,**kwargs): }) with open(e_file,"w") as fh: trace = '\n'.join(traceback.format_tb(e_trace)) - print(f"EXCEPTION [{e_type.__name__}]: {e_value}\n\n{trace}",file=fh) + print(f"EXCEPTION [{name}]: {e_type.__name__} -- {e_value}\n\n{trace}",file=fh) failed += 1 print(f"Testing completed: {tested=}, {failed=}") From b7937c336fb232202f54f82bab7a448494b8c483 Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Fri, 13 Jun 2025 11:41:26 -0700 Subject: [PATCH 24/36] Update tox.ini --- tox.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tox.ini b/tox.ini index c75c489..920d039 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,9 @@ [tox] env_list = + 3.5 + 3.6 + 3.7 + 3.8 3.9 3.10 3.11 From 15390c147d8a417691f2e2ba23b12250079fd7a0 Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Fri, 13 Jun 2025 11:59:48 -0700 Subject: [PATCH 25/36] Update test docs --- README.rst | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index a0267ce..9e7ec37 100644 --- a/README.rst +++ b/README.rst @@ -43,7 +43,7 @@ Virtual Environment PYPOWER is recommended to be installed into a virtual environment:: - $ python3.8 -m venv venv # Or any supported Python version + $ python3.12 -m venv venv # Or any supported Python version Dependencies ============ @@ -70,7 +70,23 @@ Testing PYPOWER can be tested locally using the same tooling as on Travis CI:: - $ venv/bin/python -m tox -e py27,py38 # Or any supported Python version + $ venv/bin/python -m tox -e 3.10,3.11,3.12 # Or any supported Python version + +Case Testing +============ + +The cases in the `pypower` folder can also be tested locally using the command: + +~~~ +python3.x -m venv .venv +. .venv/bin/activate +python3 -m pip install -m pip --upgrade -r requirements.txt +python3 tests/test_cases.py +~~~ + +where `x` is one of the supported python minor version numbers. + +**Note**: this test first runs the tox tests in all the supported environment. Using PYPOWER ============= From 911757441a924520f4f67e01e4c3214e73b708af Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Fri, 13 Jun 2025 12:00:45 -0700 Subject: [PATCH 26/36] Update supported python versions --- tox.ini | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tox.ini b/tox.ini index 920d039..ddba470 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,5 @@ [tox] env_list = - 3.5 - 3.6 - 3.7 - 3.8 - 3.9 3.10 3.11 3.12 From 4f847da94376ab213545faafdcbb2081b75577ac Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Fri, 13 Jun 2025 12:01:00 -0700 Subject: [PATCH 27/36] Update numpy support to match supported environments --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 342e5d2..0100668 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ chardet==5.2.0 colorama==0.4.6 distlib==0.3.9 filelock==3.18.0 -numpy==2.3.0 +numpy==2.2.6 packaging==25.0 platformdirs==4.3.8 pluggy==1.6.0 From 66dc5c8ba68720fcc8cfdb0469a2d95a6f11758e Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Fri, 13 Jun 2025 12:01:24 -0700 Subject: [PATCH 28/36] Make test script using tox environments for unit tests --- tests/test_cases.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/test_cases.py b/tests/test_cases.py index 0608ae7..4cb45c6 100644 --- a/tests/test_cases.py +++ b/tests/test_cases.py @@ -42,6 +42,7 @@ version = pkg_resources.require('pypower')[0].version + MODULEDIR = ".." TESTDIR = f"{MODULEDIR}/pypower/t" CASEDIR = f"{MODULEDIR}/pypower" @@ -110,8 +111,12 @@ def savejson(casedata,fh,result=None,**kwargs): },fh,cls=NumpyEncoder,**kwargs) # first run tox testing of pypower -print(f"Testing all pypower v{version} tests in {TESTDIR}...") -if os.system(f"{os.environ['_']} {TESTDIR}/test_pypower.py") != 0: +import configparser +config = configparser.ConfigParser() +config.read(os.path.join(MODULEDIR,"tox.ini")) +envlist = config["tox"]["env_list"].strip().split("\n") +print(f"Testing all pypower v{version} tests in {TESTDIR} using python environment {envlist}...") +if os.system(f"{os.environ['_']} -m tox -e {','.join(envlist)}") != 0: print(f"ERROR [{EXENAME}]: pypower unit tests failed--case testing cannot continue",file= sys.stderr) exit(2) From 5b32fbf26e0d6380cd9569bf7d206caf18dc5d84 Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Fri, 13 Jun 2025 12:05:07 -0700 Subject: [PATCH 29/36] Revert tox test to just run in the current environment --- README.rst | 4 +++- tests/test_cases.py | 10 ++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index 9e7ec37..8769fcb 100644 --- a/README.rst +++ b/README.rst @@ -86,7 +86,9 @@ python3 tests/test_cases.py where `x` is one of the supported python minor version numbers. -**Note**: this test first runs the tox tests in all the supported environment. +See the `tests/test_cases.py` script for additional information on the output files. + +**Note**: this test first runs the tox tests in the current environment. Using PYPOWER ============= diff --git a/tests/test_cases.py b/tests/test_cases.py index 4cb45c6..dfd767f 100644 --- a/tests/test_cases.py +++ b/tests/test_cases.py @@ -42,7 +42,6 @@ version = pkg_resources.require('pypower')[0].version - MODULEDIR = ".." TESTDIR = f"{MODULEDIR}/pypower/t" CASEDIR = f"{MODULEDIR}/pypower" @@ -111,13 +110,8 @@ def savejson(casedata,fh,result=None,**kwargs): },fh,cls=NumpyEncoder,**kwargs) # first run tox testing of pypower -import configparser -config = configparser.ConfigParser() -config.read(os.path.join(MODULEDIR,"tox.ini")) -envlist = config["tox"]["env_list"].strip().split("\n") -print(f"Testing all pypower v{version} tests in {TESTDIR} using python environment {envlist}...") -if os.system(f"{os.environ['_']} -m tox -e {','.join(envlist)}") != 0: - +print(f"Testing all pypower v{version} tests in {TESTDIR}...") +if os.system(f"{os.environ['_']} {TESTDIR}/test_pypower.py") != 0: print(f"ERROR [{EXENAME}]: pypower unit tests failed--case testing cannot continue",file= sys.stderr) exit(2) From 57868dacbad4c1c59847b949b6969f67bf9330b2 Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Fri, 13 Jun 2025 12:18:07 -0700 Subject: [PATCH 30/36] Update README.rst --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 8769fcb..dc5061f 100644 --- a/README.rst +++ b/README.rst @@ -88,7 +88,7 @@ where `x` is one of the supported python minor version numbers. See the `tests/test_cases.py` script for additional information on the output files. -**Note**: this test first runs the tox tests in the current environment. +**Note**: this test first runs the `tox` tests in the current environment. Using PYPOWER ============= From 62870714dd987be01da1508017c89e2b1592073d Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Fri, 13 Jun 2025 12:20:08 -0700 Subject: [PATCH 31/36] Update README.rst --- README.rst | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/README.rst b/README.rst index dc5061f..e9e0de1 100644 --- a/README.rst +++ b/README.rst @@ -43,50 +43,49 @@ Virtual Environment PYPOWER is recommended to be installed into a virtual environment:: - $ python3.12 -m venv venv # Or any supported Python version + python3.12 -m venv venv # Or any supported Python version Dependencies ============ PYPOWER depends upon NumPy, SciPy and PyRLU which can be installed as follows:: - $ venv/bin/python -m pip install -r requirements.txt + venv/bin/python -m pip install -r requirements.txt Installation ============ The recommended way of installing PYPOWER is using pip_:: - $ venv/bin/python -m pip install PYPOWER + venv/bin/python -m pip install PYPOWER Alternatively, `download `_ and unpack the tarball and install:: - $ tar zxf PYPOWER-5.x.y.tar.gz - $ venv/bin/python setup.py install + tar zxf PYPOWER-5.x.y.tar.gz + venv/bin/python setup.py install Testing ======= PYPOWER can be tested locally using the same tooling as on Travis CI:: - $ venv/bin/python -m tox -e 3.10,3.11,3.12 # Or any supported Python version + venv/bin/python -m tox -e 3.10,3.11,3.12 # Or any supported Python version Case Testing ============ The cases in the `pypower` folder can also be tested locally using the command: -~~~ -python3.x -m venv .venv -. .venv/bin/activate -python3 -m pip install -m pip --upgrade -r requirements.txt -python3 tests/test_cases.py -~~~ + + python3.x -m venv .venv + . .venv/bin/activate + python3 -m pip install -m pip --upgrade -r requirements.txt + python3 tests/test_cases.py where `x` is one of the supported python minor version numbers. -See the `tests/test_cases.py` script for additional information on the output files. +See the [tests/test_cases.py] script for additional information on the output files. **Note**: this test first runs the `tox` tests in the current environment. @@ -96,25 +95,25 @@ Using PYPOWER Installing PYPOWER creates ``pf`` and ``opf`` commands. To list the command options:: - $ venv/bin/pf -h + venv/bin/pf -h or:: - $ venv/bin/opf -h + venv/bin/opf -h PYPOWER includes a selection of test cases. For example, to run a power flow on the IEEE 14 bus test case:: - $ venv/bin/pf -c case14 + venv/bin/pf -c case14 Alternatively, the path to a PYPOWER case data file can be specified:: - $ venv/bin/pf /path/to/case14.py + venv/bin/pf /path/to/case14.py The ``opf`` command has the same calling syntax. For example, to solve an OPF for the IEEE Reliability Test System and write the solved case to file:: - $ venv/bin/opf -c case24_ieee_rts --solvedcase=rtsout.py + venv/bin/opf -c case24_ieee_rts --solvedcase=rtsout.py For further information please refer to https://rwl.github.io/PYPOWER/ and the `API documentation`_. From 44a1ee2abf841a14af926846c0909eb2d3b86434 Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Fri, 13 Jun 2025 12:28:30 -0700 Subject: [PATCH 32/36] Update README.rst --- README.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index e9e0de1..790a235 100644 --- a/README.rst +++ b/README.rst @@ -36,7 +36,7 @@ Prerequisites PYPOWER depends upon these prerequisites on the level of the operating system: -* Python_ >= 3.5 +* Python_ >= 3.10 Virtual Environment =================== @@ -75,8 +75,7 @@ PYPOWER can be tested locally using the same tooling as on Travis CI:: Case Testing ============ -The cases in the `pypower` folder can also be tested locally using the command: - +The cases in the `pypower` folder can also be tested locally using the command:: python3.x -m venv .venv . .venv/bin/activate @@ -85,7 +84,7 @@ The cases in the `pypower` folder can also be tested locally using the command: where `x` is one of the supported python minor version numbers. -See the [tests/test_cases.py] script for additional information on the output files. +See the `tests/test_cases.py `_ script for additional information on the output files. **Note**: this test first runs the `tox` tests in the current environment. From 6d1b6b15ac2250a3285f8fdfa3e89644601b2b65 Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Fri, 13 Jun 2025 12:30:11 -0700 Subject: [PATCH 33/36] Update README.rst --- README.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 790a235..8a4edc0 100644 --- a/README.rst +++ b/README.rst @@ -84,7 +84,7 @@ The cases in the `pypower` folder can also be tested locally using the command:: where `x` is one of the supported python minor version numbers. -See the `tests/test_cases.py `_ script for additional information on the output files. +See the `tests/test_cases.py `_ script for additional information on the output files. **Note**: this test first runs the `tox` tests in the current environment. @@ -158,6 +158,7 @@ Links * PSAT_ by Federico Milano * OpenDSS_ from EPRI * GridLAB-D_ from PNNL +* Arras-Energy_ from LF Energy * PyCIM_ .. _Python: http://www.python.org @@ -176,3 +177,5 @@ Links .. _TESP: https://tesp.readthedocs.io .. _Oct2PYPOWER: https://github.com/rwl/oct2pypower .. _matpower.app: https://matpower.app +.. _Arras-Energy: https://arras.energy/ + From 05812f2084d2a852b1b4d1a3b52fba13feb2e9f5 Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Fri, 13 Jun 2025 12:31:49 -0700 Subject: [PATCH 34/36] Update README.rst --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 8a4edc0..ea85fc6 100644 --- a/README.rst +++ b/README.rst @@ -158,7 +158,7 @@ Links * PSAT_ by Federico Milano * OpenDSS_ from EPRI * GridLAB-D_ from PNNL -* Arras-Energy_ from LF Energy +* Arras-Energy_ from `LF Energy `_ * PyCIM_ .. _Python: http://www.python.org From dcfd227a9d0420637581b44a6f5f3b33603bdaf7 Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Fri, 13 Jun 2025 12:46:09 -0700 Subject: [PATCH 35/36] Update run-test-cases.yml --- .github/workflows/run-test-cases.yml | 43 ++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/.github/workflows/run-test-cases.yml b/.github/workflows/run-test-cases.yml index 095d49b..a7b83e1 100644 --- a/.github/workflows/run-test-cases.yml +++ b/.github/workflows/run-test-cases.yml @@ -9,9 +9,46 @@ on: branches: [ main ] jobs: - test: - runs-on: ubuntu-latest + ubuntu-matrix: + + strategy: + matrix: + # See https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories + version: [22.04,24.04,latest] + + runs-on: ubuntu-${{matrix.version}} + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: '3.12' + + - name: Install dependencies + run: python3 -m pip install --upgrade pip -r requirements.txt + + - name: Run test cases + run: python3 tests/test_cases.py + + - name: Upload artifacts on failure + uses: actions/upload-artifact@v4 + if: failure() + with: + name: test-results + path: | + tests/ + + macos-matrix: + + strategy: + matrix: + # See https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories + version: [13,14,15,latest] + + runs-on: macos-${{matrix.version}} steps: - uses: actions/checkout@v3 @@ -19,7 +56,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v3 with: - python-version: '3.x' + python-version: '3.12' - name: Install dependencies run: python3 -m pip install --upgrade pip -r requirements.txt From a6385bfad06382831134ef94d641375b669b07b5 Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Fri, 13 Jun 2025 12:46:39 -0700 Subject: [PATCH 36/36] Update run-test-cases.yml --- .github/workflows/run-test-cases.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-test-cases.yml b/.github/workflows/run-test-cases.yml index a7b83e1..1667001 100644 --- a/.github/workflows/run-test-cases.yml +++ b/.github/workflows/run-test-cases.yml @@ -10,7 +10,7 @@ on: jobs: - ubuntu-matrix: + ubuntu-test-matrix: strategy: matrix: @@ -41,7 +41,7 @@ jobs: path: | tests/ - macos-matrix: + macos-test-matrix: strategy: matrix: