Skip to content
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
2 changes: 1 addition & 1 deletion pyoptsparse/pyCONMIN/pyCONMIN.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ def cnmngrad(n1, n2, x, f, g, ct, df, a, ic, nac):
dabfun = self.getOption("DABFUN")

itrm = self.getOption("ITRM")
nfeasct = self.getOption("ITRM")
nfeasct = self.getOption("NFEASCT")
nfdg = 1 # User will supply all gradients

# Counters for functions and gradients
Expand Down
39 changes: 31 additions & 8 deletions pyoptsparse/pyOpt_gradient.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Standard Python modules
from typing import Literal

# External modules
import numpy as np
import numpy.typing as npt
Expand All @@ -9,7 +12,14 @@


class Gradient:
def __init__(self, optProb: Optimization, sensType: str, sensStep: float = None, sensMode: str = "", comm=None):
def __init__(
self,
optProb: Optimization,
sensType: Literal["fd", "cd", "fdr", "cdr", "cs"],
sensStep: float | complex | None = None,
sensMode: str = "",
comm=None,
):
"""
Gradient class for automatically computing gradients with finite
difference or complex step.
Expand All @@ -20,14 +30,15 @@ def __init__(self, optProb: Optimization, sensType: str, sensStep: float = None,
This is the complete description of the optimization problem.

sensType : str
- ``FD`` for forward difference
- ``CD`` for central difference
- ``FDR`` for forward difference with relative step size
- ``CDR`` for central difference with relative step size
- ``CS`` for complex step
- ``fd`` for forward difference
- ``cd`` for central difference
- ``fdr`` for forward difference with relative step size
- ``cdr`` for central difference with relative step size
- ``cs`` for complex step

sensStep : float
Step size to use for differencing
sensStep : float | complex, optional
Step size to use for differencing. By default ``1e-6`` for ``fd/fdr``, ``1e-4`` for ``cd/cdr``, ``1e-40j`` for ``cs``.
Must be a purely imaginary value for ``cs``.

sensMode : str
Flag to compute gradients in parallel.
Expand All @@ -44,6 +55,18 @@ def __init__(self, optProb: Optimization, sensType: str, sensStep: float = None,
self.sensStep = 1e-40j
else:
self.sensStep = sensStep

if self.sensType == "cs":
# Complex step divides by the imaginary part of the step, so a purely
# real step would silently yield NaN gradients.
if np.imag(self.sensStep) == 0:
raise ValueError(f"The complex step size must have a nonzero imaginary part, got {self.sensStep}.")

# A nonzero real part would perturb x along the real axis as well, corrupting the function
# value used implicitly in the complex-step formula.
if np.real(self.sensStep) != 0:
raise ValueError(f"The complex step size must have a zero real part, got {self.sensStep}.")

self.sensMode = sensMode
self.comm = comm

Expand Down
2 changes: 1 addition & 1 deletion pyoptsparse/pyOpt_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def _clearTimings(self) -> None:
self.userObjCalls = 0
self.userSensCalls = 0

def _setSens(self, sens: str | Callable | None, sensStep: float, sensMode: str) -> None:
def _setSens(self, sens: str | Callable | None, sensStep: float | complex | None, sensMode: str) -> None:
"""
Common function to setup sens function
"""
Expand Down
2 changes: 1 addition & 1 deletion pyoptsparse/testing/pyOpt_testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def get_dict_distance(d, d2):
DEFAULT_OPTIMIZERS = {"SLSQP", "PSQP", "CONMIN", "ALPSO", "NSGA2"}

# Define gradient-based optimizers
GRAD_BASED_OPTIMIZERS = {"CONMIN", "IPOPT", "NLPQLP", "ParOpt", "PSQP", "SLSQP", "SNOPT", "Uno"}
GRAD_BASED_OPTIMIZERS = {"CONMIN", "IPOPT", "NLPQLP", "PSQP", "SLSQP", "SNOPT", "Uno"}


class OptTest(unittest.TestCase):
Expand Down
6 changes: 6 additions & 0 deletions tests/test_gradient.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ def test_scaling(self):
funcsSens, _ = grad(X0, funcs)
assert_sens_matches_analytic(funcsSens, 1e-12)

@parameterized.expand([("real_step", 1e-40), ("nonzero_real_part", 1 + 1e-40j)])
def test_cs_invalid_step_raises(self, _, sensStep):
optProb = build_optProb()
with self.assertRaises(ValueError):
Gradient(optProb, sensType="cs", sensStep=sensStep)


if __name__ == "__main__":
unittest.main()
17 changes: 16 additions & 1 deletion tests/test_sphere.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Test solution of Sphere problem"""

# Standard Python modules
from itertools import product
import unittest

# External modules
Expand All @@ -10,7 +11,7 @@
# First party modules
from pyoptsparse import Optimization
from pyoptsparse.pyOpt_optimizer import Optimizers
from pyoptsparse.testing import OptTest
from pyoptsparse.testing import GRAD_BASED_OPTIMIZERS, OptTest

ALL_OPTIMIZERS = sorted({e.name for e in Optimizers} - {"ParOpt", "NSGA2"})

Expand Down Expand Up @@ -57,6 +58,9 @@ class TestSphere(OptTest):
"maxGen": 100,
"seed": 123,
},
"CONMIN": { # CONMIN diverges when gradient is near zero, here we stop on first optimal iterate
"ITRM": 1,
},
"SNOPT": {
"Major iterations limit": 10,
},
Expand Down Expand Up @@ -101,6 +105,17 @@ def test_optimization(self, optName):
optOptions = self.optOptions.get(optName, {})
self.optimize_with_hotstart(self.tol[optName], optOptions=optOptions)

@parameterized.expand(
product(sorted(GRAD_BASED_OPTIMIZERS), ["fd", "fdr", "cd", "cdr", "cs"]),
name_func=lambda f, n, p: f"{f.__name__}_{p.args[0]}_{p.args[1]}",
)
def test_optimization_approx_deriv(self, optName, sens):

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is the only new test added here, the rest are from the other branch (the diff will look better once the other PR is merged).

self.optName = optName
self.setup_optProb()
optOptions = self.optOptions.get(optName, {})
sol = self.optimize(optOptions=optOptions, sens=sens)
self.assert_solution_allclose(sol, self.tol[optName])

@parameterized.expand(["filtersqp", "funnelsqp"])
def test_uno_presets(self, preset):
self.optName = "Uno"
Expand Down
Loading