Results and Diagnostics#
Use this page once you already have a solve and want to answer a narrower question: what did the solver actually return, and do the objects look economically and numerically sensible?
Read it after Library Quickstart or Getting Started. If you only need exact object members, use API Reference. If you still need derivations or equation-to-code mapping, go back to the BCW walkthroughs first.
The main purpose is to help you answer:
what object did the solver return?
which attributes are always safe to inspect?
what does a “healthy” solution look like numerically?
which symptoms point to modeling errors versus numerical tuning issues?
Most Useful Companions#
Solver Return Types#
In this repository, representative runs produced the following return objects:
Workflow |
Return object |
Typical companion output |
|---|---|---|
|
|
|
|
|
|
|
|
solved grid plus search diagnostics |
|
|
summary DataFrame plus |
Examples:
state, history = solver.solve()
print(type(state).__name__)
print(len(history))
search_state = solver.boundary_search(method="bisection", verbose=False)
print(type(search_state).__name__)
print(search_state.grid.boundary)
The Most Useful Objects#
state.grid#
This is the full solved grid object. For most practical inspection, it is the first thing to grab.
grid = state.grid
Useful members:
grid.boundarygrid.sgrid.vgrid.dvgrid.d2vgrid.policygrid.df
state.df#
Every solver state exposes:
print(state.df.head())
print(state.df.tail())
This is a convenience wrapper for state.grid.df.
Representative solve() output columns in this repository start with:
['s', 'v', 'dv', 'd2v', 'investment']
If your model has more controls, you will see more columns.
history#
For solve() and boundary_update(), the second returned object is a history array.
Interpretation:
it records per-iteration error magnitudes,
it helps distinguish “failed immediately” from “converged slowly,”
it is useful for comparing two configurations.
Do not confuse history with the actual economic solution. It is an iteration diagnostic, not the value function itself.
grid.boundary#
This is the cleanest place to read the solved boundary values:
print(grid.boundary)
For the four BCW repository examples, healthy boundary objects usually follow these patterns:
liquidation:
s_min=0,s_max≈0.22,v_left=0.9,refinancing:
s_min=0,s_max≈0.19,v_left>0.9, with an interior issuance targetm≈0.06,hedging:
s_min=0,s_max≈0.14,v_left>0.9, plus hedge-region cutoffs aroundw_-≈0.07andw_+≈0.11,credit line:
s_min≈-0.2,s_max≈0.08, and a much flatter marginal value aroundw=0.
The point is not exact replication. The point is to recognize the right order of magnitude and the right boundary relationships for each case.
grid.df: Column-By-Column Interpretation#
Column |
Meaning |
Why you should care |
|---|---|---|
|
state grid |
tells you where you are in cash-space |
|
value-capital ratio |
the solved value function |
|
first derivative |
marginal value of cash |
|
second derivative |
curvature and right-boundary contact diagnostic |
|
investment policy |
shows how real decisions vary with cash |
|
hedge policy in hedging case |
shows constrained vs interior vs zero hedge regions |
|
unconstrained hedge policy in hedging case |
helps diagnose where clipping to |
The Three Highest-Value Diagnostics#
If you are short on time, inspect these first:
grid.boundarygrid.df.tail()grid.d2v[-1]
Why these three:
grid.boundarytells you what problem was actually solved,the tail tells you whether the payout-side boundary behavior looks right,
grid.d2v[-1]directly tests the BCW contact condition.
Boundary Diagnostics#
Left boundary#
Ask:
does
v[0]match the intended left-boundary condition?in liquidation, is it near the liquidation value?
in refinancing and hedging, is it higher once issuance is active?
in the credit-line case, does the left boundary live at
s=-cand line up with the issuance condition?
Right boundary#
Ask:
is
dv[-1]approaching the intended payout-side slope?is
d2v[-1]approaching zero?does the last part of the curve approach the boundary smoothly?
For the BCW examples, a healthy tail looks like “close to the expected slope, and curvature numerically vanishing.”
Policy Diagnostics#
Investment#
Typical BCW interpretation:
low cash: investment is sharply reduced or negative,
middle region: investment recovers,
right tail: investment becomes mildly positive.
Case-specific refinements:
refinancing: the recovery in investment should line up with the issuance target
m,credit line: investment can stay positive around
w=0even when the no-credit benchmark is still constrained.
You are looking for a smooth economic pattern, not a perfectly linear curve.
Hedge policy (psi)#
In the hedging case:
low cash:
psishould bind at-pi,middle region: interior values should appear,
high cash:
psishould go to0.
If all three regions are absent, re-check the hedging implementation.
grid.aux: Optional, Not Guaranteed#
grid.aux calls the model’s optional auxiliary(grid) hook.
Important consequence:
if your model does not implement
auxiliary(grid),grid.auxraisesNotImplementedError.
So the safe default diagnostics are:
grid.boundary,grid.df,history,saved
Grid/Grids/SensitivityResult.
Only use grid.aux after your model explicitly defines custom auxiliary outputs.
A robust pattern is to let auxiliary(grid) return a small dictionary of derived summaries, for example:
@staticmethod
def auxiliary(grid: fjb.Grid):
return {"value_mean": jnp.mean(grid.v)}
Sensitivity Analysis Results#
sensitivity_analysis() returns a SensitivityResult:
result = solver.sensitivity_analysis(
method="hybr",
param_name="sigma",
param_values=...,
)
Two objects matter immediately:
result.dfresult.grids
Representative columns from this repository:
['sigma', 'boundary_error', 'converged', 's_min', 's_max', 'v_left', 'v_right']
Interpretation:
result.dfis the continuation summary,result.gridsstores the full solved grid at each parameter value.
Representative grid keys:
[0.08, 0.09]
That means you can inspect both:
how the boundary moved across parameters,
what the full value/policy objects looked like at each point.
Save, Reload, Re-Inspect#
Recommended patterns:
state.grid.save("outputs/liquidation_grid")
grid = fjb.load_grid("outputs/liquidation_grid")
print(grid.df.tail())
result.save("outputs/sigma_result")
loaded = fjb.load_sensitivity_result("outputs/sigma_result")
print(loaded.df)
This is especially useful when your solve is expensive and you want to separate “solving” from “interpreting.”
Symptom -> Likely Cause -> First Action#
Symptom |
Likely cause |
First action |
|---|---|---|
|
wrong boundary target or unstable search |
inspect |
|
inconsistent right boundary |
inspect |
|
unstable policy update or coarse grid |
verify equations before increasing |
|
hedge logic never enters interior region |
inspect clipping and |
|
optional hook not implemented |
ignore |
|
formulation likely wrong, not merely slow |
simplify to a fixed-boundary baseline solve |
A Minimal BCW Diagnostic Script#
grid = state.grid
print(grid.boundary)
print(grid.df.head())
print(grid.df.tail())
print("right slope:", grid.dv[-1])
print("right curvature:", grid.d2v[-1])
If you are unsure where to start, this small block gives the highest information per line of code.
When You Should Stop Tuning And Re-Read The Model#
Stop adjusting tolerances and go back to the equations if:
the solution is economically implausible across the entire grid,
different solvers all fail in similar ways,
the boundary conditions do not match the story of the model,
your diagnostics contradict the paper’s qualitative shape.
That is usually a modeling issue, not a numerical fine-tuning issue.