Skip to content
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

Support for affine spaces in code generation #344

Merged
merged 9 commits into from
Aug 15, 2024
Merged
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
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ rpmalloc = { version = "0.2", features = [
# epigraph of the squared Euclidean norm
roots = "0.0.8"

# Least squares solver
# Least squares solver (NOTE: ndarray must be version 0.15 - not 0.16)
# Bug report: https://github.com/argmin-rs/modcholesky/issues/34
ndarray = { version = "0.15", features = ["approx"] }
modcholesky = "0.1"

Expand Down
29 changes: 16 additions & 13 deletions docs/python-interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,18 +77,21 @@ following types of constraints:

| Constraint | Explanation |
|--------------------|------------------------------------------------|
| `Ball2` | Euclidean ball: `Ball2(None, r)` creates a Euclidean ball of radius `r` centered at the origin, and `Ball2(xc, r)` is a ball centered at point `xc` (list/np.array) |
| `BallInf` | Ball of infinity norm:`BallInf(None, r)` creates an infinity-norm ball of radius `r` centered at the origin, and `BallInf(xc, r)` is an infinity ball centered at point `xc` (list/np.array) |
| `Ball1` | L1 ball: `Ball(None, r)` creates an ell1-ball of radius `r` centered at the origin, and `BallInf(xc, r)` is an ell1-ball centered at point `xc` (list/np.array)|
| `Sphere2` | Euclidean sphere: `Sphere2(None, r)` creates a Euclidean sphere of radius `r` centered at the origin, and `Sphere2(xc, r)` is a sphere centered at point `xc` (list/np.array) |
| `Simplex` | A simplex of <em>size</em> $\alpha$ is a set of the form $\Delta_\alpha = \\{x \in \mathbb{R}^n {}:{} x_i \geq 0, \sum_i x_i = \alpha\\}$. Create one with `Simplex(alpha)`. Projections are computed using Condat's [fast projection method](https://link.springer.com/article/10.1007/s10107-015-0946-6). |
| `Halfspace` | A halfspace is a set of the form $\\{u \in \mathbb{R}^{n_u} {}:{} \langle c, u\rangle \leq b \\}$, for a vector $c$ and a scalar $b$. The syntax is straightforwarrd: `Halfspace(c, b)`. |
| `FiniteSet` | Finite set, $\\{u^{(1)},\ldots,u^{(m)}\\}$; the set of point is provided as a list of lists, for example, `FiniteSet([[1,2],[2,3],[4,5]])`. The commonly used set of binary numbers, $\\{0, 1\\}$, is created with `FiniteSet([[0], [1]])`. |
| `NoConstraints` | No constraints - the whole $\mathbb{R}^{n}$|
| `Rectangle` | Rectangle, $$R = \\{u \in \mathbb{R}^{n_u} {}:{} f_{\min} \leq u \leq f_{\max}\\},$$ for example, `Rectangle(fmin, fmax)` |
| `SecondOrderCone` | Second-order aka "ice cream" aka "Lorenz" cone |
| `EpigraphSquaredNorm`| The epigraph of the squared Eucliden norm is a set of the form $X = \\{(z, t) \in \mathbb{R}^{n+1}: \Vert z \Vert \leq t\\}$. |
| `CartesianProduct` | Cartesian product of any of the above. See more information below. |
| `AffineSpace` | An affine space is a set of the form $\\{x\in\mathbb{R}^n {}:{} Ax = b\\}$ for a matrix $A\in \mathbb{R}^p$ and vector $b$. Docs: ([Rust](https://docs.rs/optimization_engine/latest/optimization_engine/constraints/struct.AffineSpace.html), [Python](https://alphaville.github.io/optimization-engine/api-dox/html/opengen.constraints.html#module-opengen.constraints.affine_space)) |
| `Ball2` | Euclidean ball: `Ball2(None, r)` creates a Euclidean ball of radius `r` centered at the origin, and `Ball2(xc, r)` is a ball centered at point `xc` (list/np.array) Docs: ([Rust](https://docs.rs/optimization_engine/latest/optimization_engine/constraints/struct.Ball2.html), [Python](https://alphaville.github.io/optimization-engine/api-dox/html/opengen.constraints.html#module-opengen.constraints.ball2)) |
| `BallInf` | Ball of infinity norm:`BallInf(None, r)` creates an infinity-norm ball of radius `r` centered at the origin, and `BallInf(xc, r)` is an infinity ball centered at point `xc` (list/np.array) Docs: ([Rust](https://docs.rs/optimization_engine/latest/optimization_engine/constraints/struct.BallInf.html), [Python](https://alphaville.github.io/optimization-engine/api-dox/html/opengen.constraints.html#module-opengen.constraints.ball_inf)) |
| `Ball1` | L1 ball: `Ball(None, r)` creates an ell1-ball of radius `r` centered at the origin, and `BallInf(xc, r)` is an ell1-ball centered at point `xc` (list/np.array) Docs: ([Rust](https://docs.rs/optimization_engine/latest/optimization_engine/constraints/struct.Ball1.html), [Python](https://alphaville.github.io/optimization-engine/api-dox/html/opengen.constraints.html#module-opengen.constraints.ball1)) |
| `Sphere2` | Euclidean sphere: `Sphere2(None, r)` creates a Euclidean sphere of radius `r` centered at the origin, and `Sphere2(xc, r)` is a sphere centered at point `xc` (list/np.array) Docs: ([Rust](https://docs.rs/optimization_engine/latest/optimization_engine/constraints/struct.Sphere2.html), [Python](https://alphaville.github.io/optimization-engine/api-dox/html/opengen.constraints.html#module-opengen.constraints.sphere2)) |
| `Simplex` | A simplex of <em>size</em> $\alpha$ is a set of the form $\Delta_\alpha = \\{x \in \mathbb{R}^n {}:{} x_i \geq 0, \sum_i x_i = \alpha\\}$. Create one with `Simplex(alpha)`. Projections are computed using Condat's [fast projection method](https://link.springer.com/article/10.1007/s10107-015-0946-6). Docs: ([Rust](https://docs.rs/optimization_engine/latest/optimization_engine/constraints/struct.Simplex.html), [Python](https://alphaville.github.io/optimization-engine/api-dox/html/opengen.constraints.html#module-opengen.constraints.simplex)) |
| `Halfspace` | A halfspace is a set of the form $\\{u \in \mathbb{R}^{n_u} {}:{} \langle c, u\rangle \leq b \\}$, for a vector $c$ and a scalar $b$. The syntax is straightforward: `Halfspace(c, b)`. Docs: ([Rust](https://docs.rs/optimization_engine/latest/optimization_engine/constraints/struct.Halfspace.html), [Python](https://alphaville.github.io/optimization-engine/api-dox/html/opengen.constraints.html#module-opengen.constraints.halfspace)) |
| `Hyperplane` | A hyperplane is a set given by $H=\\{x \in \mathbb{R}^n {}:{} c^\intercal x =b \\}$. Docs: [Rust](https://docs.rs/optimization_engine/latest/optimization_engine/constraints/struct.Hyperplane.html) |
| `FiniteSet` | Finite set, $\\{u^{(1)},\ldots,u^{(m)}\\}$; the set of point is provided as a list of lists, for example, `FiniteSet([[1,2],[2,3],[4,5]])`. The commonly used set of binary numbers, $\\{0, 1\\}$, is created with `FiniteSet([[0], [1]])`. Docs: ([Rust](https://docs.rs/optimization_engine/latest/optimization_engine/constraints/struct.FiniteSet.html), [Python](https://alphaville.github.io/optimization-engine/api-dox/html/opengen.constraints.html#module-opengen.constraints.finite_set)) |
| `NoConstraints` | No constraints - the whole $\mathbb{R}^{n}$ Docs: ([Rust](https://docs.rs/optimization_engine/latest/optimization_engine/constraints/struct.NoConstraints.html), [Python](https://alphaville.github.io/optimization-engine/api-dox/html/opengen.constraints.html#module-opengen.constraints.no_constraints)) |
| `Rectangle` | Rectangle, $$R = \\{u \in \mathbb{R}^{n_u} {}:{} f_{\min} \leq u \leq f_{\max}\\},$$ for example, `Rectangle(fmin, fmax)` Docs: ([Rust](https://docs.rs/optimization_engine/latest/optimization_engine/constraints/struct.Rectangle.html), [Python](https://alphaville.github.io/optimization-engine/api-dox/html/opengen.constraints.html#module-opengen.constraints.rectangle)) |
| `SecondOrderCone` | Second-order aka "ice cream" aka "Lorenz" cone. Docs: ([Rust](https://docs.rs/optimization_engine/latest/optimization_engine/constraints/struct.SecondOrderCone.html), [Python](https://alphaville.github.io/optimization-engine/api-dox/html/opengen.constraints.html#module-opengen.constraints.soc)) |
| `EpigraphSquaredNorm`| The epigraph of the squared Euclidean norm is a set of the form $X = \\{(z, t) \in \mathbb{R}^{n+1}: \Vert z \Vert \leq t\\}$. Docs: ([Rust](https://docs.rs/optimization_engine/latest/optimization_engine/constraints/struct.EpigraphSquaredNorm.html), Python: to be implemented) |
| `CartesianProduct` | Cartesian product of any of the above. See more information below. Docs: ([Rust](https://docs.rs/optimization_engine/latest/optimization_engine/constraints/struct.CartesianProduct.html), [Python](https://alphaville.github.io/optimization-engine/api-dox/html/opengen.constraints.html#module-opengen.constraints.cartesian)) |
| `Zero` | The set $\\{0\\}$. Docs: ([Rust](https://docs.rs/optimization_engine/latest/optimization_engine/constraints/struct.Zero.html), [Python](https://alphaville.github.io/optimization-engine/api-dox/html/opengen.constraints.html#module-opengen.constraints.zero)) |



Expand Down Expand Up @@ -424,7 +427,7 @@ Note here that the solver performed 6 outer
[penalty-type iterations](https://en.wikipedia.org/wiki/Penalty_method)
and 35 inner iterations overall. The infinity norm of the constraint
violations is approximately $7.66\cdot 10^{-5}$ (which is below the
default tolerance of $10^{-4}$. This means that the solution $u^\star$
default tolerance of $10^{-4}$.) This means that the solution $u^\star$
satisfies

<div class="math">
Expand Down
11 changes: 11 additions & 0 deletions open-codegen/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

Note: This is the Changelog file of `opengen` - the Python interface of OpEn

## [0.9.0] - 2024-08-15

### Added

* Support for affine spaces in code generation (see `opengen.constraints.AffineSpace`)
* TCP solver status can be printed



## [0.8.1] - 2024-07-17

### Added
Expand Down Expand Up @@ -193,6 +202,8 @@ Note: This is the Changelog file of `opengen` - the Python interface of OpEn
* Project-specific `tcp_iface` TCP interface
* Fixed `lbfgs` typo

[0.9.0]: https://github.com/alphaville/optimization-engine/compare/opengen-0.8.1...opengen-0.9.0
[0.8.1]: https://github.com/alphaville/optimization-engine/compare/v0.9.0...opengen-0.8.1
[0.8.1]: https://github.com/alphaville/optimization-engine/compare/v0.9.0...opengen-0.8.1
[0.8.0]: https://github.com/alphaville/optimization-engine/compare/opengen-0.7.1...opengen-0.8.0
[0.7.1]: https://github.com/alphaville/optimization-engine/compare/opengen-0.7.0...opengen-0.7.1
Expand Down
2 changes: 1 addition & 1 deletion open-codegen/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.8.1
0.9.0
11 changes: 8 additions & 3 deletions open-codegen/opengen/builder/optimizer_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
_ICASADI_PREFIX = 'icasadi_'
_ROS_PREFIX = 'ros_node_'

# Template files
_OPTIMIZER_RS = "optimizer.rs.jinja"
_OPTIMIZER_CARGO_TOML = "optimizer_cargo.toml.jinja"
_OPTIMIZER_BUILD_RS = "optimizer_build.rs.jinja"


def make_dir_if_not_exists(directory):
if not os.path.exists(directory):
Expand Down Expand Up @@ -243,7 +248,7 @@ def __generate_cargo_toml(self):
self.__logger.info("Generating Cargo.toml for target optimizer")
target_dir = self.__target_dir()
cargo_template = OpEnOptimizerBuilder.__get_template(
'optimizer_cargo.toml')
_OPTIMIZER_CARGO_TOML)
cargo_output_template = cargo_template.render(
meta=self.__meta,
build_config=self.__build_config,
Expand Down Expand Up @@ -540,7 +545,7 @@ def __generate_main_project_code(self):
"Generating main code for target optimizer (lib.rs)")
target_dir = self.__target_dir()
optimizer_rs_template = OpEnOptimizerBuilder.__get_template(
'optimizer.rs')
_OPTIMIZER_RS)
optimizer_rs_output_template = optimizer_rs_template.render(
solver_config=self.__solver_config,
meta=self.__meta,
Expand All @@ -557,7 +562,7 @@ def __generate_build_rs(self):
self.__logger.info("Generating build.rs for target optimizer")
target_dir = self.__target_dir()
build_rs_template = OpEnOptimizerBuilder.__get_template(
'optimizer_build.rs')
_OPTIMIZER_BUILD_RS)
build_rs_output_template = build_rs_template.render(
meta=self.__meta,
activate_clib_generation=self.__build_config.build_c_bindings)
Expand Down
1 change: 1 addition & 0 deletions open-codegen/opengen/constraints/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@
from .finite_set import *
from .halfspace import *
from .simplex import *
from .affine_space import *
56 changes: 56 additions & 0 deletions open-codegen/opengen/constraints/affine_space.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import casadi.casadi as cs
import numpy as np
from .constraint import Constraint
import opengen.functions as fn


class AffineSpace(Constraint):
"""An affine constraint

A constraint of the form :math:`Ax = b`, where :math:`A` and :math:`b` are
a matrix and a vector of appropriate dimensions
"""

def __init__(self, A, b):
"""Constructor for an affine space

:return: new instance of AffineSpace
"""
self.__A = A.flatten('C')
self.__b = b

@property
def matrix_a(self):
"""Matrix A
"""
return self.__A

@property
def vector_b(self):
"""Vector b
"""
return self.__b

def distance_squared(self, u):
"""Squared distance to affine space

Not implemented yet
"""
raise NotImplementedError()

def project(self, u):
"""Projection on affine space

Not implemented yet
"""
raise NotImplementedError()

def is_convex(self):
"""Affine spaces are convex sets
"""
return True

def is_compact(self):
"""Affine spaces are not compact sets
"""
return False
12 changes: 12 additions & 0 deletions open-codegen/opengen/tcp/solver_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,15 @@ def cost(self):
:return: Value of cost function at the solution
"""
return self.__dict__["__cost"]

def __repr__(self):
return "Solver Status Report:\n" + \
f"Exit status....... {self.exit_status}\n" + \
f"Num Outer Iters... {self.num_outer_iterations}\n" + \
f"Num Inner Iters... {self.num_inner_iterations}\n" + \
f"FPR............... {self.last_problem_norm_fpr}\n" + \
"Infeasibility\n" + \
f" L F1............ {self.f1_infeasibility}\n" + \
f" L Fw............ {self.f2_norm}\n" + \
f"Penalty............ {self.penalty}\n" + \
f"Time............... {self.solve_time_ms} ms\n"
2 changes: 1 addition & 1 deletion open-codegen/opengen/templates/icasadi/icasadi_lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ pub fn precondition(

/// Computes the initial penalty
///
/// Make sure that you have called init_
/// Make sure that you have called init_{{ meta.optimizer_name }}
pub fn initial_penalty(
u: &[f64],
static_params: &[f64],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,17 +61,18 @@ pub const {{meta.optimizer_name|upper}}_N1: usize = {{problem.dim_constraints_au
/// Number of penalty constraints
pub const {{meta.optimizer_name|upper}}_N2: usize = {{problem.dim_constraints_penalty() or 0}};

{% include "c/optimizer_cinterface.rs" %}
{% include "c/optimizer_cinterface.rs.jinja" %}

// ---Parameters of the constraints----------------------------------------------------------------------

{# CASE I: Ball* or Sphere #}
{% if 'Ball1' == problem.constraints.__class__.__name__ or 'Ball2' == problem.constraints.__class__.__name__ or 'BallInf' == problem.constraints.__class__.__name__ or 'Sphere2' == problem.constraints.__class__.__name__ -%}
/// Constraints: Centre of Ball
const CONSTRAINTS_BALL_XC: Option<&[f64]> = {% if problem.constraints.center is not none %}Some(&[{{problem.constraints.center | join(', ')}}]){% else %}None{% endif %};

/// Constraints: Radius of Ball
const CONSTRAINTS_BALL_RADIUS : f64 = {{problem.constraints.radius}};
{% elif 'Rectangle' == problem.constraints.__class__.__name__ -%}
{% endif %}
{# CASE II: Rectangle #}
{% if 'Rectangle' == problem.constraints.__class__.__name__ -%}
const CONSTRAINTS_XMIN :Option<&[f64]> = {% if problem.constraints.xmin is not none %}Some(&[
{%- for xmini in problem.constraints.xmin -%}
{%- if float('-inf') == xmini -%}std::f64::NEG_INFINITY{%- else -%}{{xmini}}{%- endif -%},
Expand All @@ -85,7 +86,6 @@ const CONSTRAINTS_XMAX :Option<&[f64]> = {% if problem.constraints.xmax is not n
{% endif %}



{% if problem.alm_set_c is not none %}
// ---Parameters of ALM-type constraints (Set C)---------------------------------------------------------
{% if 'Ball2' == problem.alm_set_c.__class__.__name__ or 'BallInf' == problem.alm_set_c.__class__.__name__ -%}
Expand Down Expand Up @@ -150,6 +150,10 @@ fn make_constraints() -> impl Constraint {
{% elif 'Rectangle' == problem.constraints.__class__.__name__ -%}
// - Rectangle:
Rectangle::new(CONSTRAINTS_XMIN, CONSTRAINTS_XMAX)
{% elif 'AffineSpace' == problem.constraints.__class__.__name__ -%}
let constraints_affine_a = vec![{{problem.constraints.matrix_a | join(', ')}}];
let constraints_affine_b = vec![{{problem.constraints.vector_b | join(', ')}}];
AffineSpace::new(constraints_affine_a, constraints_affine_b)
{% elif 'FiniteSet' == problem.constraints.__class__.__name__ -%}
// - Finite Set:
let data: &[&[f64]] = &[
Expand Down Expand Up @@ -192,6 +196,11 @@ fn make_constraints() -> impl Constraint {
let center_{{loop.index}}: Option<&[f64]> = {% if set_i.center is not none %}Some(&[{{set_i.center | join(', ')}}]){% else %}None{% endif %};
let set_{{loop.index}} = Sphere2::new(center_{{loop.index}}, radius_{{loop.index}});
let bounds = bounds.add_constraint(idx_{{loop.index}}, set_{{loop.index}});
{% elif 'AffineSpace' == set_i.__class__.__name__ -%}
let constraints_affine_a = vec![{{problem.constraints.matrix_a | join(', ')}}];
let constraints_affine_b = vec![{{problem.constraints.vector_b | join(', ')}}];
let set_{{loop.index}} = AffineSpace::new(constraints_affine_a, constraints_affine_b)
let bounds = bounds.add_constraint(idx_{{loop.index}}, set_{{loop.index}});
{% elif 'Simplex' == set_i.__class__.__name__ -%}
let alpha_{{loop.index}} = {{set_i.alpha}};
let set_{{loop.index}} = Simplex::new(alpha_{{loop.index}});
Expand Down
Loading