Skip to content

Feat/update min python version #7

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 25 additions & 18 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
name: CI
name: CI workflow

on:
push:
branches: [ "main" ]
branches: ["main"]
pull_request:
branches: [ "main" ]
branches: ["main"]

jobs:
build:
Expand All @@ -13,27 +13,34 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11"]
python-version: ["3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v3

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}

- name: Install poetry
run: python -m pip install poetry

- name: Install dependencies
run: poetry install

- name: Lint and format with ruff
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
env:
PYTHONPATH: ${{ github.workspace }}/src
run: |
pytest
python -m ruff check --fix
python -m ruff format

- name: Auto-commit ruff changes
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: "Lint and format with ruff"

- name: Perform type checking with mypy
run: mypy src

- name: Run tests
run: poetry run pytest --cov=src
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Generate API documentation
on:
push:
branches: [ "main" ]
branches: ["main"]

jobs:
deploy:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: '3.11'
python-version: "3.11"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand Down
8 changes: 5 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ repos:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/psf/black
rev: 22.10.0
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.6
hooks:
- id: black
- id: ruff
- id: ruff-format
args: [--line-length=120]
- repo: https://github.com/pycqa/pydocstyle
rev: 6.3.0
hooks:
Expand Down
59 changes: 43 additions & 16 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,27 +1,54 @@
[build-system]
requires = ["setuptools >= 61.0"]
build-backend = "setuptools.build_meta"

[project]
[tool.poetry]
name = "halfspace-optimizer"
version = "0.1.1"
version = "0.1.2"
description = "Cutting-plane solver for mixed-integer convex optimization problems"
license ="MIT"
authors = [
{ name="Joshua Ivanhoe", email="joshua.k.ivanhoe@gmail.com" },
"Joshua Ivanhoe <joshua.k.ivanhoe@gmail.com>"
]
description = "Cutting-plane solver for mixed-integer convex optimization problems"
readme = "README.md"
license = {file = "LICENSE"}
requires-python = ">=3.9,<3.12"
repository = "https://github.com/joshivanhoe/halfspace"
documentation = "https://joshivanhoe.github.io/halfspace/"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
dynamic = ["dependencies"]
packages = [
{ include = "halfspace", from = "src" }
]

[tool.poetry.dependencies]
python = ">=3.10,<3.13"
mip = ">=1.15.0"
numpy = ">=1.25.2"
pandas = ">=2.0.3"

[tool.poetry.group.dev.dependencies]
mypy = "*"
pre-commit = "*"
ruff = "*"

[tool.setuptools.dynamic]
dependencies = {file = ["requirements.txt"]}
[tool.poetry.group.test.dependencies]
pytest = "*"
pytest-cov = "*"

[project.urls]
Homepage = "https://github.com/joshivanhoe/halfspace"
Issues = "https://github.com/joshivanhoe/halfspace/issues"
[tool.ruff]
line-length = 100

[tool.ruff.lint]
extend-select = ["D"] # pydocstyle

[tool.ruff.lint.pydocstyle]
convention = "google"

[tool.ruff.lint.per-file-ignores]
"tests/**" = ["D"] # Ignore all directories named `tests` for pydocstyle

[tool.mypy]
ignore_missing_imports = true
exclude = ["convex_term.py"]

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
7 changes: 0 additions & 7 deletions requirements.txt

This file was deleted.

3 changes: 3 additions & 0 deletions src/halfspace/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
"""The `halfspace` module implements a modelling class for mixed-integer convex optimization problems."""

from .model import Model

__all__ = ["Model"]
69 changes: 36 additions & 33 deletions src/halfspace/convex_term.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@
It provides a modular framework for generating cutting planes.
"""

from typing import Union, Callable, Optional, Iterable
from typing import Callable, Iterable
from typing import TypeAlias, Literal, overload

import mip
import numpy as np

from .utils import standard_basis_vector

QueryPoint = dict[mip.Var, float]
Var = Union[mip.Var, Iterable[mip.Var], mip.LinExprTensor]
Input = Union[float, Iterable[float], np.ndarray]
Func = Callable[[Input], float]
FuncGrad = Callable[[Input], tuple[float, Union[float, np.ndarray]]]
Grad = Callable[[Input], Union[float, np.ndarray]]
QueryPoint: TypeAlias = dict[mip.Var, float]
Var: TypeAlias = mip.Var | Iterable[mip.Var] | mip.LinExprTensor
Input: TypeAlias = float | Iterable[float] | np.ndarray
Func: TypeAlias = Callable[[Input], float]
FuncGrad: TypeAlias = Callable[[Input], tuple[float, float | np.ndarray]]
Grad: TypeAlias = Callable[[Input], float | np.ndarray]


class ConvexTerm:
Expand All @@ -39,11 +41,11 @@ class ConvexTerm:
def __init__(
self,
var: Var,
func: Union[Func, FuncGrad],
grad: Optional[Union[Grad, bool]] = None,
func: Func | FuncGrad,
grad: Grad | bool | None = None,
step_size: float = 1e-6,
name: str = "",
):
) -> None:
"""Convex term constructor.

Args:
Expand All @@ -59,9 +61,15 @@ def __init__(
self.step_size = step_size
self.name = name

@overload
def __call__(self, query_point: QueryPoint, return_grad: Literal[False] = False) -> float: ...

@overload
def __call__(
self, query_point: QueryPoint, return_grad: bool = False
) -> Union[float, tuple[float, Union[float, np.ndarray]]]:
self, query_point: QueryPoint, return_grad: Literal[True] = True
) -> tuple[float, float | np.ndarray]: ...

def __call__(self, query_point: QueryPoint, return_grad: bool = False) -> float | tuple[float, float | np.ndarray]:
"""Evaluate the term and (optionally) its gradient.

Args:
Expand Down Expand Up @@ -96,20 +104,18 @@ def generate_cut(self, query_point: QueryPoint) -> mip.LinExpr:
Returns:
The linear constraint representing the cutting plane.
"""
fun, grad = self(query_point=query_point, return_grad=True)
func, grad = self(query_point=query_point, return_grad=True)
x = self._get_input(query_point=query_point)
if self.is_multivariable:
return mip.xsum(grad * (np.array(self.var) - x)) + fun
return grad * (self.var - x) + fun
return mip.xsum(grad * (np.array(self.var) - x)) + func
return grad * (self.var - x) + func

def _get_input(self, query_point: QueryPoint) -> Input:
if self.is_multivariable:
return np.array([query_point[var] for var in self.var])
return query_point[self.var]

def _evaluate_func(
self, x: Input
) -> Union[float, tuple[float, Union[float, np.ndarray]]]:
def _evaluate_func(self, x: Input) -> float | tuple[float, float | np.ndarray]:
"""Evaluate the function value.

If `grad=True`, then both the value of the function and it's gradient are returned.
Expand All @@ -120,7 +126,7 @@ def _evaluate_func(
return self.func(*x)
raise TypeError(f"Input of type '{type(x)}' not supported.")

def _evaluate_grad(self, x: Input) -> Union[float, np.ndarray]:
def _evaluate_grad(self, x: Input) -> float | np.ndarray:
"""Evaluate the gradient."""
if not self.grad:
return self._approximate_grad(x=x)
Expand All @@ -130,21 +136,18 @@ def _evaluate_grad(self, x: Input) -> Union[float, np.ndarray]:
return self.grad(*x)
raise TypeError(f"Input of type '{type(x)}' not supported.")

def _approximate_grad(self, x: Input) -> Union[float, np.ndarray]:
def _approximate_grad(self, x: Input) -> float | np.ndarray:
"""Approximate the gradient of the function at point using the central finite difference method."""
if self.is_multivariable:
indexes = np.arange(len(x))
return np.array(
[
(
self._evaluate_func(x=x + self.step_size / 2 * (indexes == i))
- self._evaluate_func(x=x - self.step_size / 2 * (indexes == i))
)
/ self.step_size
for i in indexes
]
)
n_dim = len(x)
grad = np.zeros(n_dim)
for i in range(n_dim):
e_i = standard_basis_vector(i=i, n_dim=n_dim)
grad[i] = (
self._evaluate_func(x=x + self.step_size / 2 * e_i)
- self._evaluate_func(x=x - self.step_size / 2 * e_i)
) / self.step_size
return grad
return (
self._evaluate_func(x=x + self.step_size / 2)
- self._evaluate_func(x=x - self.step_size / 2)
self._evaluate_func(x=x + self.step_size / 2) - self._evaluate_func(x=x - self.step_size / 2)
) / self.step_size
Loading
Loading