diff --git a/ogcore/TPI.py b/ogcore/TPI.py index 2032d42c3..5a216d711 100644 --- a/ogcore/TPI.py +++ b/ogcore/TPI.py @@ -731,6 +731,36 @@ def inner_loop(guesses, outer_loop_vars, initial_values, ubi, j, ind, p): return euler_errors, b_mat, n_mat +def _rc_error_message(RC_error, RC_TPI): + """ + Build the message for a transition resource-constraint failure. + + Reports the maximum absolute resource-constraint error, the period it + occurs in, and how to read it: a violation confined to the first or last + periods is usually an initial- or terminal-boundary artifact, while one + spread across the path points to an inconsistent calibration. + + Args: + RC_error (array_like): resource constraint error, time on axis 0 + RC_TPI (scalar): the tolerance the error is checked against + + Returns: + msg (str): the diagnostic error message + """ + abs_rc = np.absolute(RC_error) + rc_by_period = abs_rc.reshape(abs_rc.shape[0], -1).max(axis=1) + t_worst = int(np.argmax(rc_by_period)) + return ( + "Transition path equilibrium not found (RC_error): max " + f"|resource constraint error| = {rc_by_period[t_worst]:.2e} at " + f"period {t_worst} of {rc_by_period.shape[0]} (tolerance " + f"RC_TPI = {RC_TPI}). A violation confined to the first or last " + "periods is usually an initial- or terminal-boundary artifact; one " + "spread across the path points to an inconsistent calibration " + "(spending, revenue, debt_ratio_ss)." + ) + + def run_TPI(p, client=None): """ Solve for transition path equilibrium of OG-Core. @@ -1837,9 +1867,7 @@ def run_TPI(p, client=None): raise RuntimeError(msg) if (np.any(np.absolute(RC_error) >= p.RC_TPI)) and ENFORCE_SOLUTION_CHECKS: - raise RuntimeError( - "Transition path equlibrium not found " + "(RC_error)" - ) + raise RuntimeError(_rc_error_message(RC_error, p.RC_TPI)) if ( np.any(np.absolute(eul_savings) >= p.mindist_TPI) diff --git a/tests/test_TPI.py b/tests/test_TPI.py index 2a2b680d1..1cc50567b 100644 --- a/tests/test_TPI.py +++ b/tests/test_TPI.py @@ -258,6 +258,18 @@ def test_get_initial_SS_values(baseline, param_updates, filename, tmpdir): ) +def test_rc_error_message(): + """The resource-constraint failure message names the largest violation, + the period it occurs in, and the tolerance.""" + T, M = 320, 2 + RC_error = np.full((T, M), 1e-8) + RC_error[T - 1, 1] = -0.05 # a terminal-boundary spike (signed) + msg = TPI._rc_error_message(RC_error, 1e-4) + assert "5.00e-02" in msg # the max absolute error, reported + assert f"period {T - 1}" in msg # the period it occurs in + assert "1e-04" in msg or "0.0001" in msg # the tolerance + + def test_firstdoughnutring(): # Test TPI.firstdoughnutring function. Provide inputs to function and # ensure that output returned matches what it has been before.