Skip to content

un_insure.md: harden fsolve calibration against numpy array→scalar strictness (defensive) #347

Description

@mmcky

Summary

This is a defensive / robustness suggestion, not a live bug. The calibration cell in lectures/un_insure.md passes a bare Python scalar as the initial guess to scipy.optimize.fsolve, and its residual function returns array-valued expressions when fsolve hands it a 1-element array. That pattern executes fine on the current scientific stack, but it briefly broke notebook execution downstream under a dependency-version drift, so it is worth hardening.

Where

In lectures/un_insure.md, the autarky calibration uses (around lines 613, 632 and 635):

# inside r_error(self, r)
Vu_star = sp.optimize.fsolve(Vu_error_Λ, 15000, args=(r,))[0]
...
r_calibrated = sp.optimize.brentq(r_error_Λ, 1e-10, 1 - 1e-10)
Vu_aut = sp.optimize.fsolve(Vu_error_Λ, 15000, args=(r_calibrated,))[0]

fsolve internally does x0 = asarray(x0).flatten(), so the iterate Vu arrives at Vu_error as a shape-(1,) array, and the residual is computed as a 1-element array.

Background

The QuantEcon build container (quantecon-build) had its scientific stack unpinned, so it drifted ahead of the anaconda=2025.12 baseline this repo pins. During that drift (≈ March 2026) un_insure.md failed with TypeError: only 0-dimensional arrays can be converted to Python scalars in the brentq → fsolve path, on repos building against the lean container. That is tracked in QuantEcon/actions#28 and the container has now been pinned back to the 2025.12 baseline in QuantEcon/actions#84, which resolves the CI symptom.

The underlying trigger class is real: numpy 2.4.0 promoted converting an ndim > 0 array to a Python scalar (via float() / int()) from a deprecation to a hard TypeError.

Why it is not a live bug

I re-ran the actual calibration cell across four numpy/scipy combinations and it passes on all of them:

numpy scipy result
2.3.5 1.16.3 (anaconda 2025.12)
2.4.0 1.15.3
2.4.0 1.16.3
2.4.6 1.17.1 (anaconda 2026.06)

So the specific breaking combination has since been patched upstream and is no longer reproducible. The code is simply fragile to this class of change.

Suggested hardening

Make the calibration robust to the iterate arriving as a 1-element array and ensure scalars are returned, e.g. normalise the input in Vu_error:

def Vu_error(self, Vu, r):
    β, Ve = self.β, self.Ve
    Vu = np.asarray(Vu).item()          # treat the fsolve iterate as a scalar
    a = max(0.0, invp_prime(1 / (β * (Ve - Vu)), r))
    return u(self, 0) - a + β * (p(a, r) * Ve + (1 - p(a, r)) * Vu) - Vu

(Equivalently, pass x0 as a 1-element array and extract results with float(result[0]) / .item().) I verified this variant produces identical results (Vu_aut ≈ 16758.7, p ≈ 0.1) on all four stacks above.

Priority

Low — defensive only. Good first issue. No action is required for current builds; this just prevents a recurrence if the dependency stack moves again.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions