Skip to content

Commit 4a4d969

Browse files
authored
Update AlgebraicSolver tolerances + default ESOH solver method (#4982)
* update success criterion + tighter tols * tests * change esoh from minimizer to root finder * docs * Update CHANGELOG.md
1 parent 4363962 commit 4a4d969

File tree

5 files changed

+57
-26
lines changed

5 files changed

+57
-26
lines changed

CHANGELOG.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
# [Unreleased](https://github.com/pybamm-team/PyBaMM/)
22

3+
## Bug fixes
4+
5+
- Improve reliablity of `AlgebraicSolver` and change `ElectrodeSOHHalfCell` solver to a Trust-Region method. ([#4982](https://github.com/pybamm-team/PyBaMM/pull/4982))
6+
37
# [v25.4.1](https://github.com/pybamm-team/PyBaMM/tree/v25.4.1) - 2025-04-16
48

59
## Bug fixes
610

7-
- Remove a regularization term in the harmonic mean. ([#4977](https://github.com/pybamm-team/PyBaMM/pull/4977))
11+
- Remove a regularization term in the harmonic mean. ([#4977](https://github.com/pybamm-team/PyBaMM/pull/4977))
812

913
## Breaking changes
1014

src/pybamm/models/full_battery_models/lithium_ion/electrode_soh_half_cell.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def __init__(self, name="ElectrodeSOH model"):
5454
@property
5555
def default_solver(self):
5656
# Use AlgebraicSolver as CasadiAlgebraicSolver gives unnecessary warnings
57-
return pybamm.AlgebraicSolver(method="minimize L-BFGS-B", tol=1e-7)
57+
return pybamm.AlgebraicSolver(method="lsq__trf", tol=1e-7)
5858

5959

6060
def get_initial_stoichiometry_half_cell(

src/pybamm/solvers/algebraic_solver.py

+46-19
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,29 @@ class AlgebraicSolver(pybamm.BaseSolver):
2828
Any options to pass to the rootfinder. Vary depending on which method is chosen.
2929
Please consult `SciPy documentation
3030
<https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.show_options.html>`_
31-
for details.
31+
for details. Addititional options to pass to the solver, by default:
32+
33+
.. code-block:: python
34+
35+
extra_options = {
36+
# Tolerance for termination by the change of the independent variables.
37+
"xtol": 1e-12,
38+
# Tolerance for termination by the norm of the gradient.
39+
"gtol": 1e-12,
40+
}
3241
"""
3342

3443
def __init__(self, method="lm", tol=1e-6, extra_options=None):
3544
super().__init__(method=method)
3645
self.tol = tol
37-
self.extra_options = extra_options or {}
46+
47+
default_extra_options = {
48+
"xtol": 1e-12,
49+
"gtol": 1e-12,
50+
}
51+
extra_options = extra_options or {}
52+
self.extra_options = default_extra_options | extra_options
53+
3854
self.name = f"Algebraic solver ({method})"
3955
self._algebraic_solver = True
4056
pybamm.citations.register("Virtanen2020")
@@ -158,6 +174,7 @@ def jac_fn(y_alg, jac=jac):
158174
**self.extra_options,
159175
)
160176
integration_time += timer.time()
177+
success |= sol.success
161178
# Methods which use minimize are specified as either "minimize",
162179
# which uses the default method, or with "minimize__methodname"
163180
elif self.method.startswith("minimize"):
@@ -183,17 +200,31 @@ def jac_norm(y, jac_fn=jac_fn):
183200
bounds = [
184201
(lb, ub) for lb, ub in zip(model.bounds[0], model.bounds[1])
185202
]
186-
extra_options["bounds"] = bounds
203+
else:
204+
bounds = None
205+
206+
hess = extra_options.get("hess", None)
207+
hessp = extra_options.get("hessp", None)
208+
constraints = extra_options.get("constraints", ())
209+
callback = extra_options.get("callback", None)
210+
187211
timer.reset()
188212
sol = optimize.minimize(
189213
root_norm,
190214
y0_alg,
191215
method=method,
192216
tol=self.tol,
193217
jac=jac_norm,
194-
**extra_options,
218+
bounds=bounds,
219+
hess=hess,
220+
hessp=hessp,
221+
constraints=constraints,
222+
callback=callback,
223+
options=extra_options,
195224
)
196225
integration_time += timer.time()
226+
# The solution.success flag is unreliable, so we ignore
227+
# it and use the norm of the residual instead
197228
else:
198229
timer.reset()
199230
sol = optimize.root(
@@ -205,25 +236,21 @@ def jac_norm(y, jac_fn=jac_fn):
205236
options=self.extra_options,
206237
)
207238
integration_time += timer.time()
239+
# The solution.success flag is unreliable, so we ignore
240+
# it and use the norm of the residual instead
208241

209-
if sol.success and np.all(abs(sol.fun) < self.tol):
210-
# update initial guess for the next iteration
211-
y0_alg = sol.x
212-
# update solution array
242+
y0_alg = sol.x
243+
success |= np.all(abs(sol.fun) < self.tol)
244+
if success:
213245
y_alg[:, idx] = y0_alg
214-
success = True
215-
elif not sol.success:
246+
break
247+
248+
if itr > maxiter:
216249
raise pybamm.SolverError(
217-
f"Could not find acceptable solution: {sol.message}"
250+
"Could not find acceptable solution: solver terminated "
251+
"unsuccessfully and maximum solution error "
252+
f"({np.max(abs(sol.fun))}) above tolerance ({self.tol})"
218253
)
219-
else:
220-
y0_alg = sol.x
221-
if itr > maxiter:
222-
raise pybamm.SolverError(
223-
"Could not find acceptable solution: solver terminated "
224-
"successfully, but maximum solution error "
225-
f"({np.max(abs(sol.fun))}) above tolerance ({self.tol})"
226-
)
227254
itr += 1
228255

229256
# Concatenate differential part

tests/unit/test_solvers/test_algebraic_solver.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def test_algebraic_solver_init(self):
1414
method="hybr", tol=1e-4, extra_options={"maxfev": 100}
1515
)
1616
assert solver.method == "hybr"
17-
assert solver.extra_options == {"maxfev": 100}
17+
assert solver.extra_options == {"xtol": 1e-12, "gtol": 1e-12, "maxfev": 100}
1818
assert solver.tol == 1e-4
1919

2020
solver.method = "krylov"
@@ -78,14 +78,14 @@ def algebraic_eval(self, t, y, inputs):
7878
solver = pybamm.AlgebraicSolver(method="hybr")
7979
with pytest.raises(
8080
pybamm.SolverError,
81-
match="Could not find acceptable solution: The iteration is not making",
81+
match="Could not find acceptable solution",
8282
):
8383
solver._integrate(model, np.array([0]))
8484

8585
solver = pybamm.AlgebraicSolver()
8686
with pytest.raises(
8787
pybamm.SolverError,
88-
match="Could not find acceptable solution: solver terminated",
88+
match="Could not find acceptable solution",
8989
):
9090
solver._integrate(model, np.array([0]))
9191

tests/unit/test_solvers/test_base_solver.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -247,13 +247,13 @@ def algebraic_eval(self, t, y, inputs):
247247

248248
with pytest.raises(
249249
pybamm.SolverError,
250-
match="Could not find acceptable solution: The iteration is not making",
250+
match="Could not find acceptable solution",
251251
):
252252
solver.calculate_consistent_state(Model())
253253
solver = pybamm.BaseSolver(root_method="lm")
254254
with pytest.raises(
255255
pybamm.SolverError,
256-
match="Could not find acceptable solution: solver terminated",
256+
match="Could not find acceptable solution",
257257
):
258258
solver.calculate_consistent_state(Model())
259259
# with casadi

0 commit comments

Comments
 (0)