From 821297aec458d2300341791294943f53647bdf26 Mon Sep 17 00:00:00 2001 From: Timothy Nunn Date: Fri, 4 Apr 2025 10:53:43 +0000 Subject: [PATCH 1/4] Improve code quality with increased ruff rules --- .pre-commit-config.yaml | 2 +- LICENSE | 2 +- README.md | 2 +- docs/source/conf.py | 2 +- examples/converges.ipynb | 14 ++-- pyproject.toml | 24 +++++- src/pyvmcon/__init__.py | 8 +- src/pyvmcon/exceptions.py | 33 ++++----- src/pyvmcon/problem.py | 35 +++++---- src/pyvmcon/vmcon.py | 150 +++++++++++++++++++++++++++----------- tests/test_vmcon_paper.py | 23 ++++-- 11 files changed, 198 insertions(+), 97 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e1c7982..cc0492e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.4.4 + rev: v0.11.3 hooks: - id: ruff args: [--fix] diff --git a/LICENSE b/LICENSE index e5dfcec..fb498c3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023-2024 UK Atomic Energy Authority +Copyright (c) 2023-2025 UK Atomic Energy Authority Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index d5c975a..bc43fae 100644 --- a/README.md +++ b/README.md @@ -28,4 +28,4 @@ Documentation for the VMCON algorithm and PyVMCON API/use can be found on our [* ## License PyVMCON is provided under the MIT license, please see the LICENSE file for full details. -Copyright (c) 2023-2024 UK Atomic Energy Authority +Copyright (c) 2023-2025 UK Atomic Energy Authority diff --git a/docs/source/conf.py b/docs/source/conf.py index 83581a5..bceee73 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,7 +1,7 @@ # -- Project information ----------------------------------------------------- project = "PyVMCON" -copyright = "2023-2024, UK Atomic Energy Authority" +copyright = "2023-2025, UK Atomic Energy Authority" author = "Timothy Nunn" # The full version, including alpha/beta/rc tags diff --git a/examples/converges.ipynb b/examples/converges.ipynb index dfebdfa..95c20f8 100644 --- a/examples/converges.ipynb +++ b/examples/converges.ipynb @@ -30,13 +30,15 @@ "metadata": {}, "outputs": [], "source": [ - "from typing import List\n", "import numpy as np\n", + "\n", "from pyvmcon import Problem\n", "\n", - "def f(x: List):\n", + "\n", + "def f(x: list):\n", " return (x[0] - 2) ** 2 + (x[1] - 1) ** 2\n", "\n", + "\n", "problem = Problem(\n", " f,\n", " lambda x: np.array([2 * (x[0] - 2), 2 * (x[1] - 1)]),\n", @@ -63,7 +65,7 @@ "source": [ "from pyvmcon import solve\n", "\n", - "initial_x = np.array([2,2])\n", + "initial_x = np.array([2, 2])\n", "\n", "x, lamda_equality, lamda_inequality, result = solve(problem, initial_x)\n", "\n", @@ -98,9 +100,9 @@ "outputs": [], "source": [ "x, lamda_equality, lamda_inequality, result = solve(\n", - " problem, \n", - " initial_x, \n", - " lbs=np.array([-10, -10]), \n", + " problem,\n", + " initial_x,\n", + " lbs=np.array([-10, -10]),\n", " ubs=np.array([10, 10]),\n", ")\n", "\n", diff --git a/pyproject.toml b/pyproject.toml index 2fe9f89..3bd3c65 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ keywords = ["vmcon", "optimisation", "non-linear constrained optimisation"] readme = "README.md" license = { file = "LICENSE" } authors = [{ name = "Timothy Nunn", email = "timothy.nunn@ukaea.uk" }] -requires-python = ">=3.8" +requires-python = ">=3.10" dependencies = ["numpy>=1.19", "cvxpy>=1.5.2"] classifiers = [ "Intended Audience :: Science/Research", @@ -34,3 +34,25 @@ docs = [ [tool.setuptools.packages.find] where = ["src"] + +[tool.ruff] +target-version = "py310" +extend-exclude = ["docs/"] + +[tool.ruff.lint] +select = ["ALL"] +ignore = [ + "D203", + "D213", + "D401", + "COM812", + "PLR", + "N818", + "N803", + "N806", + "N802", +] + +[tool.ruff.lint.per-file-ignores] +"*.ipynb" = ["T201", "S311", "ANN", "D103"] +"tests/*" = ["INP001", "ANN", "D103", "S101", "SLF001", "PLR0915", "PLR2004"] diff --git a/src/pyvmcon/__init__.py b/src/pyvmcon/__init__.py index bc4b2d3..efdaac9 100644 --- a/src/pyvmcon/__init__.py +++ b/src/pyvmcon/__init__.py @@ -1,3 +1,5 @@ +"""Python implementation of the VMCON non-linear constrained optimiser.""" + from .exceptions import ( LineSearchConvergenceException, QSPSolverException, @@ -7,11 +9,11 @@ from .vmcon import solve __all__ = [ - "solve", "AbstractProblem", + "LineSearchConvergenceException", "Problem", + "QSPSolverException", "Result", "VMCONConvergenceException", - "LineSearchConvergenceException", - "QSPSolverException", + "solve", ] diff --git a/src/pyvmcon/exceptions.py b/src/pyvmcon/exceptions.py index 6ff44e1..200953b 100644 --- a/src/pyvmcon/exceptions.py +++ b/src/pyvmcon/exceptions.py @@ -1,4 +1,4 @@ -from typing import Optional +"""Exceptions and errors raised within VMCON.""" import numpy as np @@ -6,21 +6,21 @@ class VMCONConvergenceException(Exception): - """Base class for an exception that indicates VMCON has - failed to converge. This exception allows certain diagnostics - to be passed and propagated with the exception. + """Base error for VMCON errors. + + This exception allows certain diagnostics to be passed and propagated + with the exception. """ def __init__( self, *args: object, - x: Optional[np.ndarray] = None, - result: Optional[Result] = None, - lamda_equality: Optional[np.ndarray] = None, - lamda_inequality: Optional[np.ndarray] = None, + x: np.ndarray | None = None, + result: Result | None = None, + lamda_equality: np.ndarray | None = None, + lamda_inequality: np.ndarray | None = None, ) -> None: - """Constructor for the exception raised when VMCON cannot converge - on a feasible solution. + """Constructs an exception raised within VMCON. Parameters ---------- @@ -39,6 +39,7 @@ def __init__( lamda_inequality : Optional[ndarray] The jth Lagrange multipliers for the inequality constraints + """ super().__init__(*args) @@ -49,18 +50,12 @@ def __init__( class _QspSolveException(Exception): - """An exception that should only be used internally - to identify that the QSP has failed to solve. - """ + """Internal exception indicating the QSP failed to solve.""" class QSPSolverException(VMCONConvergenceException): - """Indicates VMCON failed to solve because the QSP Solver was unable - to solve. - """ + """Indicates VMCON failed to solve because the QSP Solver was unable to solve.""" class LineSearchConvergenceException(VMCONConvergenceException): - """Indicates the line search portion of VMCON was unable to - solve within a pre-defined number of iterations - """ + """Indicates the line search was unable to converge.""" diff --git a/src/pyvmcon/problem.py b/src/pyvmcon/problem.py index 1f71022..c4fd9f8 100644 --- a/src/pyvmcon/problem.py +++ b/src/pyvmcon/problem.py @@ -1,5 +1,8 @@ +"""Defines the interface for VMCON to exchange data with external software.""" + from abc import ABC, abstractmethod -from typing import Callable, List, NamedTuple, TypeVar +from collections.abc import Callable +from typing import NamedTuple, TypeVar import numpy as np @@ -7,7 +10,7 @@ class Result(NamedTuple): - """The data from calling a problem""" + """The data from calling a problem.""" f: T """Value of the objective function""" @@ -40,29 +43,31 @@ class AbstractProblem(ABC): @abstractmethod def __call__(self, x: np.ndarray) -> Result: - pass + """Evaluate the optimisation problem at input x.""" @property @abstractmethod def num_equality(self) -> int: - """Returns the number of equality constraints this problem has""" + """The number of equality constraints this problem has.""" @property def has_equality(self) -> bool: + """Indicates whether or not this problem has equality constraints.""" return self.num_equality > 0 @property def has_inequality(self) -> bool: + """Indicates whether or not this problem has inequality constraints.""" return self.num_inequality > 0 @property @abstractmethod def num_inequality(self) -> int: - """Returns the number of inequality constraints this problem has""" + """The number of inequality constraints this problem has.""" @property def total_constraints(self) -> int: - """Returns the total number of constraints `m`""" + """The total number of constraints `m`.""" return self.num_equality + self.num_inequality @@ -71,20 +76,21 @@ def total_constraints(self) -> int: class Problem(AbstractProblem): - """Provides a simple implementation of an AbstractProblem - that essentially acts as a caller to equations to gather all - of the various data. + """A simple implementation of an AbstractProblem. + + It essentially acts as a caller to equations to gather all of the various data. """ def __init__( self, f: _FunctionAlias, df: _DerivativeFunctionAlias, - equality_constraints: List[_FunctionAlias], - inequality_constraints: List[_FunctionAlias], - dequality_constraints: List[_DerivativeFunctionAlias], - dinequality_constraints: List[_DerivativeFunctionAlias], + equality_constraints: list[_FunctionAlias], + inequality_constraints: list[_FunctionAlias], + dequality_constraints: list[_DerivativeFunctionAlias], + dinequality_constraints: list[_DerivativeFunctionAlias], ) -> None: + """Construct the problem.""" super().__init__() self._f = f @@ -95,6 +101,7 @@ def __init__( self._dinequality_constraints = dinequality_constraints def __call__(self, x: np.ndarray) -> Result: + """Evaluate the problem at input point x.""" return Result( self._f(x), self._df(x), @@ -106,8 +113,10 @@ def __call__(self, x: np.ndarray) -> Result: @property def num_equality(self) -> int: + """The number of equality constraints this problem has.""" return len(self._equality_constraints) @property def num_inequality(self) -> int: + """The number of inequality constraints this problem has.""" return len(self._inequality_constraints) diff --git a/src/pyvmcon/vmcon.py b/src/pyvmcon/vmcon.py index 41a67d8..fa8af6d 100644 --- a/src/pyvmcon/vmcon.py +++ b/src/pyvmcon/vmcon.py @@ -1,5 +1,8 @@ +"""The Python implementation of the VMCON algorithm.""" + import logging -from typing import Any, Callable, Dict, List, Optional, Tuple, Union +from collections.abc import Callable +from typing import Any import cvxpy as cp import numpy as np @@ -18,19 +21,20 @@ def solve( problem: AbstractProblem, x: np.ndarray, - lbs: Optional[np.ndarray] = None, - ubs: Optional[np.ndarray] = None, + lbs: np.ndarray | None = None, + ubs: np.ndarray | None = None, *, max_iter: int = 10, epsilon: float = 1e-8, - qsp_options: Optional[Dict[str, Any]] = None, - initial_B: Optional[np.ndarray] = None, - callback: Optional[Callable[[int, Result, np.ndarray, float], None]] = None, - additional_convergence: Optional[ - Callable[[Result, np.ndarray, np.ndarray, np.ndarray, np.ndarray], None] - ] = None, + qsp_options: dict[str, Any] | None = None, + initial_B: np.ndarray | None = None, + callback: Callable[[int, Result, np.ndarray, float], None] | None = None, + additional_convergence: Callable[ + [Result, np.ndarray, np.ndarray, np.ndarray, np.ndarray], None + ] + | None = None, overwrite_convergence_criteria: bool = False, -) -> Tuple[np.ndarray, np.ndarray, np.ndarray, Result]: +) -> tuple[np.ndarray, np.ndarray, np.ndarray, Result]: """The main solving loop of the VMCON non-linear constrained optimiser. Parameters @@ -95,29 +99,44 @@ def solve( result : Result The result from running the solution vector through the problem. - """ + + """ # noqa: E501 if len(x.shape) != 1: - raise ValueError("Input vector `x` is not a 1D array") + error_msg = "Input vector `x` is not a 1D array" + raise ValueError(error_msg) if lbs is not None and (x < lbs).any(): - msg = "x is initially in an infeasible region because at least one x is lower than a lower bound" # noqa: E501 + msg = ( + "x is initially in an infeasible region because at least one x is lower" + "than a lower bound" + ) + error_msg = ( + f"{msg}. The out of bounds variables are at indices" + f"{', '.join(_find_out_of_bounds_vars(x, lbs))} (0-based indexing)" + ) logger.error( - f"{msg}. The out of bounds variables are at indices {', '.join(_find_out_of_bounds_vars(x, lbs))} (0-based indexing)" # noqa: E501 + error_msg, ) raise ValueError(msg) if ubs is not None and (x > ubs).any(): - msg = "x is initially in an infeasible region because at least one x is greater than an upper bound" # noqa: E501 - logger.error( - f"{msg}. The out of bounds variables are at indices {', '.join(_find_out_of_bounds_vars(ubs, x))} (0-based indexing)" # noqa: E501 + msg = ( + "x is initially in an infeasible region because at least one x is greater" + "than an upper bound" + ) + error_msg = ( + f"{msg}. The out of bounds variables are at indices" + f"{', '.join(_find_out_of_bounds_vars(ubs, x))} (0-based indexing)" ) + logger.error(error_msg) raise ValueError(msg) if overwrite_convergence_criteria and additional_convergence is None: - raise ValueError( + error_msg = ( "Cannot overwrite convergence criteria without " "providing an 'additional_convergence' callable." ) + raise ValueError(error_msg) # n is denoted in the VMCON paper # as the number of inputs the function @@ -150,11 +169,20 @@ def solve( # for our constraints try: delta, lamda_equality, lamda_inequality = solve_qsp( - problem, result, x, B, lbs, ubs, qsp_options or {} + problem, + result, + x, + B, + lbs, + ubs, + qsp_options or {}, ) except _QspSolveException as e: + error_msg = ( + "QSP failed to solve, indicating no feasible solution could be found." + ) raise QSPSolverException( - "QSP failed to solve, indicating no feasible solution could be found.", + error_msg, x=x, result=result, lamda_equality=lamda_equality, @@ -164,13 +192,20 @@ def solve( # Exit to optimisation loop if the convergence # criteria is met convergence_info = convergence_value( - result, delta, lamda_equality, lamda_inequality + result, + delta, + lamda_equality, + lamda_inequality, ) callback(i, result, x, convergence_info) if additional_convergence( - result, x, delta, lamda_equality, lamda_inequality + result, + x, + delta, + lamda_equality, + lamda_inequality, ) and (overwrite_convergence_criteria or convergence_info < epsilon): break @@ -207,8 +242,11 @@ def solve( x = xj else: + error_msg = ( + f"Could not converge on a feasible solution after {max_iter} iterations." + ) raise VMCONConvergenceException( - f"Could not converge on a feasible solution after {max_iter} iterations.", + error_msg, x=x, result=result, lamda_equality=lamda_equality, @@ -223,12 +261,13 @@ def solve_qsp( result: Result, x: np.ndarray, B: np.ndarray, - lbs: Optional[np.ndarray], - ubs: Optional[np.ndarray], - options: Dict[str, Any], -) -> Tuple[np.ndarray, ...]: - """Solves the quadratic programming problem detailed in equation 4 and 5 - of the VMCON paper. + lbs: np.ndarray | None, + ubs: np.ndarray | None, + options: dict[str, Any], +) -> tuple[np.ndarray, ...]: + """Solves the quadratic programming problem. + + This function solves equations 4 and 5 of the VMCON paper. The QSP is solved using cvxpy. cvxpy requires the problem be convex, which is ensured by equation 9 of the VMCON paper. @@ -262,12 +301,13 @@ def solve_qsp( * By default, OSQP (https://osqp.org/) is the `solver` used in the `solve` method however this can be changed by specifying a different `solver` in the `options` dictionary. + """ delta = cp.Variable(x.shape) problem_statement = cp.Minimize( result.f + (0.5 * cp.quad_form(delta, B, assume_PSD=True)) - + (delta.T @ result.df) + + (delta.T @ result.df), ) equality_index = 0 @@ -289,7 +329,8 @@ def solve_qsp( qsp.solve(**{"solver": cp.OSQP, **options}) if delta.value is None: - raise _QspSolveException(f"QSP failed to solve: {qsp.status}") + error_msg = f"QSP failed to solve: {qsp.status}" + raise _QspSolveException(error_msg) lamda_equality = np.array([]) lamda_inequality = np.array([]) @@ -314,6 +355,7 @@ def convergence_value( lamda_inequality: np.ndarray, ) -> float: """Test if the convergence criteria of VMCON have been met. + Equation 11 of the VMCON paper. Note this tests convergence at the point (j-1)th evaluation point. @@ -325,13 +367,14 @@ def convergence_value( delta_j : ndarray The search direction for the jth evaluation point. - lambda_equality : ndarray + lamda_equality : ndarray The Lagrange multipliers for equality constraints for the jth evaluation point. - lambda_inequality : ndarray + lamda_inequality : ndarray The Lagrange multipliers for inequality constraints for the jth evaluation point. + """ ind_eq = min(lamda_equality.shape[0], result.eq.shape[0]) ind_ieq = min(lamda_inequality.shape[0], result.ie.shape[0]) @@ -342,7 +385,7 @@ def convergence_value( return abs_df_dot_delta + abs_equality_err + abs_inequality_err -def _calculate_mu_i(mu_im1: Union[np.ndarray, None], lamda: np.ndarray) -> np.ndarray: +def _calculate_mu_i(mu_im1: np.ndarray | None, lamda: np.ndarray) -> np.ndarray: if mu_im1 is None: return np.abs(lamda) @@ -353,13 +396,13 @@ def _calculate_mu_i(mu_im1: Union[np.ndarray, None], lamda: np.ndarray) -> np.nd def perform_linesearch( problem: AbstractProblem, result: Result, - mu_equality: Optional[np.ndarray], - mu_inequality: Optional[np.ndarray], + mu_equality: np.ndarray | None, + mu_inequality: np.ndarray | None, lamda_equality: np.ndarray, lamda_inequality: np.ndarray, delta: np.ndarray, x_jm1: np.ndarray, -) -> Tuple[float, np.ndarray, np.ndarray, Result]: +) -> tuple[float, np.ndarray, np.ndarray, Result]: """Performs the line search on equation 6 (to minimise phi). Parameters @@ -375,6 +418,21 @@ def perform_linesearch( mu_inequality : ndarray The mu values for the inequality constraints. + + lamda_equality : ndarray + The Lagrange multipliers for equality constraints for the (j-1)th + evaluation point. + + lamda_inequality : ndarray + The Lagrange multipliers for inequality constraints for the (j-1)th + evaluation point. + + delta : ndarray + The search direction. + + x_jm1 : ndarray + The current input vector. + """ mu_equality = _calculate_mu_i(mu_equality, lamda_equality) mu_inequality = _calculate_mu_i(mu_inequality, lamda_inequality) @@ -396,8 +454,7 @@ def phi(result: Result) -> T: alpha = 1.0 for _ in range(10): - # exit if we satisfy the Armijo condition - # or the Kovari condition + # exit if we satisfy the Armijo condition or the Kovari condition new_result = problem(x_jm1 + alpha * delta) if alpha != 1.0 else result_at_1 phi_alpha = phi(new_result) if phi_alpha <= phi_0 + 0.1 * alpha * capital_delta or phi_alpha > phi_0: @@ -409,8 +466,9 @@ def phi(result: Result) -> T: ) else: + error_msg = "Line search did not converge on an approximate minima" raise LineSearchConvergenceException( - "Line search did not converge on an approximate minima", + error_msg, x=x_jm1, result=result, lamda_equality=lamda_equality, @@ -428,10 +486,10 @@ def _derivative_lagrangian( ind_eq = min(lamda_equality.shape[0], result.deq.shape[0]) ind_ieq = min(lamda_inequality.shape[0], result.die.shape[0]) c_equality_prime = (lamda_equality[:ind_eq, None] * result.deq[:ind_eq]).sum( - axis=None if ind_eq == 0 else 0 + axis=None if ind_eq == 0 else 0, ) c_inequality_prime = (lamda_inequality[:ind_ieq, None] * result.die[:ind_ieq]).sum( - axis=None if ind_ieq == 0 else 0 + axis=None if ind_ieq == 0 else 0, ) return result.df - c_equality_prime - c_inequality_prime @@ -457,6 +515,10 @@ def calculate_new_B( lamda_equality: np.ndarray, lamda_inequality: np.ndarray, ) -> np.ndarray: + """Updates the hessian approximation matrix. + + Uses Equation 8 of the Crane report. + """ # xi (the symbol name) would be a bit confusing in this context, # ksi is how its pronounced in modern greek # reshape ksi to be a matrix @@ -489,6 +551,6 @@ def calculate_new_B( return B -def _find_out_of_bounds_vars(higher: np.ndarray, lower: np.ndarray) -> List[str]: - """Return the indices of the out of bounds variables""" +def _find_out_of_bounds_vars(higher: np.ndarray, lower: np.ndarray) -> list[str]: + """Return the indices of the out of bounds variables.""" return np.nonzero((higher - lower) < 0)[0].astype(str).tolist() diff --git a/tests/test_vmcon_paper.py b/tests/test_vmcon_paper.py index ec4115b..41479a8 100644 --- a/tests/test_vmcon_paper.py +++ b/tests/test_vmcon_paper.py @@ -1,13 +1,19 @@ -from typing import NamedTuple +"""Tests the entire VMCON solver with examples from the Crane paper.""" + +from dataclasses import dataclass import numpy as np import pytest + from pyvmcon import solve from pyvmcon.exceptions import VMCONConvergenceException from pyvmcon.problem import Problem -class VMCONTestAsset(NamedTuple): +@dataclass +class VMCONTestAsset: + """Dataclass describing a problem input and expected result.""" + problem: Problem initial_x: np.ndarray expected_x: np.ndarray @@ -115,9 +121,10 @@ class VMCONTestAsset(NamedTuple): ], ) def test_vmcon_paper_feasible_examples(vmcon_example: VMCONTestAsset): - """Tests example runs of VMCON provided in the VMCON paper - produce similar results between their implementation, and this - implementation. + """Tests an example of VMCON from the Crane paper. + + Overall, we assert that this implementation produces similar results to + the Crane implementation. """ x, lamda_equality, lamda_inequality, _ = solve( vmcon_example.problem, @@ -154,8 +161,10 @@ def test_vmcon_paper_feasible_examples(vmcon_example: VMCONTestAsset): ], ) def test_vmcon_paper_infeasible_examples(vmcon_example: VMCONTestAsset): - """Tests runs of VMCON where the problem describes a minimisation - which is infeasible given the constraints. + """Tests runs of VMCON from the Crane paper. + + The problem describes a minimisation which is infeasible given the constraints + and tests an appropriate error is raised. Assertions on the returned `x` (the last tried input vector) and corresponding Lagrange multipliers have been removed as the QSP From 33a8d5a952afa0605ea4ea83051648c5dd378ac6 Mon Sep 17 00:00:00 2001 From: Timothy Nunn Date: Fri, 4 Apr 2025 10:56:18 +0000 Subject: [PATCH 2/4] No longer assume quadratic subproblem is PSD --- src/pyvmcon/vmcon.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pyvmcon/vmcon.py b/src/pyvmcon/vmcon.py index fa8af6d..08b3492 100644 --- a/src/pyvmcon/vmcon.py +++ b/src/pyvmcon/vmcon.py @@ -305,9 +305,7 @@ def solve_qsp( """ delta = cp.Variable(x.shape) problem_statement = cp.Minimize( - result.f - + (0.5 * cp.quad_form(delta, B, assume_PSD=True)) - + (delta.T @ result.df), + result.f + (0.5 * cp.quad_form(delta, B)) + (delta.T @ result.df), ) equality_index = 0 From 0998357e2bf7c04c47fa8101cf7451aced7900f5 Mon Sep 17 00:00:00 2001 From: Timothy Nunn Date: Fri, 4 Apr 2025 11:01:47 +0000 Subject: [PATCH 3/4] Update numpy required version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3bd3c65..ba16a27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ readme = "README.md" license = { file = "LICENSE" } authors = [{ name = "Timothy Nunn", email = "timothy.nunn@ukaea.uk" }] requires-python = ">=3.10" -dependencies = ["numpy>=1.19", "cvxpy>=1.5.2"] +dependencies = ["numpy>=1.24", "cvxpy>=1.5.2"] classifiers = [ "Intended Audience :: Science/Research", "License :: OSI Approved :: MIT License", From aeb94c094cdf4d27fb9773ac8079b9d836037bdd Mon Sep 17 00:00:00 2001 From: Timothy Nunn Date: Fri, 4 Apr 2025 11:03:27 +0000 Subject: [PATCH 4/4] Remove old python version tests and add 3.13 --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 76cae78..2a46aaa 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12", "3.13"] runs-on: ${{ matrix.os }} steps: - uses: actions/setup-python@v4