Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
1ee45a5
First version of scipy interface and tests
FrancoisGallard Jan 10, 2026
60004c1
Minor improvements on messages, status, unconstrained and BC tests
FrancoisGallard Jan 11, 2026
76d0d2b
Apply Black formatter
FrancoisGallard Jan 11, 2026
e4a0265
Interface works with constraints
FrancoisGallard Jan 11, 2026
b35cd5f
Lincenses and doc
FrancoisGallard Jan 11, 2026
88697c9
Improved scipy and GEMSO interfaces
FrancoisGallard Jan 25, 2026
c82b0a6
First implementaiton of Hesssian : still buggy
FrancoisGallard Jan 25, 2026
0576a0d
Update with JC Giret and Claude code, works with Hessian, 1 test fails
FrancoisGallard May 10, 2026
c845d44
Minor cleanup
FrancoisGallard May 10, 2026
bb5d451
Added CI workflows
cvanaret May 13, 2026
12251a0
Fixed paths
cvanaret May 13, 2026
0932a56
Install pytest
cvanaret May 13, 2026
e2214b3
Install scipy in GEMSEO workflow
cvanaret May 13, 2026
3738966
pip install pytest gemseo scipy
cvanaret May 13, 2026
4f45285
Moved test files back into their respective directories + fixed paths
cvanaret May 13, 2026
3e5d599
Run tests from root directory
cvanaret May 13, 2026
8cb3c39
Tried to fix the unopy packaging
cvanaret May 13, 2026
5db4a0a
python -m pytest --import-mode=importlib
cvanaret May 13, 2026
9eec59b
Use model.set_lagrangian_sign_convention
cvanaret May 13, 2026
37d5086
Upgrade to unopy 0.4.*
cvanaret May 13, 2026
7add569
Added scipy_interface to PYTHONPATH
cvanaret May 13, 2026
19ad406
Install scipy and GEMSEO interfaces
cvanaret May 13, 2026
0b6913d
Disabled filterslp tests, enabled ipopt instead
cvanaret May 13, 2026
a5d2a2a
Added debug prints
cvanaret May 13, 2026
2bfead1
Commented out test_rosen_constr
cvanaret May 13, 2026
ab56a84
Added debug print to understand Dimension mismatch in constraints_low…
cvanaret May 13, 2026
b97aff9
Fixed bounds length in test_rosen_constr2
cvanaret May 13, 2026
e896830
Custom exception for Jacobian evaluation error
cvanaret May 13, 2026
74b53a8
Reverted to original constraint_jacobian_callback
cvanaret May 13, 2026
60701e2
Removed ipopt test (Hessian operator not compatible with EQPSolver)
cvanaret May 13, 2026
262e91e
Uncommented test_rosen_constr
cvanaret May 13, 2026
2628245
Removed UNO_IPOPT from GEMSEO tests
cvanaret May 13, 2026
906f48c
Removed prints
cvanaret May 13, 2026
7174b20
Set logger to DEBUG3 to debug GEMSEO tests
cvanaret May 14, 2026
80501f5
Allow pytest to print to stdout
cvanaret May 14, 2026
25952f2
Logger: print subproblem and solution
cvanaret May 14, 2026
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
5 changes: 2 additions & 3 deletions .github/workflows/unopy-test-example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ jobs:
python-version: ${{ matrix.python-version }}
architecture: ${{ matrix.architecture }}

- name: Install pybind11 on Linux
if: runner.os == 'Linux'
- name: Install pybind11
run: sudo apt install -y python3-pybind11 pybind11-dev

- name: Download dependencies
Expand All @@ -55,4 +54,4 @@ jobs:

- name: Run example
working-directory: ${{github.workspace}}/interfaces/Python/example
run: python example_hs015.py
run: python example_hs015.py
59 changes: 59 additions & 0 deletions .github/workflows/unopy-tests-gemseo.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Test unopy via GEMSEO interface

on:
push:
branches: [ "main" ]
paths-ignore:
- '*.md'
- 'LICENSE'
- '*.cff'
- '*.yml'
- '*.yaml'
- 'docs/**'
pull_request:
branches: [ "main" ]
paths-ignore:
- '*.md'
- 'LICENSE'
- '*.cff'
- '*.yml'
- '*.yaml'
- 'docs/**'

env:
BUILD_TYPE: Debug

jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest]
architecture: [x64]
python-version: ["3.12"]

steps:
- uses: actions/checkout@v4

- name: Set up Python
# This is the version of the action for setting up Python, not the Python version.
uses: actions/setup-python@v5
with:
# Semantic version range syntax or exact version of a Python version
python-version: ${{ matrix.python-version }}
architecture: ${{ matrix.architecture }}

- name: Install pybind11
run: sudo apt install -y python3-pybind11 pybind11-dev

- name: Install pytest, gemseo, and scipy
run: pip install pytest gemseo scipy

- name: Download dependencies
run: bash dependencies/scripts/download_dependencies.sh

- name: Compile and install unopy
run: CMAKE_BUILD_PARALLEL_LEVEL=$(nproc) pip install . -v

- name: Run tests
run: python -m pytest -s --import-mode=importlib ${{github.workspace}}/interfaces/Python/gemseo_uno/test_gemseo_uno.py
59 changes: 59 additions & 0 deletions .github/workflows/unopy-tests-scipy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Test unopy via Scipy interface

on:
push:
branches: [ "main" ]
paths-ignore:
- '*.md'
- 'LICENSE'
- '*.cff'
- '*.yml'
- '*.yaml'
- 'docs/**'
pull_request:
branches: [ "main" ]
paths-ignore:
- '*.md'
- 'LICENSE'
- '*.cff'
- '*.yml'
- '*.yaml'
- 'docs/**'

env:
BUILD_TYPE: Debug

jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest]
architecture: [x64]
python-version: ["3.12"]

steps:
- uses: actions/checkout@v4

- name: Set up Python
# This is the version of the action for setting up Python, not the Python version.
uses: actions/setup-python@v5
with:
# Semantic version range syntax or exact version of a Python version
python-version: ${{ matrix.python-version }}
architecture: ${{ matrix.architecture }}

- name: Install pybind11
run: sudo apt install -y python3-pybind11 pybind11-dev

- name: Install pytest and scipy
run: pip install pytest scipy

- name: Download dependencies
run: bash dependencies/scripts/download_dependencies.sh

- name: Compile and install unopy
run: CMAKE_BUILD_PARALLEL_LEVEL=$(nproc) pip install . -v

- name: Run tests
run: python -m pytest --import-mode=importlib ${{github.workspace}}/interfaces/Python/scipy_interface/test_scipy_interface.py
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,9 @@ if(SKBUILD)
target_compile_definitions(unopy PRIVATE PYBIND11_DETAILED_ERROR_MESSAGES)
endif()

install(DIRECTORY interfaces/Python/unopy/ DESTINATION unopy)
install(DIRECTORY interfaces/Python/scipy_interface/ DESTINATION scipy_interface)
install(DIRECTORY interfaces/Python/gemseo_uno/ DESTINATION gemseo_uno)
install(TARGETS unopy
LIBRARY DESTINATION unopy # Linux/macOS: unopy/unopy.so
RUNTIME DESTINATION unopy # Windows: unopy/unopy.pyd
Expand Down
2 changes: 1 addition & 1 deletion interfaces/AMPL/AMPLModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ namespace uno {
fint error_flag = 0;
this->asl->p.Jacval(this->asl, const_cast<double*>(x.data()), jacobian_values, &error_flag);
if (0 < error_flag) {
throw GradientEvaluationError();
throw JacobianEvaluationError();
}
++this->number_model_evaluations.jacobian;
}
Expand Down
8 changes: 4 additions & 4 deletions interfaces/C/Uno_C_API.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ class UnoModel: public Model {
const uno_int return_code = this->user_model.jacobian(this->user_model.number_variables,
this->user_model.number_jacobian_nonzeros, x.data(), jacobian_values, this->user_model.user_data);
if (0 < return_code) {
throw GradientEvaluationError();
throw JacobianEvaluationError();
}
++this->number_model_evaluations.jacobian;
}
Expand Down Expand Up @@ -189,7 +189,7 @@ class UnoModel: public Model {
const uno_int return_code = this->user_model.jacobian_operator(this->user_model.number_variables,
this->user_model.number_constraints, x, true, vector, result, this->user_model.user_data);
if (0 < return_code) {
throw GradientEvaluationError();
throw JacobianEvaluationError();
}
}
else {
Expand All @@ -202,7 +202,7 @@ class UnoModel: public Model {
const uno_int return_code = this->user_model.jacobian_transposed_operator(this->user_model.number_variables,
this->user_model.number_constraints, x, true, vector, result, this->user_model.user_data);
if (0 < return_code) {
throw GradientEvaluationError();
throw JacobianEvaluationError();
}
}
else {
Expand Down Expand Up @@ -1265,4 +1265,4 @@ void uno_destroy_solver(void* solver) {
}
delete uno_solver;
}
}
}
6 changes: 3 additions & 3 deletions interfaces/Python/cpp_classes/PythonModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ namespace uno {
++this->number_model_evaluations.jacobian;
}
catch (const std::exception&) {
throw GradientEvaluationError();
throw JacobianEvaluationError();
}
}
}
Expand Down Expand Up @@ -192,7 +192,7 @@ namespace uno {
(*this->user_model.jacobian_operator)(x_py, true, vector_py, result_py);
}
catch (const std::exception&) {
throw GradientEvaluationError();
throw JacobianEvaluationError();
}
}
else {
Expand All @@ -211,7 +211,7 @@ namespace uno {
(*this->user_model.jacobian_transposed_operator)(x_py, true, vector_py, result_py);
}
catch (const std::exception&) {
throw GradientEvaluationError();
throw JacobianEvaluationError();
}
}
else {
Expand Down
1 change: 1 addition & 0 deletions interfaces/Python/example/example_hs015.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# Licensed under the MIT license. See LICENSE file in the project directory for details.

import unopy

Inf = float("inf")

# hs015.mod
Expand Down
Empty file.
147 changes: 147 additions & 0 deletions interfaces/Python/gemseo_uno/gemseo_uno.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# Copyright 2021 IRT Saint Exupéry, https://www.irt-saintexupery.com
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License version 3 as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# Contributors:
# INITIAL AUTHORS - initial API and implementation and/or initial
# documentation
# :author: François Gallard
# OTHER AUTHORS - MACROSCOPIC CHANGES
"""The library of Uno constrained gradient-based optimization algorithms."""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any
from typing import ClassVar
from typing import TYPE_CHECKING

from gemseo.algos.design_space_utils import get_value_and_bounds
from gemseo.algos.opt.base_optimization_library import (
OptimizationAlgorithmDescription,
BaseOptimizationLibrary,
)
from numpy import isfinite
from numpy import real

from gemseo_uno.settings.base_uno_settings import UNO_Settings
from scipy_interface.scipy_uno import minimize

if TYPE_CHECKING:
from gemseo.algos.optimization_problem import OptimizationProblem


@dataclass
class UnoAlgorithmDescription(OptimizationAlgorithmDescription):
"""The description of the Uno constrained grdient based optimization library."""

library_name: str = "Uno"
"""The library name."""

handle_equality_constraints: bool = True
"""Whether the optimization algorithm handles equality constraints."""

handle_inequality_constraints: bool = True
"""Whether the optimization algorithm handles inequality constraints."""

positive_constraints: bool = True
"""Whether the optimization algorithm requires positive constraints."""

require_gradient: bool = True
"""Whether the optimization algorithm requires the gradient."""

Settings: type[UNO_Settings] = UNO_Settings
"""The option validation model for Uno optimization library."""

website: str = "https://unosolver.readthedocs.io/en/latest/"
"""The website of the wrapped library or algorithm."""


class UnoOpt(BaseOptimizationLibrary[UNO_Settings]):
"""The library of Uno optimization algorithms."""

ALGORITHM_INFOS: ClassVar[dict[str, UnoAlgorithmDescription]] = {
"UNO_Filter_SQP": UnoAlgorithmDescription(
algorithm_name="UNO_Filter_SQP",
description=(
"Sequential Quadratic Programming (SQP) "
"implemented in the Uno library"
),
internal_algorithm_name="filtersqp",
Settings=UNO_Settings,
),
"UNO_Filter_SLP": UnoAlgorithmDescription(
algorithm_name="UNO_Filter_SLP",
description=(
"Sequential Linear Programming (SLP) " "implemented in the Uno library"
),
internal_algorithm_name="filterslp",
Settings=UNO_Settings,
),
"UNO_Funnel_SQP": UnoAlgorithmDescription(
algorithm_name="UNO_Funnel_SQP",
description=(
"Funnel Sequential Quadratic Programming (SQP)"
"implemented in the Uno library"
),
internal_algorithm_name="funnelsqp",
Settings=UNO_Settings,
),
"UNO_IPOPT": UnoAlgorithmDescription(
algorithm_name="UNO_IPOPT",
description=(
"Interior Point Optimization (IPOPT)" "implemented in the Uno library"
),
internal_algorithm_name="ipopt",
Settings=UNO_Settings,
),
}

def _run(self, problem: OptimizationProblem) -> tuple[str, Any]:
# Get the normalized bounds:
x_0, l_b, u_b = get_value_and_bounds(
problem.design_space, self._settings.normalize_design_space
)
# Replace infinite values with None:
l_b = [val if isfinite(val) else None for val in l_b]
u_b = [val if isfinite(val) else None for val in u_b]
bounds = list(zip(l_b, u_b, strict=False))

# Get constraint in SciPy format
scipy_constraints = [
{
"type": constraint.f_type,
"fun": constraint.evaluate,
"jac": constraint.jac,
}
for constraint in self._get_right_sign_constraints(problem)
]

# Filter settings to get only the uno ones
settings_ = self._filter_settings(self._settings.model_dump(), UNO_Settings)

# Deactivate stopping criteria which are handled by GEMSEO
tolerance = 0.0

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sets the primal-dual tolerances to 0, therefore Uno most likely won't terminate (see Power2 test).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GEMSEO has its own stop criteria based on xtol, ftol, maxiter or KKT, so we always deactivate the solver tolerances to have consistent stopping criteria when the user changes of optimizer.

@cvanaret cvanaret May 15, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But how do you expect the optimizer to terminate if you set the tolerance to 0? Surely you usually pass a termination callback of some sort? It's not the case for Uno (yet).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does not by itself, we raise an exception in the objective function call and that stops the optimization, then we catch it and handle the search for the best point. We can also add an option to activate the solver stopping criteria if you want to check that all goes well.

@cvanaret cvanaret May 15, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooh that's a brittle strategy. That works for (say) Fortran codes because they cannot catch exceptions, but you cannot assume any solver will behave the way you expect. Uno always catches the evaluation exceptions and backtracks. In the Power2 case, I can read from the Uno log: A function could not be evaluated. The trust-region radius will be reduced: indeed the radius is decreased 30 times in a row until Uno terminates with Algorithmic error.

A good alternative would be to keep the tolerances to 0 and set a user termination callback based on GEMSEO criteria. It's implemented in the C API (see here, and used here in the Uno outer loop), so it would be minimal effort to add this to unopy.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think ensuring that we catch the exception in pyuno and manage the solver stop there would be better. It will appear in many user scripts independently of gemseo that exceptions be raised when calling the objective.

@FrancoisGallard FrancoisGallard May 19, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cvanaret if you can expose to unopy the uno_termination_callback from the c API, I will try to make this work.

@cvanaret cvanaret May 19, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK I'm on it: #720

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you are OK I propose to add a try catch around the evaluation of the functions in the scipy interface, if an exception is catched, we return a NaN and then store this state of error in a global variable, and the termination callback returns True based on this global state.

@cvanaret cvanaret May 20, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's give it a shot and we'll see what it looks like.


opt_result = minimize(
fun=lambda x: real(problem.objective.evaluate(x)),
jac=problem.objective.jac,
x0=x_0,
method=self.ALGORITHM_INFOS[self._algo_name].internal_algorithm_name,
bounds=bounds,
constraints=scipy_constraints,
options=settings_,
tol=tolerance,
)

return opt_result.message, opt_result.status
Empty file.
Loading
Loading