Skip to content

Commit

Permalink
Merge pull request #7 from fabian-sp/best-practices-2
Browse files Browse the repository at this point in the history
Apply Scientific Python best practices
  • Loading branch information
fabian-sp authored Mar 8, 2024
2 parents 63b95c3 + df0798b commit f79b0db
Show file tree
Hide file tree
Showing 19 changed files with 765 additions and 505 deletions.
34 changes: 19 additions & 15 deletions .github/workflows/unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,24 @@ on:
workflow_dispatch:

jobs:
build:
lint:
name: Lint
runs-on: ubuntu-latest
steps:

- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Test with pytest
run: |
pip install pytest
pytest ncopt/tests/
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.9"
- uses: pre-commit/action@v3.0.1

test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.9"
- run: pipx install hatch
- run: hatch env create
- run: hatch run test
9 changes: 9 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.3.0
hooks:
# Run the linter.
- id: ruff
# Run the formatter.
- id: ruff-format
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ This repository contains a `Python` implementation of the SQP-GS (*Sequential Qu

**Note:** this implementation is a **prototype code**, it has been tested only for a simple problem and it is not performance-optimized. A Matlab implementation is available from the authors of the paper, see [2].

## Installation

If you want to install an editable version of this package in your Python environment, run the command

```
python -m pip install --editable .
```

## Mathematical description

The algorithm can solve problems of the form
Expand Down
58 changes: 28 additions & 30 deletions example_rosenbrock.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"""
@author: Fabian Schaipp
"""
import numpy as np

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.lines import Line2D

from ncopt.funs import f_rosenbrock, g_max
from ncopt.sqpgs import SQPGS
from ncopt.funs import f_rosenbrock, g_max, g_linear

#%% Setup
# %% Setup

f = f_rosenbrock()
g = g_max()
Expand All @@ -20,65 +22,61 @@
# equality constraints (list of functions)
gE = []

xstar = np.array([1/np.sqrt(2), 0.5]) # solution
xstar = np.array([1 / np.sqrt(2), 0.5]) # solution

#%% How to use the solver
# %% How to use the solver

problem = SQPGS(f, gI, gE, x0=None, tol=1e-6, max_iter=100, verbose=True)
x = problem.solve()

print(f"Distance to solution:", f'{np.linalg.norm(x-xstar):.9f}')
print("Distance to solution:", f"{np.linalg.norm(x-xstar):.9f}")

#%% Solve from multiple starting points and plot
# %% Solve from multiple starting points and plot

_x, _y = np.linspace(-2,2,100), np.linspace(-2,2,100)
_x, _y = np.linspace(-2, 2, 100), np.linspace(-2, 2, 100)
X, Y = np.meshgrid(_x, _y)
Z = np.zeros_like(X)

for j in np.arange(X.shape[0]):
for i in np.arange(X.shape[1]):
Z[i,j] = f.eval(np.array([X[i,j], Y[i,j]]))
Z[i, j] = f.eval(np.array([X[i, j], Y[i, j]]))

#%%
from matplotlib.lines import Line2D
# %%

fig, ax = plt.subplots(figsize=(5,4))
fig, ax = plt.subplots(figsize=(5, 4))

np.random.seed(123)

# Plot contour and solution
ax.contourf(X, Y, Z, cmap='gist_heat', levels=20, alpha=0.7,
antialiased=True, lw=0, zorder=0)
# ax.contourf(X, Y, Z, colors='lightgrey', levels=20, alpha=0.7,
ax.contourf(X, Y, Z, cmap="gist_heat", levels=20, alpha=0.7, antialiased=True, lw=0, zorder=0)
# ax.contourf(X, Y, Z, colors='lightgrey', levels=20, alpha=0.7,
# antialiased=True, lw=0, zorder=0)
# ax.contour(X, Y, Z, cmap='gist_heat', levels=8, alpha=0.7,
# ax.contour(X, Y, Z, cmap='gist_heat', levels=8, alpha=0.7,
# antialiased=True, linewidths=4, zorder=0)
ax.scatter(xstar[0], xstar[1], marker="*", s=200, c="gold", zorder=1)
ax.scatter(xstar[0], xstar[1], marker="*", s=200, c="gold", zorder=1)

for i in range(20):
x0 = np.random.randn(2)
problem = SQPGS(f, gI, gE, x0, tol=1e-6, max_iter=100, verbose=False,
store_history=True)
problem = SQPGS(f, gI, gE, x0, tol=1e-6, max_iter=100, verbose=False, store_history=True)
x_k = problem.solve()
print(x_k)

x_hist = problem.x_hist
ax.plot(x_hist[:,0], x_hist[:,1], c="silver", lw=1, ls='--', alpha=0.5, zorder=2)
ax.plot(x_hist[:, 0], x_hist[:, 1], c="silver", lw=1, ls="--", alpha=0.5, zorder=2)
ax.scatter(x_k[0], x_k[1], marker="+", s=50, c="k", zorder=3)
ax.scatter(x0[0], x0[1], marker="o", s=30, c="silver", zorder=3)
ax.set_xlim(-2,2)
ax.set_ylim(-2,2)

ax.set_xlim(-2, 2)
ax.set_ylim(-2, 2)

fig.suptitle("Trajectory for multiple starting points")

legend_elements = [Line2D([0], [0], marker='*', lw=0, color='gold', label='Solution',
markersize=15),
Line2D([0], [0], marker='o', lw=0, color='silver', label='Starting point',
markersize=8),
Line2D([0], [0], marker='+', lw=0, color='k', label='Final iterate',
markersize=8),]
legend_elements = [
Line2D([0], [0], marker="*", lw=0, color="gold", label="Solution", markersize=15),
Line2D([0], [0], marker="o", lw=0, color="silver", label="Starting point", markersize=8),
Line2D([0], [0], marker="+", lw=0, color="k", label="Final iterate", markersize=8),
]
ax.legend(handles=legend_elements, ncol=3, fontsize=8)

fig.tight_layout()
fig.savefig('data/img/rosenbrock.png')
fig.savefig("data/img/rosenbrock.png")
2 changes: 0 additions & 2 deletions ncopt/sqpgs/__init__.py

This file was deleted.

37 changes: 0 additions & 37 deletions ncopt/sqpgs/defaults.py

This file was deleted.

104 changes: 104 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "ncopt"
dynamic = ["version"]
description = 'This repository contains a Python implementation of the SQP-GS (Sequential Quadratic Programming - Gradient Sampling) algorithm by Curtis and Overton.'
readme = "README.md"
requires-python = ">=3.9"
license = "BSD-3-Clause"
keywords = []
authors = [
{ name = "fabian-sp", email = "fabian.schaipp@gmail.com" },
]
classifiers = [
"Development Status :: 4 - Beta",
"Programming Language :: Python",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]
dependencies = [
"numpy",
"torch",
"cvxopt",
]

[project.urls]
Documentation = "https://github.com/fabian-sp/ncOPT#readme"
Issues = "https://github.com/fabian-sp/ncOPT/issues"
Source = "https://github.com/fabian-sp/ncOPT"

[tool.hatch.version]
path = "src/ncopt/__about__.py"

[tool.hatch.envs.default]
dependencies = [
"coverage[toml]>=6.5",
"pytest",
"matplotlib",
]

[tool.hatch.envs.default.scripts]
test = "pytest {args:tests}"
test-cov = "coverage run -m pytest {args:tests}"
cov-report = [
"- coverage combine",
"coverage report",
]
cov = [
"test-cov",
"cov-report",
]

[[tool.hatch.envs.all.matrix]]
python = ["3.9", "3.10", "3.11", "3.12"]

[tool.hatch.envs.types]
dependencies = [
"mypy>=1.0.0",
]
[tool.hatch.envs.types.scripts]
check = "mypy --install-types --non-interactive {args:src/ncopt tests}"

[tool.coverage.run]
source_pkgs = ["ncopt", "tests"]
branch = true
parallel = true
omit = [
"src/ncopt/__about__.py",
]

[tool.coverage.paths]
ncopt = ["src/ncopt", "*/ncopt/src/ncopt"]
tests = ["tests", "*/ncopt/tests"]

[tool.coverage.report]
exclude_lines = [
"no cov",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
]

[tool.ruff]
lint.select = [
"E",
"F",
"I",
"NPY201",
"W605", # Check for invalid escape sequences in docstrings (errors in py >= 3.11)
]
lint.ignore = [
"E741", # ambiguous variable name
]
line-length = 100

# The minimum Python version that should be supported
target-version = "py39"

src = ["src"]
30 changes: 16 additions & 14 deletions scripts/timing_rosenbrock.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,42 @@
"""
author: Fabian Schaipp
"""
import numpy as np
import sys

sys.path.append('..')
import timeit

import numpy as np

from ncopt.funs import f_rosenbrock, g_linear, g_max
from ncopt.sqpgs import SQPGS
from ncopt.funs import f_rosenbrock, g_max, g_linear

#%%
# %%
f = f_rosenbrock()
g = g_max()

# inequality constraints (list of functions)
gI = [g]

xstar = np.array([1/np.sqrt(2), 0.5])
xstar = np.array([1 / np.sqrt(2), 0.5])

np.random.seed(31)
x0 = np.random.randn(2)

#%% Timing one scalar inequality constraint
# %% Timing one scalar inequality constraint

# equality constraints (list of scalar functions)
gE = []
problem = SQPGS(f, gI, gE, x0, tol=1e-20, max_iter=100, verbose=False)

%timeit -n 20 x_k = problem.solve()
timeit.timeit("x_k = problem.solve()", number=20)

# result (start of refactoring):
# result (start of refactoring):
# 168 ms ± 2.96 ms per loop (mean ± std. dev. of 7 runs, 20 loops each)

#%% Timing equality constraint
# %% Timing equality constraint

A = np.eye(2); b = np.zeros(2); g1 = g_linear(A, b)
A = np.eye(2)
b = np.zeros(2)
g1 = g_linear(A, b)

# equality constraints (list of scalar functions)
gE = [g1]
Expand All @@ -43,7 +45,7 @@
np.random.seed(31)
x0 = np.random.randn(2)

%timeit -n 10 x_k = problem.solve()
timeit.timeit("x_k = problem.solve()", number=10)

# result (start of refactoring):
# 291 ms ± 3.53 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
# result (start of refactoring):
# 291 ms ± 3.53 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Loading

0 comments on commit f79b0db

Please sign in to comment.