diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 714be32..0000000 --- a/.flake8 +++ /dev/null @@ -1,10 +0,0 @@ -[flake8] -# McCabe complexity threshold -max-complexity = 25 - -# exclude = */__init__.py, src/matrix_manager/nearest_correlation.py - -# Ensure compatibility with black -# https://black.readthedocs.io/en/stable/compatible_configs.html#flake8 -max-line-length = 88 -extend-ignore = E203, W503 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f67b884..ab7e910 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: # Ruff replaces Black, isort, flake8, autoflake, pydocstyle - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.0 + rev: v0.15.15 hooks: - id: ruff args: [--fix] @@ -26,13 +26,13 @@ repos: # Check python types with MyPy - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.19.1 + rev: v2.1.0 hooks: - id: mypy # Strip all output from jupyter notebook cells - repo: https://github.com/kynan/nbstripout - rev: 0.9.0 + rev: 0.9.1 hooks: - id: nbstripout @@ -40,20 +40,20 @@ repos: - repo: https://github.com/nbQA-dev/nbQA rev: 1.9.1 hooks: - - id: nbqa-isort - args: ["--float-to-top"] - - id: nbqa-black + - id: nbqa-ruff + args: ["--fix"] + - id: nbqa-ruff-format # Find dead code - repo: https://github.com/jendrikseipp/vulture - rev: v2.14 + rev: v2.16 hooks: - id: vulture args: ["--min-confidence", "70"] # Security linter for Python code - repo: https://github.com/PyCQA/bandit - rev: 1.9.3 + rev: 1.9.4 hooks: - id: bandit args: ["--skip=B101,B301,B403,B605,B607"] diff --git a/.python-version b/.python-version index e4fba21..6324d40 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.12 +3.14 diff --git a/CLAUDE.md b/CLAUDE.md index b53aa9b..7091d59 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,7 +9,7 @@ functions. ## Tech stack -- **Language:** Python 3.12 (strict: `>=3.12,<3.13`) +- **Language:** Python 3.14 (strict: `>=3.14`) - **Package manager:** `uv` (lock file: `uv.lock`) - **Build backend:** hatchling - **Solvers:** Gurobi (primary), Pyomo/GLPK, HiGHS, PuLP, AMPL, CPLEX @@ -59,11 +59,9 @@ bandit -r src/ --skip=B101,B301,B403,B605,B607 # Security linting ## Code conventions - **Formatting:** 88-character line length (ruff/black compatible) -- **Type hints:** Required on all function signatures; use `typing` module types and - `numpy.typing.NDArray` +- **Type hints:** Required on all function signatures; use built-in generics (`list[X]`, `dict[K,V]`, `tuple[...]`) for Python 3.9+ style; `typing.Any`/`typing.Callable` still from `typing`; `numpy.typing.NDArray` - **Docstrings:** NumPy-style with Parameters/Returns sections -- **Imports:** stdlib, then third-party, then local; sorted by isort (black profile); - no wildcard imports +- **Imports:** stdlib, then third-party, then local; sorted by ruff; no wildcard imports - **Naming:** `snake_case` for functions/variables, `UPPER_CASE` for constants - **Security:** Uses `secrets.SystemRandom()` for randomness; DB credentials via environment variables diff --git a/mypy.ini b/mypy.ini index bfd8b56..f2f8d85 100644 --- a/mypy.ini +++ b/mypy.ini @@ -3,7 +3,7 @@ # Global options explicit_package_bases = True -python_version = 3.12 +python_version = 3.14 warn_unused_configs = True ignore_missing_imports = True ; disallow_untyped_calls = True diff --git a/pyproject.toml b/pyproject.toml index 1767876..5b0e183 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,75 +3,69 @@ name = "crash_code" version = "1.0.0" description = "Code for crashing optimization." authors = [{ name = "Ricardo A. Collado" }] -requires-python = ">=3.12,<3.13" +requires-python = ">=3.14" readme = "README.md" dependencies = [ - "pandas>=2.2.3,<3", - "numpy>=2.2.3,<3", - "scipy>=1.15.2,<2", - "networkx>=3.4.2,<4", - "matplotlib>=3.10.1,<4", - "pydot>=3.0.4,<4", - "sqlalchemy>=2.0.39,<3", - "pyfiglet>=1.0.2,<2", - "bootstrapped>=0.0.2,<0.0.3", - "jellyfish>=1.1.3,<2", - "statsmodels>=0.14.4,<0.15", - "ace-tools-open>=0.1.0,<0.2", + "pandas>=3.0.3", + "numpy>=2.4.6", + "scipy>=1.17.1", + "networkx>=3.6.1", + "matplotlib>=3.10.9", + "pydot>=4.0.1", + "sqlalchemy>=2.0.50", + "pyfiglet>=1.0.4", + "bootstrapped>=0.0.2", + "jellyfish>=1.2.1", + "statsmodels>=0.14.6", + "ace-tools-open>=0.1.0", # Git submodules as local dependencies: # "helper @ file:///${PROJECT_ROOT}/libs/helper", ] [dependency-groups] open_opt = [ - "pyomo>=6.9.1,<7", - "glpk>=0.4.8,<0.5", - "highspy>=1.9.0,<2", - "pulp>=3.0.2,<4", - "amplpy>=0.14.0,<0.15", + "pyomo>=6.10.0", + "glpk>=0.4.8", + "highspy>=1.14.0", + "pulp>=3.3.2", + "amplpy>=0.16.1", # Installed manually from source: # uv pip install /path/to/pygmo.whl - # "pygmo>=2.19.7,<3", + # "pygmo>=2.19.7", ] closed_opt = [ - "gurobipy>=12.0.1,<13", - "cplex>=22.1.2.0,<23", + "gurobipy>=13.0.2", + "cplex>=22.2.0.0", ] dev = [ - "jupyter>=1.0.0,<2", - "notebook>=6.5.4,<7", - "mypy>=1.3.0,<2", - "types-python-dateutil>=2.9.0.20241206,<3", - "scipy-stubs>=1.15.2.1,<2", - "flake8>=6.0.0,<7", - "pre-commit>=3.3.1,<4", - "vulture>=2.13,<3", - "bandit>=1.7.10,<2", - "pydeps>=2.0.1,<3", - "isort>=5.12.0,<6", - "pydocstyle>=6.3.0,<7", - "black<23.3.0", - "nbqa>=1.8.5,<2", - "jupyter-contrib-nbextensions>=0.7.0,<0.8", - "nbstripout>=0.7.1,<0.8", + "jupyter>=1.1.1", + "notebook>=7.5.6", + "mypy>=2.1.0", + "types-python-dateutil>=2.9.0.20260518", + "scipy-stubs>=1.17.1.5", + "ruff>=0.15.15", + "pre-commit>=4.6.0", + "vulture>=2.16", + "bandit>=1.9.4", + "pydeps>=3.0.6", + "nbqa>=1.9.1", + "nbstripout>=0.9.1", ] package = [ - "wheel>=0.43.0,<0.44", - "pip~=24.0", + "wheel>=0.47.0", + "hatchling>=1.29.0", ] doc = [ - "mkdocs>=1.6.0,<2", - "mkdocstrings[python]>=0.25.1,<0.26", - "mkdocs-material>=9.5.23,<10", + "mkdocs>=1.6.1", + "mkdocstrings[python]>=1.0.4", + "mkdocs-material>=9.7.6", "mkdocs-material-extensions>=1.3.1", "mkdocs-autorefs>=1.4.4", "python-markdown-math>=0.9", ] [tool.uv] -default-groups = [ - "dev", -] +default-groups = ["dev"] [tool.hatch.build.targets.sdist] include = [ diff --git a/src/db_manager/driver.py b/src/db_manager/driver.py index cb6cba7..b12142c 100644 --- a/src/db_manager/driver.py +++ b/src/db_manager/driver.py @@ -8,11 +8,11 @@ import json import os import pickle -from typing import Any, Callable, Dict, List, Tuple, Type +from typing import Any, Callable import pandas as pd from networkx.readwrite import json_graph -from sqlalchemy import Column, ForeignKey, Sequence, create_engine +from sqlalchemy import URL, Column, ForeignKey, Sequence, create_engine from sqlalchemy.dialects.postgresql import ( ARRAY, BIGINT, @@ -23,12 +23,10 @@ JSONB, TEXT, ) -from sqlalchemy.engine.url import URL -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import relationship, sessionmaker +from sqlalchemy.orm import declarative_base, relationship, sessionmaker -def initialize_db() -> Tuple[ # pylint: disable=too-many-statements +def initialize_db() -> tuple[ # pylint: disable=too-many-statements Callable, Callable, Callable, @@ -54,8 +52,8 @@ def initialize_db() -> Tuple[ # pylint: disable=too-many-statements - close_db: Function to close the database session. """ # Database connection from environment variables - connect_url = URL( - "postgres", + connect_url = URL.create( + "postgresql", host=os.environ["crash_db_host"], port=os.environ["crash_db_port"], username=os.environ["crash_db_user"], @@ -64,7 +62,7 @@ def initialize_db() -> Tuple[ # pylint: disable=too-many-statements ) # db = create_engine(connect_url) - Base: Type = declarative_base() # pylint: disable=invalid-name + Base: type = declarative_base() # pylint: disable=invalid-name class Meta(Base): # pylint: disable=unused-variable """Metadata grouping for crash experiments. @@ -189,8 +187,8 @@ class Output(Base): experiment_id = 0 def push_experiment_db( - seeds: List[int], - attributes: Dict[str, Any], + seeds: list[int], + attributes: dict[str, Any], run_time: float = 0.0, ) -> int: """Serialize and insert a new experiment row. @@ -265,8 +263,8 @@ def push_experiment_db( def push_iteration_db( cov_matrix: pd.DataFrame, - partition_list: List[Dict[str, Any]], - attributes: Dict[str, Any], + partition_list: list[dict[str, Any]], + attributes: dict[str, Any], iteration_number: int, elapsed_iter_time: float, ) -> None: @@ -329,12 +327,12 @@ def update_exp_time(new_time: float) -> None: nonlocal experiment_id nonlocal session - experiment = session.query(Experiment).get(experiment_id) + experiment = session.get(Experiment, experiment_id) experiment.exp_time = new_time session.commit() return - def push_solution_db(solution: Dict[str, Any]) -> None: + def push_solution_db(solution: dict[str, Any]) -> None: """Insert a solution row for the current experiment. Parameters diff --git a/src/gen_manager/crash.py b/src/gen_manager/crash.py index af4e0e7..70f09bd 100644 --- a/src/gen_manager/crash.py +++ b/src/gen_manager/crash.py @@ -5,16 +5,15 @@ """ from collections import deque -from typing import List -from numpy.random import uniform +import numpy as np def generate_crash_times( no_of_nodes: int, low_limit: float, high_limit: float, -) -> List[float]: +) -> list[float]: """Generate activity crash times in percentage. Parameters @@ -28,10 +27,10 @@ def generate_crash_times( Returns ------- - List[float] + list[float] List of generated crash times. """ - crash_time_deque = deque(uniform(low_limit, high_limit, no_of_nodes)) + crash_time_deque = deque(np.random.uniform(low_limit, high_limit, no_of_nodes)) crash_time_deque.appendleft(0.0) crash_time_deque.append(0.0) @@ -44,7 +43,7 @@ def generate_crash_cost( no_of_nodes: int, low_cost: float, high_cost: float, -) -> List[float]: +) -> list[float]: """Generate activity crash costs. Parameters @@ -58,10 +57,10 @@ def generate_crash_cost( Returns ------- - List[float] + list[float] List of generated crash costs. """ - crash_cost_deque = deque(uniform(low_cost, high_cost, no_of_nodes)) + crash_cost_deque = deque(np.random.uniform(low_cost, high_cost, no_of_nodes)) crash_cost_deque.appendleft(0.0) crash_cost_deque.append(0.0) diff --git a/src/gen_manager/distribution.py b/src/gen_manager/distribution.py index a114def..fd0e9d9 100644 --- a/src/gen_manager/distribution.py +++ b/src/gen_manager/distribution.py @@ -6,13 +6,13 @@ """ import secrets -from typing import Any, Dict +from typing import Any import numpy as np from scipy.stats import beta -def generate_pert_distributions(geom_prob: Dict[Any, float]) -> Dict[str, Any]: +def generate_pert_distributions(geom_prob: dict[Any, float]) -> dict[str, Any]: """Generate beta distributions for activity times. Parameters @@ -24,7 +24,7 @@ def generate_pert_distributions(geom_prob: Dict[Any, float]) -> Dict[str, Any]: Returns ------- - dict of str to Any + dict[str, Any] Dictionary with: - ``"distributions"``: mapping from node id to frozen @@ -111,7 +111,7 @@ def generate_pert_distributions(geom_prob: Dict[Any, float]) -> Dict[str, Any]: } -def generate_geometric(no_of_nodes: int) -> Dict[int, float]: +def generate_geometric(no_of_nodes: int) -> dict[int, float]: """Generate probabilities for geometric distributions. Used to select the projects betas. diff --git a/src/gen_manager/network.py b/src/gen_manager/network.py index 49610dc..e5c2027 100644 --- a/src/gen_manager/network.py +++ b/src/gen_manager/network.py @@ -7,7 +7,7 @@ import io import secrets from itertools import product -from typing import Dict, List, Tuple + import matplotlib.pyplot as plt import networkx as nx @@ -20,7 +20,7 @@ def network_skeleton( num_nodes: int, num_layers: int, -) -> List[Tuple[int, int]]: +) -> list[tuple[int, int]]: """Generate edge list of network skeleton. Parameters @@ -32,7 +32,7 @@ def network_skeleton( Returns ------- - edges : List[Tuple[int, int]] + edges : list[tuple[int, int]] List of pairs of nodes comprising the skeleton edges. """ # Generate initial layer partition @@ -92,7 +92,7 @@ def generate_network( num_nodes: int, num_layers: int, density: float, -) -> Tuple[nx.DiGraph, bytes, Dict[int, Tuple[float, float]]]: +) -> tuple[nx.DiGraph, bytes, dict[int, tuple[float, float]]]: """Generate a connected network graph. Parameters diff --git a/src/gen_manager/penalty.py b/src/gen_manager/penalty.py index 80c9931..5d264cb 100644 --- a/src/gen_manager/penalty.py +++ b/src/gen_manager/penalty.py @@ -4,7 +4,7 @@ and computes penalty bounds by solving uncrashed baseline scenarios. """ -from typing import Any, Dict, List +from typing import Any from numpy import power @@ -12,10 +12,10 @@ def generate_penalty_vals_linear( - t: List[float], + t: list[float], m: float, b1: float, -) -> List[float]: +) -> list[float]: """Generate penalty linear values. Parameters @@ -32,16 +32,16 @@ def generate_penalty_vals_linear( vals : list of float Linear values of the form m*t[i] + b1 (except val[0] which is always zero). """ - vals: List[float] = [0] + vals: list[float] = [0] vals.extend([m * t[i] + b1 for i in range(1, len(t))]) return vals def generate_penalty_vals_exponential( - t: List[float], + t: list[float], m: float, b1: float, -) -> List[float]: +) -> list[float]: """Generate penalty exponential values. Parameters @@ -59,15 +59,15 @@ def generate_penalty_vals_exponential( Exponential values of the form m * b1^(t[i]) (except val[0] which is always zero). """ - vals: List[float] = [0] + vals: list[float] = [0] vals.extend([m * power(b1, t[i]) for i in range(1, len(t))]) return vals def generate_penalty_bounds( network: Any, - pert_dist: Dict[str, List[float]], -) -> List[float]: + pert_dist: dict[str, list[float]], +) -> list[float]: """Obtain t_init and t_final for calculation of objective penalty function. This is done by solving the main problem without crashing with most likely @@ -79,12 +79,12 @@ def generate_penalty_bounds( ---------- network : Any The network representing the project. - pert_dist : dict of str to list of float + pert_dist : dict[str, list[float]] Dictionary containing 'most_likely' and 'pessimistic' scenarios. Returns ------- - list of float + list[float] A list containing t_init and t_final values. """ t_init, _ = uncrashed_project_time(network, pert_dist["most_likely"]) diff --git a/src/gen_manager/scenario.py b/src/gen_manager/scenario.py index bab7592..e53bec9 100644 --- a/src/gen_manager/scenario.py +++ b/src/gen_manager/scenario.py @@ -4,10 +4,9 @@ specified correlation structure and marginal PERT distributions. """ -from typing import Any, Dict +from typing import Any import numpy as np -from numpy.random import multivariate_normal from scipy.stats import norm @@ -15,7 +14,7 @@ def dynamic_scenarios( num_activities: int, num_scenarios: int, correlation_matrix: np.ndarray, - distribution_dict: Dict[int, Any], + distribution_dict: dict[int, Any], ) -> np.ndarray: """Generate samples based on activities and correlation matrix. @@ -38,12 +37,12 @@ def dynamic_scenarios( """ # Generate samples via Gaussian Copula mean = np.zeros(num_activities) + rng = np.random.default_rng() samples = norm.cdf( - multivariate_normal( # pylint: disable=unexpected-keyword-arg + rng.multivariate_normal( mean=mean, cov=correlation_matrix, size=num_scenarios, - check_valid="raise", ) ) k_list = [] diff --git a/src/matrix_manager/nearest_correlation.py b/src/matrix_manager/nearest_correlation.py index 55d939c..396c0ab 100644 --- a/src/matrix_manager/nearest_correlation.py +++ b/src/matrix_manager/nearest_correlation.py @@ -14,7 +14,6 @@ from numpy.linalg import norm from typing import Any - class ExceededMaxIterationsError(Exception): """Error class for exceeding iterations.""" @@ -63,7 +62,6 @@ def __str__(self) -> str: """ return repr(self.msg) - def nearcorr( symmetric_input_matrix: np.ndarray | ExceededMaxIterationsError, tol: list[float] | np.ndarray | None = None, @@ -175,7 +173,6 @@ def nearcorr( return X - def proj_spd(A: np.ndarray) -> np.ndarray: """Project a symmetric matrix onto the positive semidefinite cone. diff --git a/src/matrix_manager/utilities.py b/src/matrix_manager/utilities.py index 6ee33dc..839264e 100644 --- a/src/matrix_manager/utilities.py +++ b/src/matrix_manager/utilities.py @@ -4,12 +4,9 @@ converting between covariance and correlation representations. """ -from typing import Tuple - import numpy as np from numpy.typing import NDArray - def is_pos_def(x: NDArray[np.float64]) -> bool: """Evaluate positive-definiteness. @@ -25,7 +22,6 @@ def is_pos_def(x: NDArray[np.float64]) -> bool: """ return np.all(np.linalg.eigvals(x) > 0) - def is_pos_semi_def(x: NDArray[np.float64]) -> bool: """Evaluate positive-semi-definiteness. @@ -41,10 +37,9 @@ def is_pos_semi_def(x: NDArray[np.float64]) -> bool: """ return np.all(np.linalg.eigvals(x) >= 0) - def correlation_from_covariance( covariance: NDArray[np.float64], -) -> Tuple[NDArray[np.float64], NDArray[np.float64]]: +) -> tuple[NDArray[np.float64], NDArray[np.float64]]: """Get correlation from covariance. Parameters @@ -65,7 +60,6 @@ def correlation_from_covariance( correlation[covariance == 0] = 0 return correlation, v - def covariance_from_correlation( correlation: NDArray[np.float64], v: NDArray[np.float64] ) -> NDArray[np.float64]: diff --git a/src/opt_manager/generator_subproblem.py b/src/opt_manager/generator_subproblem.py index ea74d4a..77a0ba6 100644 --- a/src/opt_manager/generator_subproblem.py +++ b/src/opt_manager/generator_subproblem.py @@ -4,7 +4,7 @@ incorporating piecewise penalty approximations and network-flow constraints. """ -from typing import Any, Dict, List +from typing import Any import gurobipy as gp import networkx as nx @@ -14,21 +14,20 @@ generate_penalty_vals_linear, ) - def optimize_subproblem( network: nx.DiGraph, - crash_time: List[float], - crash_cost: List[float], - subproblem: Dict[str, Any], + crash_time: list[float], + crash_cost: list[float], + subproblem: dict[str, Any], t_init: float, t_final: float, - pessimistic: List[float], + pessimistic: list[float], penalty_type: str, m: float, b1: float, penalty_steps: float, - scenario: List[float], -) -> Dict[str, Any]: + scenario: list[float], +) -> dict[str, Any]: """Define and solve an optimization problem where some variables are fixed. Parameters diff --git a/src/opt_manager/knowledge_gradient.py b/src/opt_manager/knowledge_gradient.py index 2edc81b..89cbbb3 100644 --- a/src/opt_manager/knowledge_gradient.py +++ b/src/opt_manager/knowledge_gradient.py @@ -6,12 +6,11 @@ """ from functools import partial -from typing import Any, List, Tuple +from typing import Any import numpy as np from scipy.stats import norm - def f_func(z: float) -> float: """Provide required f function for KG evaluation. @@ -27,7 +26,6 @@ def f_func(z: float) -> float: """ return norm.pdf(z) + z * norm.cdf(z) - def sigma( S: np.ndarray, x: int, @@ -56,14 +54,13 @@ def sigma( return np.dot(S, e_x) / np.sqrt(lambda_[x - 1] + S[x - 1][x - 1]) - def update_mu_s( mu_n: np.ndarray, S_n: np.ndarray, lambda_: np.ndarray, x: int, y_n1: float, -) -> Tuple[np.ndarray, np.ndarray]: +) -> tuple[np.ndarray, np.ndarray]: """Get updated mu and S after taking a sample y at subproblem with name x. Parameters @@ -102,11 +99,10 @@ def update_mu_s( return mu_n1, S_n - def algorithm_1( - a: List[float], - b: List[float], -) -> Tuple[List[float], List[int]]: + a: list[float], + b: list[float], +) -> tuple[list[float], list[int]]: """Algorithm 1 from paper. Parameters @@ -152,12 +148,11 @@ def algorithm_1( return c, A - def kg_alg( mu: np.ndarray, S: np.ndarray, lambda_: np.ndarray, -) -> Tuple[int, float]: +) -> tuple[int, float]: """KG algorithm: single-threaded. Parameters @@ -216,13 +211,12 @@ def kg_alg( return xx - 1, vv - def kg_iteration( mu: np.ndarray, S: np.ndarray, lambda_: np.ndarray, x: int, -) -> Tuple[int, float]: +) -> tuple[int, float]: """Single KG iteration. Parameters @@ -276,13 +270,12 @@ def kg_iteration( return x - 1, v - def kg_multi( mu: np.ndarray, S: np.ndarray, lambda_: np.ndarray, pool: Any, -) -> Tuple[int, float]: +) -> tuple[int, float]: """KG algorithm: parallelized for large instances. Parameters diff --git a/src/opt_manager/optimize.py b/src/opt_manager/optimize.py index 123f693..0a513b8 100644 --- a/src/opt_manager/optimize.py +++ b/src/opt_manager/optimize.py @@ -5,17 +5,15 @@ """ import time -from typing import List, Tuple from db_manager.driver import initialize_db from opt_manager.stochastic import branch_bound_algorithm, initialize_attributes - def optimize( problem: dict, method: dict, - seeds: List[int], -) -> Tuple[int, float, dict]: + seeds: list[int], +) -> tuple[int, float, dict]: """Commit problem to db and the branch & bound method. Parameters diff --git a/src/opt_manager/stochastic.py b/src/opt_manager/stochastic.py index 1eccaf5..1e72bd2 100644 --- a/src/opt_manager/stochastic.py +++ b/src/opt_manager/stochastic.py @@ -8,13 +8,16 @@ import math import time from functools import partial -from typing import Any, Callable, Dict, List, Tuple +from typing import Any, Callable import bootstrapped.bootstrap as bs import bootstrapped.stats_functions as bs_stats import numpy as np import pandas as pd -import pygmo as pg +try: + import pygmo as pg +except ImportError: + pg = None from jellyfish import levenshtein_distance as l_dist from statsmodels.stats.correlation_tools import cov_nearest @@ -40,21 +43,21 @@ def increment_lc() -> int: def initialize_attributes( - problem: Dict[str, Any], - method: Dict[str, Any], -) -> Dict[str, Any]: + problem: dict[str, Any], + method: dict[str, Any], +) -> dict[str, Any]: """Initialize common attributes and parameters needed for SB&B algorithm. Parameters ---------- - problem : Dict[str, Any] + problem : dict[str, Any] The problem definition. - method : Dict[str, Any] + method : dict[str, Any] The method parameters. Returns ------- - attributes : Dict[str, Any] + attributes : dict[str, Any] Dictionary with keys: pool, project_network, scenarios, crashtime, crashcost, t_init, t_final, outlocation, b, b2, alpha, experiment_id, conn, method, @@ -127,9 +130,9 @@ def initialize_attributes( def branch_bound_algorithm( - attributes: Dict[str, Any], + attributes: dict[str, Any], push_iteration: Callable, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Stochastic Branch & Bound implementation. Parameters @@ -160,7 +163,7 @@ def branch_bound_algorithm( total_scenarios -= 4 * scen_est_num # Step 1: Initialization - subproblem: Dict[str, Any] = {} + subproblem: dict[str, Any] = {} variable_list = [] for node in attributes["nodes"]: @@ -189,12 +192,9 @@ def branch_bound_algorithm( subproblem["KG_sample_sol"] = [] subproblem["name"] = increment_lc() - # Update cov_df matrix + # Update cov_df matrix — initialize as 1×1 for the root subproblem name = subproblem["name"] - cov_df[name] = "" - cov_df = cov_df.append([""], ignore_index=True) - cov_df.drop([0], axis=1, inplace=True) - cov_df.index = np.arange(1, len(cov_df) + 1) + cov_df = pd.DataFrame(index=[name], columns=[name]) # Initial KG beliefs (KG_mu, KG_lambda): # We start from single problem so we basically do not have beliefs. Later we would @@ -266,8 +266,8 @@ def branch_bound_algorithm( def partition_record_set( - attributes: Dict[str, Any], - partition_list: List[Dict[str, Any]], + attributes: dict[str, Any], + partition_list: list[dict[str, Any]], scenario_start_index: int, scenario_end_index: int, ) -> bool: @@ -375,10 +375,8 @@ def partition_record_set( cov_df[sub2_name] = "" # Add two empty rows for new subproblems - s1 = pd.Series(name=sub1_name) - s2 = pd.Series(name=sub2_name) - cov_df = cov_df.append(s1) - cov_df = cov_df.append(s2) + new_rows = pd.DataFrame(index=[sub1_name, sub2_name], columns=cov_df.columns) + cov_df = pd.concat([cov_df, new_rows]) # Update with exponential kernel values for the two new rows and columns # and find nearest covariance matrix @@ -417,8 +415,8 @@ def partition_record_set( def work_on_subproblems( - attributes: Dict[str, Any], - partition_list: List[Dict[str, Any]], + attributes: dict[str, Any], + partition_list: list[dict[str, Any]], scenario_start_index: int, scenario_end_index: int, ) -> None: @@ -498,9 +496,9 @@ def work_on_subproblems( def estimate_bounds( - attributes: Dict[str, Any], - subproblem: Dict[str, Any], - scenarios: List[Any], + attributes: dict[str, Any], + subproblem: dict[str, Any], + scenarios: list[Any], ) -> float: """Call solver on a given subproblem for all scenarios. @@ -601,10 +599,10 @@ def estimate_bounds( def multisolve_scenarios( - attributes: Dict[str, Any], - subproblem: Dict[str, Any], - scenarios: List[Any], -) -> List[Any]: + attributes: dict[str, Any], + subproblem: dict[str, Any], + scenarios: list[Any], +) -> list[Any]: """Solves optimization problem for every scenario applied to the given subproblem. Parameters @@ -659,9 +657,9 @@ def multisolve_scenarios( def bootstrap_mean_std_estimate( - subproblem: Dict[str, Any], - attributes: Dict[str, Any], -) -> Tuple[float, float]: + subproblem: dict[str, Any], + attributes: dict[str, Any], +) -> tuple[float, float]: """Evaluate a variance-reduction bootstrap method. Parameters @@ -712,10 +710,10 @@ def bootstrap_mean_std_estimate( def bootstrap_kg( - subproblem: Dict[str, Any], - attributes: Dict[str, Any], - sample_solutions: List[Any], -) -> Tuple[float, float]: + subproblem: dict[str, Any], + attributes: dict[str, Any], + sample_solutions: list[Any], +) -> tuple[float, float]: """Evaluate bootstrap for KG samples. Parameters @@ -769,10 +767,10 @@ def bootstrap_kg( def assign_scenarios( - partition_list: List[Dict[str, Any]], - total_scenarios: List[Any], + partition_list: list[dict[str, Any]], + total_scenarios: list[Any], method: str, -) -> Dict[int, List[Any]]: +) -> dict[int, list[Any]]: """Compute and assign scenarios to nodes of partition_list. This is done on Random, Random_1, and Distance methods. @@ -824,10 +822,10 @@ def assign_scenarios( def assign_scenarios_KG( - partition_list: List[Dict[str, Any]], - total_scenarios: List[Any], - attributes: Dict[str, Any], -) -> Dict[int, List[Any]]: + partition_list: list[dict[str, Any]], + total_scenarios: list[Any], + attributes: dict[str, Any], +) -> dict[int, list[Any]]: """Compute and assign scenarios to nodes of partition_list on KG method Parameter. Parameters @@ -881,11 +879,11 @@ def assign_scenarios_KG( def assign_scenarios_pareto( - partition_list: List[Dict[str, Any]], - total_scenarios: List[Any], + partition_list: list[dict[str, Any]], + total_scenarios: list[Any], beta: float, method: str, -) -> Dict[int, List[Any]]: +) -> dict[int, list[Any]]: """Compute and assign scenarios to nodes of partition_list. This is done on Pareto Inverse and Pareto_Boltzman methods. @@ -935,8 +933,8 @@ def assign_scenarios_pareto( def rank_by_distance( - partition_list: List[Dict[str, Any]], -) -> List[float]: + partition_list: list[dict[str, Any]], +) -> list[float]: """Ranks nodes in partition list by distance of its (Z_E, Z_std) point. Parameters @@ -973,10 +971,10 @@ def rank_by_distance( def pareto_fronts_probabilities( - partition_list: List[Dict[str, Any]], + partition_list: list[dict[str, Any]], beta: float, probability_method: str, -) -> Tuple[int, List, List[float]]: +) -> tuple[int, List, list[float]]: """Get non-dominated fronts and its probabilities. Parameters @@ -1004,6 +1002,10 @@ def pareto_fronts_probabilities( ] # Obtain the non-dominated sorting of points + if pg is None: + raise ImportError( + "pygmo is required for Pareto branching. Install it with: uv add pygmo" + ) ndf, dl, dc, ndr = pg.fast_non_dominated_sorting(points=points) num_fronts = len(ndf) @@ -1043,10 +1045,10 @@ def pareto_fronts_probabilities( def update_row_col( - subproblem1: Dict[str, Any], - subproblem2: Dict[str, Any], - partition_list: List[Dict[str, Any]], - attributes: Dict[str, Any], + subproblem1: dict[str, Any], + subproblem2: dict[str, Any], + partition_list: list[dict[str, Any]], + attributes: dict[str, Any], ) -> None: """Update the last two rows and columns of cov_df. diff --git a/src/opt_manager/uncrashed_bounds.py b/src/opt_manager/uncrashed_bounds.py index 66ffa50..0a51d2e 100644 --- a/src/opt_manager/uncrashed_bounds.py +++ b/src/opt_manager/uncrashed_bounds.py @@ -4,16 +4,13 @@ baseline project duration bounds for penalty function calibration. """ -from typing import List, Tuple - import gurobipy as gp import networkx as nx - def uncrashed_project_time( network: nx.DiGraph, - scenario: List[float], -) -> Tuple[float, List[int]]: + scenario: list[float], +) -> tuple[float, list[int]]: """Solves the uncrashed unpenalized scheduling problem on a single scenario. Parameters diff --git a/src/opt_manager/uncrashed_bounds_pyomo.py b/src/opt_manager/uncrashed_bounds_pyomo.py index dd68cbd..33b24f1 100644 --- a/src/opt_manager/uncrashed_bounds_pyomo.py +++ b/src/opt_manager/uncrashed_bounds_pyomo.py @@ -4,7 +4,7 @@ GLPK open-source solver backend. """ -from typing import Any, List, Tuple +from typing import Any import networkx as nx import pyomo.environ as pyo @@ -19,11 +19,10 @@ # minimize, # ) - def uncrashed_project_time( network: nx.DiGraph, - scenario: List[float], -) -> Tuple[float, List[int]]: + scenario: list[float], +) -> tuple[float, list[int]]: """Solves the uncrashed unpenalized scheduling problem on a single scenario. Parameters diff --git a/src/run_manager/single_run.py b/src/run_manager/single_run.py index ae19630..ec8fdb4 100644 --- a/src/run_manager/single_run.py +++ b/src/run_manager/single_run.py @@ -9,7 +9,7 @@ import os import platform import random -from typing import Any, Dict +from typing import Any import numpy as np from pyfiglet import Figlet @@ -62,7 +62,7 @@ problem["crash_cost"] = generate_crash_cost(NO_OF_NODES, LOW_COST, HIGH_COST) # Generate penalty function - penalty: Dict[str, int | float | str | Any] = {} + penalty: dict[str, int | float | str | Any] = {} penalty["type"] = "linear" # or "exponential" penalty["steps"] = 20.0 penalty["m"] = 15.0 @@ -75,7 +75,7 @@ # ---------------------------------------------------------- # Set method parameters # ---------------------------------------------------------- - method: Dict[str, int | float | bool | str | mp.pool.Pool] = {} + method: dict[str, int | float | bool | str | mp.pool.Pool] = {} method["pool"] = pool method["type"] = "KG"