Skip to content

Add numerical guards for CES with heterogeneous epsilon#1096

Merged
jdebacker merged 4 commits intoPSLmodels:masterfrom
vahid-ahmadi:fix/ces-numerical-guards
Apr 13, 2026
Merged

Add numerical guards for CES with heterogeneous epsilon#1096
jdebacker merged 4 commits intoPSLmodels:masterfrom
vahid-ahmadi:fix/ces-numerical-guards

Conversation

@vahid-ahmadi
Copy link
Copy Markdown
Contributor

Summary

  • Adds _FLOOR = 1e-12 guards in firm.py to prevent 0^(negative) → inf/NaN when CES epsilon ≠ 1
  • Fixes bug in get_MPx: np.any(x) == 0 should be np.any(x == 0) (the original always evaluates to False)
  • Guards applied in get_Y, get_MPx, get_KLratio_KLonly, and solve_L — only in the general CES code paths; Cobb-Douglas (ε=1) is unchanged

Motivation

When epsilon varies across sectors (e.g., Energy=0.50, Real Estate=0.40, Info & Finance=1.20), the CES production function raises K, L, K_g to (epsilon-1)/epsilon. For epsilon < 1, this exponent is negative, so any zero or near-zero input during initial solver iterations produces inf/NaN and crashes the solver.

This was blocking OG-UK from using calibrated sector-specific CES elasticities from Chirinko (2008) and Knoblach et al. (2020). With this fix, OG-UK's 8-sector model with heterogeneous epsilon (0.40–1.30) converges successfully in both SS and TPI.

Test plan

  • Existing OG-Core tests pass (guards only activate when inputs ≈ 0)
  • OG-UK 8-sector SS with heterogeneous epsilon converges (tested, resource constraint ~1e-6)
  • Single-sector (M=1, ε=1.0) results unchanged (guards not activated for Cobb-Douglas)

🤖 Generated with Claude Code

…epsilon

When epsilon differs from 1.0 across sectors, the CES production function
raises K, L, and K_g to (epsilon-1)/epsilon, which is negative for epsilon < 1.
If any input is zero or near-zero (common during initial solver iterations),
this produces inf or NaN, causing the SS and TPI solvers to fail.

Changes:
- Add _FLOOR (1e-12) constant and apply np.maximum() guards before all
  CES power operations in get_Y, get_MPx, get_KLratio_KLonly, and solve_L
- Fix bug in get_MPx: np.any(x) == 0 should be np.any(x == 0)
- Guard denominator in get_KLratio_KLonly CES branch against negative values

These guards only affect the general CES code paths (epsilon != 1); the
Cobb-Douglas special case (epsilon == 1) is unchanged.

Tested with OG-UK 8-sector model using heterogeneous epsilon values
(0.40-1.30) — SS solver now converges successfully.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
vahid-ahmadi added a commit to vahid-ahmadi/OG-UK that referenced this pull request Mar 31, 2026
Two changes:

1. Make single-sector (M=1) the default run mode. The 8-sector industry
   calibration is now opt-in via `multi_sector=True` parameter on
   solve_steady_state() and run_transition_path(), or via the
   `multi-sector` CLI flag:
     uv run python examples/run_oguk.py ss pooled              # M=1
     uv run python examples/run_oguk.py ss pooled multi-sector # M=8

2. Enable calibrated heterogeneous CES elasticities (epsilon) in the
   8-sector mode. Previously forced to 1.0 (Cobb-Douglas) for all
   sectors due to solver NaN issues — now uses literature values from
   Chirinko (2008) and Knoblach et al. (2020):
     Energy=0.50, Construction=0.70, Trade & Transport=1.00,
     Info & Finance=1.20, Real Estate=0.40, Business Services=1.30,
     Public & Other=0.90, Manufacturing=0.80

   Supporting changes:
   - Recalibrate TFP (Z) using CES Solow residuals instead of
     Cobb-Douglas residuals when epsilon != 1
   - Use hybr root-finder (Powell hybrid) instead of LM for
     multi-sector SS — LM gets stuck at ~1e-5 residuals
   - Relax mindist_SS and RC_SS to 1e-4 for multi-sector

   Requires OG-Core PR PSLmodels/OG-Core#1096 (numerical guards for
   CES production functions).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jdebacker
Copy link
Copy Markdown
Member

@vahid-ahmadi Can you merge in upstream changes? And then run make format?

I think tests should pass and this will be set to merge then. Thanks!

vahid-ahmadi and others added 3 commits April 10, 2026 11:32
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The np.maximum(denom, _FLOOR) guard was clamping legitimately negative
denominators to a small positive value, producing wrong KLratio results
when epsilon < 1. Use sign-preserving guard instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vahid-ahmadi
Copy link
Copy Markdown
Contributor Author

Fixed the failing tests (test_get_KLratio_from_r[epsilon=0.5,TP] and test_get_KLratio[epsilon=0.5,TP]).

Root cause: np.maximum(denom, _FLOOR) in get_KLratio_KLonly was clamping legitimately negative denominators to a small positive value. For epsilon=0.5 with certain interest rates, denom goes negative (valid — indicates an economically infeasible region), but the floor was silently flipping it positive, producing near-zero KLratios instead of the correct values.

Fix: Replaced with a sign-preserving guard:

denom = np.where(
    denom >= 0,
    np.maximum(denom, _FLOOR),
    np.minimum(denom, -_FLOOR),
)

This still prevents division by zero but preserves negative denominators.

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Apr 13, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 73.09%. Comparing base (b69f7e9) to head (fdb17de).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1096      +/-   ##
==========================================
- Coverage   73.17%   73.09%   -0.09%     
==========================================
  Files          21       21              
  Lines        5190     5211      +21     
==========================================
+ Hits         3798     3809      +11     
- Misses       1392     1402      +10     
Flag Coverage Δ
unittests 73.09% <100.00%> (-0.09%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
ogcore/firm.py 96.66% <100.00%> (+0.37%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jdebacker
Copy link
Copy Markdown
Member

Thanks for the PR @vahid-ahmadi. Merging.

@jdebacker jdebacker merged commit 85733bb into PSLmodels:master Apr 13, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants