Troubleshooting#

Use this page when something fails, or when the script runs but the output does not look trustworthy.

If you are still in the first-time setup stage, go back to Installation and Environment. The purpose here is separation and diagnosis, not initial setup.

The best troubleshooting habit in FinHJB is to separate three layers:

  1. environment problems,

  2. modeling mistakes,

  3. numerical configuration problems.

If you mix them together, it becomes hard to tell whether the problem is import-related, equation-related, or solver-related.

Quick Triage#

Symptom

Most likely layer

First page to pair with this one

import fails before solving starts

environment

Installation and Environment

example runs but boundary diagnostics look wrong

numerical workflow

Results and Diagnostics

your custom model raises key or shape errors

model implementation

Modeling Guide

you are unsure whether to use solve, boundary_update, or boundary_search

workflow choice

Solver Guide

Environment Failures#

ModuleNotFoundError: No module named 'finhjb'#

Cause:

  • you are running python directly instead of the project environment,

  • or the package was not installed.

First fix:

uv sync
uv run python -c "import finhjb as fjb; print(fjb.__all__[:5])"

ModuleNotFoundError: No module named 'jax'#

Cause:

  • dependencies are missing in the active environment.

First fix:

uv sync

If you were using plain python, switch to uv run python.

Matplotlib display / backend errors#

Typical symptom:

  • the script imports successfully and then crashes on display or backend setup.

First fix:

export MPLBACKEND=Agg
MPLBACKEND=Agg uv run python src/example/BCW2011Liquidation.py

Loader Errors#

TypeError when using load_grid, load_grids, or load_sensitivity_result#

Cause:

  • the file was saved as one type but loaded with the wrong loader.

Correct mapping:

Saved with

Load with

state.grid.save(path)

load_grid(path)

result.grids.save(path)

load_grids(path)

result.save(path)

load_sensitivity_result(path)

The loaders validate types on load, so a wrong pairing fails loudly by design.

Workflow Selection Errors#

NotImplementedError: Solver.boundary_update() requires the model class to implement update_boundary(grid)#

Cause:

  • boundary_update() was called on a model that does not implement Model.update_boundary(grid).

What to do:

  • use solve() if boundaries are fixed,

  • use boundary_search() if you want a root/search condition,

  • implement update_boundary(grid) only if your model has an explicit outer update rule.

This is not a solver bug. It is the guardrail for the boundary-update workflow.

Boundary Search Problems#

boundary_search() returns but grid.d2v[-1] is not close to zero#

This usually means one of the following:

  1. the target condition is wrong,

  2. the fixed-boundary solve is already unstable,

  3. the search bracket is poor,

  4. the grid is too coarse to resolve the right tail cleanly.

Check in this order:

print(grid.dv[-1], grid.d2v[-1])
print(grid.boundary)

Then ask:

  • does the target in boundary_condition() really encode the desired contact condition?

  • does the fixed-boundary solve behave reasonably before search?

  • for bisection, does the bracket plausibly contain the root?

Bisection does not settle#

Most common causes:

  • the lower and upper bounds are not economically meaningful,

  • the target does not switch sign over the bracket,

  • the model equations are mis-specified, so no sensible root exists in that range.

What to try:

  1. inspect the target value at a few candidate boundaries manually,

  2. narrow the bracket around a region that looks economically plausible,

  3. confirm the base solve() problem is stable before searching.

Policy Iteration Problems#

The run finishes, but policy values look surprising#

Do not assume “surprising” means “wrong.”

For BCW:

  • liquidation investment is strongly negative at low cash,

  • hedging psi is pinned at -pi in distressed states,

  • positive investment only appears toward the healthier right side of the grid.

Before editing the code, compare your pattern to:

Also remember the case-specific patterns:

  • refinancing should raise p(0) above liquidation value and create an interior issuance target m,

  • credit lines can push p'(0) close to 1 and keep investment positive around w=0,

  • frictionless hedging should move the payout boundary left relative to costly margin.

Convergence is slow or unstable#

Things to adjust in order:

  1. verify equations and boundary formulas,

  2. simplify the initial policy guess,

  3. reduce ambition and get a smaller, stable baseline run first,

  4. only then change number, tolerances, or search method.

Changing tolerances before checking the economics usually wastes time.

Derivative and Grid Problems#

dv or d2v looks explosive#

Possible causes:

  • the value function is not being solved consistently,

  • the grid is too coarse in a region with sharp curvature,

  • denominators such as dv or d2v are used unstably in policy formulas,

  • boundaries are inconsistent with the economics.

What to inspect:

df = grid.df
print(df[["s", "v", "dv", "d2v"]].head())
print(df[["s", "v", "dv", "d2v"]].tail())

Questions to ask:

  • is v increasing with cash?

  • is dv[-1] approaching the right-boundary target?

  • is d2v[-1] moving toward zero when it should?

  • are the extreme values confined to the left tail, or are they everywhere?

When should I increase number?#

Increase the grid size after the formulation is already sensible, not before.

Good reason:

  • the curves are qualitatively correct but too jagged.

Bad reason:

  • the model does not converge at all and you have not checked the equations.

policy_guess Confusion#

Why does changing policy_guess affect convergence?#

Because it changes the starting point.

  • policy_guess=True means “use the initializer exactly as the initial policy.”

  • policy_guess=False means “construct a policy container, then immediately push it through an improvement step.”

If your initializer is already strong, True may converge faster. If your initializer is weak or economically poor, False can rescue the run.

grid.aux Raises NotImplementedError#

Cause:

  • Grid.aux calls Model.auxiliary(grid),

  • and your model has not implemented that optional hook.

This is expected behavior.

What to do:

  • ignore grid.aux until you actually need custom diagnostics,

  • or implement auxiliary(grid) in your model.

A Good Minimal Debugging Loop#

When your custom model fails, debug in this order:

  1. uv run python -c "import finhjb" to confirm environment,

  2. get solver.solve() working on fixed boundaries,

  3. inspect state.df.head() and state.df.tail(),

  4. only then add boundary_update() or boundary_search(),

  5. only after that run sensitivity analysis.

This sequence prevents “stacked failures” where multiple moving parts hide the real problem.

When To Stop Debugging And Rethink The Formulation#

Step back and re-check the model if:

  • the right boundary never approaches its intended contact condition,

  • different search methods all fail in similar ways,

  • the policy formulas require dividing by values that routinely approach zero,

  • the solution shape is economically implausible everywhere, not just at one tail.

That usually signals a formulation problem, not a tuning problem.