Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
35a1e42
Add Exponential Spectral Scaling (ESS)
May 23, 2025
3d2eb18
Add ess when using OmnigenousField as Optimizable as well
May 23, 2025
2b30169
Merge branch 'master' into cj/ESS
rahulgaur104 May 24, 2025
ce2f670
Merge branch 'master' into cj/ESS
dpanici May 28, 2025
e6eeffb
Generalize spectral scale creation logic
Aug 1, 2025
b53bd35
Generalize x_scale construction beyond fixed-boundary assumption
Aug 1, 2025
41ea327
Fix x_scale handling for full state vector size and ProximalProjectio…
Aug 1, 2025
102f666
Merge branch 'master' into cj/ESS
dpanici Sep 25, 2025
acf6fec
Merge branch 'master' into cj/ESS
dpanici Oct 1, 2025
be74ab7
Merge branch 'master' into cj/ESS
dpanici Oct 24, 2025
0268745
Merge branch 'master' into cj/ESS
dpanici Nov 3, 2025
67003a9
add changes for ESS - Support ESS for Equilibrium and FourierRZToroid…
Nov 10, 2025
15e55f6
Merge branch 'master' into cj/ESS
dpanici Nov 24, 2025
824c944
Add logic for handling pytree x_scale
f0uriest Dec 3, 2025
e18c7e4
Update changelog
f0uriest Dec 3, 2025
f38ae4b
Merge branch 'rc/xscale' into cj/ESS
f0uriest Dec 3, 2025
cb4a9f0
Add ess helper to optimizable classes
f0uriest Dec 3, 2025
2f0c3d5
Merge branch 'master' into cj/ESS
f0uriest Dec 4, 2025
f6fbf6b
Merge branch 'master' into rc/xscale
f0uriest Dec 4, 2025
8043752
Ensure x_scale matches with order of things in objective
f0uriest Dec 4, 2025
473b80b
fix test
f0uriest Dec 5, 2025
ee78457
Merge branch 'rc/xscale' into cj/ESS
f0uriest Dec 5, 2025
bc6112f
Fix test
f0uriest Dec 6, 2025
5fb25db
Merge branch 'master' into cj/ESS
f0uriest Dec 10, 2025
6e41b61
Fix bug in x_scale for augmented lagrangian methods
f0uriest Dec 10, 2025
b22ccf2
Return unprojected x_scale from optimize method
f0uriest Dec 10, 2025
583f60a
Add test for getting correct ess scale
f0uriest Dec 10, 2025
3effcbd
Merge branch 'master' into cj/ESS
f0uriest Dec 10, 2025
16887e8
Merge branch 'master' into cj/ESS
f0uriest Dec 11, 2025
85b9e63
Add option to combine user x_scale and automatic scaling
f0uriest Dec 11, 2025
7011a46
Clean up docstrings for ess stuff
f0uriest Dec 11, 2025
973d557
Merge branch 'master' into cj/ESS
YigitElma Dec 11, 2025
c8c650a
Merge branch 'cj/ESS' into rc/auto_scale
f0uriest Dec 11, 2025
7710039
Merge branch 'master' into rc/auto_scale
f0uriest Dec 12, 2025
ea13bd1
Update auto scaling API
f0uriest Dec 12, 2025
f4d40c7
Merge branch 'master' into rc/auto_scale
ddudt Dec 15, 2025
f9b87dd
Allow choosing default value for non-ess variables
f0uriest Dec 16, 2025
f585867
Don't terminate on ftol or xtol if last step hit trust region boundary
f0uriest Dec 16, 2025
bc2c8b9
Allow combining automatic x_scale with user x_scale
f0uriest Dec 17, 2025
acf3955
Fix isinstance
f0uriest Dec 17, 2025
36021a1
Revert "Don't terminate on ftol or xtol if last step hit trust region…
f0uriest Dec 17, 2025
95d6300
Merge branch 'master' into rc/auto_scale
f0uriest Dec 17, 2025
641dc21
Fix get_ess test
f0uriest Dec 18, 2025
6330944
Fix projection of x_scale
f0uriest Dec 18, 2025
33644ad
Merge branch 'master' into rc/auto_scale
dpanici Jan 28, 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
61 changes: 35 additions & 26 deletions desc/equilibrium/equilibrium.py
Original file line number Diff line number Diff line change
Expand Up @@ -2243,7 +2243,7 @@ def solve(
stopping tolerances. `None` will use defaults for given optimizer.
maxiter : int
Maximum number of solver steps.
x_scale : array, list[dict | ``'ess'``], ``'ess'`` or ``'auto'``, optional
x_scale : array, list[dict | ``'ess'``, ``'auto'``], ``'ess'`` or ``'auto'``
Characteristic scale of each variable. Setting ``x_scale`` is equivalent
to reformulating the problem in scaled variables ``xs = x / x_scale``.
An alternative view is that the size of a trust region along jth
Expand All @@ -2253,17 +2253,20 @@ def solve(
function. Default is ``'auto'``, which iteratively updates the scale using
the inverse norms of the columns of the Jacobian or Hessian matrix.
If set to ``'ess'``, the scale is set using Exponential Spectral Scaling,
this scaling is set with two parameters, ``ess_alpha`` and ``ess_order``
which are passed through ``options``. ``ess_alpha`` is the decay rate of
the scaling, and ``ess_order`` is the norm order for multi-index modes,
which can be ``1``, ``2``, or ``np.inf``. If not provided in ``options``,
the defaults are: ``ess_alpha=1.2``, ``ess_order=np.inf'`` and
``ess_min_value=1e-7`` (minimum allowed scale value). If an array, should
be the same size as sum(thing.dim_x for thing in things). If a list, the
list should have 1 element for each thing, and each element should either
be ``'ess'`` to use exponential spectral scaling for that thing, or a dict
this scaling is set with parameters, ``ess_alpha``, ``ess_order``,
``ess_min_value`` and ``ess_default`` which are passed through ``options``.
``ess_alpha`` is the decay rate of the scaling, ``ess_order`` is the norm
order for multi-index modes, which can be ``1``, ``2``, or ``np.inf``.
``ess_min_value`` is the minimum allowed scale value, and ``ess_default``
sets the default scale for variables without an ess rule defined.
If not provided in ``options``, the defaults are: ``ess_alpha=1.2``,
``ess_order=np.inf'``, ``ess_min_value=1e-7``, ``ess_default=0.0``.
If an array, should be the same size as sum(thing.dim_x for thing in
things). If a list, the list should have 1 element for each thing, and
each element should either be ``'ess'``, ``'auto'`` to use exponential
spectral scaling or automatic jacobian scaling for that thing, or a dict
with the same keys and dimensions as thing.params_dict to specify scales
manually.
manually. Anywhere ``x_scale==0``, automatic jacobian scaling will be used.
options : dict
Dictionary of additional options to pass to optimizer.
verbose : int
Expand Down Expand Up @@ -2361,7 +2364,7 @@ def optimize(
stopping tolerances. `None` will use defaults for given optimizer.
maxiter : int
Maximum number of solver steps.
x_scale : array, list[dict | ``'ess'``], ``'ess'`` or ``'auto'``, optional
x_scale : array, list[dict | ``'ess'``, ``'auto'``], ``'ess'`` or ``'auto'``
Copy link
Collaborator

Choose a reason for hiding this comment

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

example use to use auto for just lambda:

x_scale = get_ess_scale(eq)

x_scale["L_lmn"] = np.zeros_like(x_scale["L_lmn"])

Characteristic scale of each variable. Setting ``x_scale`` is equivalent
to reformulating the problem in scaled variables ``xs = x / x_scale``.
An alternative view is that the size of a trust region along jth
Expand All @@ -2371,17 +2374,20 @@ def optimize(
function. Default is ``'auto'``, which iteratively updates the scale using
the inverse norms of the columns of the Jacobian or Hessian matrix.
If set to ``'ess'``, the scale is set using Exponential Spectral Scaling,
this scaling is set with two parameters, ``ess_alpha`` and ``ess_order``
which are passed through ``options``. ``ess_alpha`` is the decay rate of
the scaling, and ``ess_order`` is the norm order for multi-index modes,
which can be ``1``, ``2``, or ``np.inf``. If not provided in ``options``,
the defaults are: ``ess_alpha=1.2``, ``ess_order=np.inf'`` and
``ess_min_value=1e-7`` (minimum allowed scale value). If an array, should
be the same size as sum(thing.dim_x for thing in things). If a list, the
list should have 1 element for each thing, and each element should either
be ``'ess'`` to use exponential spectral scaling for that thing, or a dict
this scaling is set with parameters, ``ess_alpha``, ``ess_order``,
``ess_min_value`` and ``ess_default`` which are passed through ``options``.
``ess_alpha`` is the decay rate of the scaling, ``ess_order`` is the norm
order for multi-index modes, which can be ``1``, ``2``, or ``np.inf``.
``ess_min_value`` is the minimum allowed scale value, and ``ess_default``
sets the default scale for variables without an ess rule defined.
If not provided in ``options``, the defaults are: ``ess_alpha=1.2``,
``ess_order=np.inf'``, ``ess_min_value=1e-7``, ``ess_default=0.0``.
If an array, should be the same size as sum(thing.dim_x for thing in
things). If a list, the list should have 1 element for each thing, and
each element should either be ``'ess'``, ``'auto'`` to use exponential
spectral scaling or automatic jacobian scaling for that thing, or a dict
with the same keys and dimensions as thing.params_dict to specify scales
manually.
manually. Anywhere ``x_scale==0``, automatic jacobian scaling will be used.
options : dict
Dictionary of additional options to pass to optimizer.
verbose : int
Expand Down Expand Up @@ -2515,7 +2521,7 @@ def perturb(

return eq

def _get_ess_scale(self, alpha=1.2, order=np.inf, min_value=1e-7):
def _get_ess_scale(self, alpha=1.2, order=np.inf, min_value=1e-7, default=0.0):
"""Create x_scale using exponential spectral scaling.

Parameters
Expand All @@ -2530,16 +2536,19 @@ def _get_ess_scale(self, alpha=1.2, order=np.inf, min_value=1e-7):
Default is 'np.inf'
min_value : float, optional
Minimum allowed scale value. Default is 1e-7
default : float, optional
Default scale for variables that don't have an ess rule defined. 0 means
use automatic jacobian scaling.

Returns
-------
dict of ndarray
Array of scale values for each parameter
"""
# this is the all ones scale:
scales = super()._get_ess_scale(alpha, order, min_value)
bdry_scale = self.surface._get_ess_scale(alpha, order, min_value)
axis_scale = self.axis._get_ess_scale(alpha, order, min_value)
scales = super()._get_ess_scale(alpha, order, min_value, default)
bdry_scale = self.surface._get_ess_scale(alpha, order, min_value, default)
axis_scale = self.axis._get_ess_scale(alpha, order, min_value, default)
# we use ESS for the following:
modes = {
"R_lmn": self.R_basis.modes,
Expand Down
28 changes: 20 additions & 8 deletions desc/geometry/curve.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ def from_values(cls, coords, N=10, NFP=1, sym=False, basis="rpz", name=""):
name=name,
)

def _get_ess_scale(self, alpha=1.2, order=np.inf, min_value=1e-7):
def _get_ess_scale(self, alpha=1.2, order=np.inf, min_value=1e-7, default=0.0):
"""Create x_scale using exponential spectral scaling.

Parameters
Expand All @@ -349,14 +349,17 @@ def _get_ess_scale(self, alpha=1.2, order=np.inf, min_value=1e-7):
Default is 'np.inf'
min_value : float, optional
Minimum allowed scale value. Default is 1e-7
default : float, optional
Default scale for variables that don't have an ess rule defined. 0 means
use automatic jacobian scaling.

Returns
-------
dict of ndarray
Array of scale values for each parameter
"""
# this is the base class scale:
scales = super()._get_ess_scale(alpha, order, min_value)
scales = super()._get_ess_scale(alpha, order, min_value, default)
# we use ESS for the following:
modes = {"R_n": self.R_basis.modes, "Z_n": self.Z_basis.modes}
scales.update(get_ess_scale(modes, alpha, order, min_value))
Expand Down Expand Up @@ -631,7 +634,7 @@ def from_values(cls, coords, N=10, s=None, basis="xyz", name=""):
X_n=X_n, Y_n=Y_n, Z_n=Z_n, modes=basis.modes[:, 2], name=name
)

def _get_ess_scale(self, alpha=1.2, order=np.inf, min_value=1e-7):
def _get_ess_scale(self, alpha=1.2, order=np.inf, min_value=1e-7, default=0.0):
"""Create x_scale using exponential spectral scaling.

Parameters
Expand All @@ -646,14 +649,17 @@ def _get_ess_scale(self, alpha=1.2, order=np.inf, min_value=1e-7):
Default is 'np.inf'
min_value : float, optional
Minimum allowed scale value. Default is 1e-7
default : float, optional
Default scale for variables that don't have an ess rule defined. 0 means
use automatic jacobian scaling.

Returns
-------
dict of ndarray
Array of scale values for each parameter
"""
# this is the base class scale:
scales = super()._get_ess_scale(alpha, order, min_value)
scales = super()._get_ess_scale(alpha, order, min_value, default)
# we use ESS for the following:
modes = {
"X_n": self.X_basis.modes,
Expand Down Expand Up @@ -979,7 +985,7 @@ def from_values(cls, coords, N=10, basis="xyz", name=""):
name=name,
)

def _get_ess_scale(self, alpha=1.2, order=np.inf, min_value=1e-7):
def _get_ess_scale(self, alpha=1.2, order=np.inf, min_value=1e-7, default=0.0):
"""Create x_scale using exponential spectral scaling.

Parameters
Expand All @@ -994,14 +1000,17 @@ def _get_ess_scale(self, alpha=1.2, order=np.inf, min_value=1e-7):
Default is 'np.inf'
min_value : float, optional
Minimum allowed scale value. Default is 1e-7
default : float, optional
Default scale for variables that don't have an ess rule defined. 0 means
use automatic jacobian scaling.

Returns
-------
dict of ndarray
Array of scale values for each parameter
"""
# this is the base class scale:
scales = super()._get_ess_scale(alpha, order, min_value)
scales = super()._get_ess_scale(alpha, order, min_value, default)
# we use ESS for the following:
modes = {"r_n": self.r_basis.modes}
scales.update(get_ess_scale(modes, alpha, order, min_value))
Expand Down Expand Up @@ -1384,7 +1393,7 @@ def from_values(cls, coords, N=10, s=None, basis="xyz", name=""):
name=name,
)

def _get_ess_scale(self, alpha=1.2, order=np.inf, min_value=1e-7):
def _get_ess_scale(self, alpha=1.2, order=np.inf, min_value=1e-7, default=0.0):
"""Create x_scale using exponential spectral scaling.

Parameters
Expand All @@ -1399,14 +1408,17 @@ def _get_ess_scale(self, alpha=1.2, order=np.inf, min_value=1e-7):
Default is 'np.inf'
min_value : float, optional
Minimum allowed scale value. Default is 1e-7
default : float, optional
Default scale for variables that don't have an ess rule defined. 0 means
use automatic jacobian scaling.

Returns
-------
dict of ndarray
Array of scale values for each parameter
"""
# this is the base class scale:
scales = super()._get_ess_scale(alpha, order, min_value)
scales = super()._get_ess_scale(alpha, order, min_value, default)
# we use ESS for the following:
modes = {"X_n": self.X_basis.modes, "Y_n": self.Y_basis.modes}
scales.update(get_ess_scale(modes, alpha, order, min_value))
Expand Down
14 changes: 10 additions & 4 deletions desc/geometry/surface.py
Original file line number Diff line number Diff line change
Expand Up @@ -837,7 +837,7 @@ def get_axis(self):
)
return axis

def _get_ess_scale(self, alpha=1.2, order=np.inf, min_value=1e-7):
def _get_ess_scale(self, alpha=1.2, order=np.inf, min_value=1e-7, default=0.0):
"""Create x_scale using exponential spectral scaling.

Parameters
Expand All @@ -852,14 +852,17 @@ def _get_ess_scale(self, alpha=1.2, order=np.inf, min_value=1e-7):
Default is 'np.inf'
min_value : float, optional
Minimum allowed scale value. Default is 1e-7
default : float, optional
Default scale for variables that don't have an ess rule defined. 0 means
use automatic jacobian scaling.

Returns
-------
dict of ndarray
Array of scale values for each parameter
"""
# this is the base class scale:
scales = super()._get_ess_scale(alpha, order, min_value)
scales = super()._get_ess_scale(alpha, order, min_value, default)
# we use ESS for the following:
modes = {"R_lmn": self.R_basis.modes, "Z_lmn": self.Z_basis.modes}
scales.update(get_ess_scale(modes, alpha, order, min_value))
Expand Down Expand Up @@ -1177,7 +1180,7 @@ def get_axis(self):
axis = FourierRZCurve(R_n=data["R"][0], Z_n=data["Z"][0], sym=self.sym)
return axis

def _get_ess_scale(self, alpha=1.2, order=np.inf, min_value=1e-7):
def _get_ess_scale(self, alpha=1.2, order=np.inf, min_value=1e-7, default=0.0):
"""Create x_scale using exponential spectral scaling.

Parameters
Expand All @@ -1192,14 +1195,17 @@ def _get_ess_scale(self, alpha=1.2, order=np.inf, min_value=1e-7):
Default is 'np.inf'
min_value : float, optional
Minimum allowed scale value. Default is 1e-7
default : float, optional
Default scale for variables that don't have an ess rule defined. 0 means
use automatic jacobian scaling.

Returns
-------
dict of ndarray
Array of scale values for each parameter
"""
# this is the base class scale:
scales = super()._get_ess_scale(alpha, order, min_value)
scales = super()._get_ess_scale(alpha, order, min_value, default)
# we use ESS for the following:
modes = {"R_lmn": self.R_basis.modes, "Z_lmn": self.Z_basis.modes}
scales.update(get_ess_scale(modes, alpha, order, min_value))
Expand Down
7 changes: 5 additions & 2 deletions desc/magnetic_fields/_current_potential.py
Original file line number Diff line number Diff line change
Expand Up @@ -1028,7 +1028,7 @@ def to_CoilSet( # noqa: C901 - FIXME: simplify this
final_coilset = CoilSet(*coils, check_intersection=check_intersection)
return final_coilset

def _get_ess_scale(self, alpha=1.2, order=np.inf, min_value=1e-7):
def _get_ess_scale(self, alpha=1.2, order=np.inf, min_value=1e-7, default=0.0):
"""Create x_scale using exponential spectral scaling.

Parameters
Expand All @@ -1043,14 +1043,17 @@ def _get_ess_scale(self, alpha=1.2, order=np.inf, min_value=1e-7):
Default is 'np.inf'
min_value : float, optional
Minimum allowed scale value. Default is 1e-7
default : float, optional
Default scale for variables that don't have an ess rule defined. 0 means
use automatic jacobian scaling.

Returns
-------
dict of ndarray
Array of scale values for each parameter
"""
# this is the base class scale:
scales = super()._get_ess_scale(alpha, order, min_value)
scales = super()._get_ess_scale(alpha, order, min_value, default)
# we use ESS for the following, the R,Z scales are already in the base class:
modes = {"Phi_mn": self.Phi_basis.modes}
scales.update(get_ess_scale(modes, alpha, order, min_value))
Expand Down
9 changes: 5 additions & 4 deletions desc/objectives/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,17 +121,18 @@ def factorize_linear_constraints(objective, constraint, x_scale="auto"): # noqa
A, b, xp, unfixed_idx, fixed_idx = remove_fixed_parameters(A, b, xp)

# compute x_scale if not provided
# Note: this x_scale is not the same as the x_scale as in solve_options["x_scale"]
# but the one given as solve_options["linear_constraint_options"]["x_scale"]
x0 = objective.x(*objective.things)
auto_x_scale = np.where(np.abs(x0) < 1e2, 1, np.abs(x0))
if x_scale == "auto":
x_scale = objective.x(*objective.things)
x_scale = auto_x_scale
errorif(
x_scale.shape != xp.shape,
ValueError,
"x_scale must be the same size as the full state vector. "
+ f"Got size {x_scale.size} for state vector of size {xp.size}.",
)
D = np.where(np.abs(x_scale) < 1e2, 1, np.abs(x_scale))
# x_scale==0 means use auto scale, otherwise use user scale
D = np.where(x_scale == 0, auto_x_scale, x_scale)

# null space & particular solution
A = A * D[None, unfixed_idx]
Expand Down
14 changes: 10 additions & 4 deletions desc/optimizable.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@
"""
return sorted(set(list(args)))

def _get_ess_scale(self, alpha=1.2, order=np.inf, min_value=1e-7):
def _get_ess_scale(self, alpha=1.2, order=np.inf, min_value=1e-7, default=0.0):
"""Create x_scale using exponential spectral scaling.

Parameters
Expand All @@ -141,6 +141,9 @@
Default is 'np.inf'
min_value : float, optional
Minimum allowed scale value. Default is 1e-7
default : float, optional
Default scale for variables that don't have an ess rule defined. 0 means
use automatic jacobian scaling.

Returns
-------
Expand All @@ -149,7 +152,7 @@
"""
# we don't know anything about the object so just assume scale is all 1s.
# subclasses can implement their own logic.
return tree_map(jnp.ones_like, self.params_dict)
return tree_map(lambda x: default * jnp.ones_like(x), self.params_dict)


class OptimizableCollection(Optimizable):
Expand Down Expand Up @@ -233,7 +236,7 @@
params = [s.unpack_params(xi) for s, xi in zip(self, xs)]
return params

def _get_ess_scale(self, alpha=1.2, order=np.inf, min_value=1e-7):
def _get_ess_scale(self, alpha=1.2, order=np.inf, min_value=1e-7, default=0.0):
"""Create x_scale using exponential spectral scaling.

Parameters
Expand All @@ -248,13 +251,16 @@
Default is 'np.inf'
min_value : float, optional
Minimum allowed scale value. Default is 1e-7
default : float, optional
Default scale for variables that don't have an ess rule defined. 0 means
use automatic jacobian scaling.

Returns
-------
list of dict of ndarray
Array of scale values for each parameter
"""
return [s._get_ess_scale(alpha, order, min_value) for s in self]
return [s._get_ess_scale(alpha, order, min_value, default) for s in self]

Check warning on line 263 in desc/optimizable.py

View check run for this annotation

Codecov / codecov/patch

desc/optimizable.py#L263

Added line #L263 was not covered by tests


def optimizable_parameter(f):
Expand Down
Loading
Loading