bamengine 0.9.1__tar.gz → 0.9.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. {bamengine-0.9.1/src/bamengine.egg-info → bamengine-0.9.2}/PKG-INFO +12 -20
  2. {bamengine-0.9.1 → bamengine-0.9.2}/README.md +11 -19
  3. {bamengine-0.9.1 → bamengine-0.9.2}/pyproject.toml +5 -1
  4. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/__init__.py +1 -1
  5. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/core/relationship.py +2 -2
  6. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/simulation.py +68 -20
  7. {bamengine-0.9.1 → bamengine-0.9.2/src/bamengine.egg-info}/PKG-INFO +12 -20
  8. {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/robustness/internal_validity.py +35 -9
  9. {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/robustness/sensitivity.py +6 -1
  10. {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/robustness/viz.py +6 -5
  11. {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/scenarios/_utils.py +44 -0
  12. {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/scenarios/baseline/targets.yaml +8 -8
  13. {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/scenarios/baseline/viz.py +14 -2
  14. {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/scenarios/growth_plus/viz.py +11 -38
  15. {bamengine-0.9.1 → bamengine-0.9.2}/LICENSE +0 -0
  16. {bamengine-0.9.1 → bamengine-0.9.2}/setup.cfg +0 -0
  17. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/config/__init__.py +0 -0
  18. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/config/default_pipeline.yml +0 -0
  19. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/config/defaults.yml +0 -0
  20. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/config/schema.py +0 -0
  21. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/config/validator.py +0 -0
  22. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/core/__init__.py +0 -0
  23. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/core/agent.py +0 -0
  24. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/core/decorators.py +0 -0
  25. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/core/event.py +0 -0
  26. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/core/pipeline.py +0 -0
  27. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/core/registry.py +0 -0
  28. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/core/role.py +0 -0
  29. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/economy.py +0 -0
  30. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/__init__.py +0 -0
  31. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/_internal/__init__.py +0 -0
  32. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/_internal/bankruptcy.py +0 -0
  33. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/_internal/credit_market.py +0 -0
  34. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/_internal/goods_market.py +0 -0
  35. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/_internal/labor_market.py +0 -0
  36. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/_internal/planning.py +0 -0
  37. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/_internal/production.py +0 -0
  38. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/_internal/revenue.py +0 -0
  39. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/bankruptcy.py +0 -0
  40. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/credit_market.py +0 -0
  41. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/economy_stats.py +0 -0
  42. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/goods_market.py +0 -0
  43. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/labor_market.py +0 -0
  44. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/planning.py +0 -0
  45. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/production.py +0 -0
  46. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/revenue.py +0 -0
  47. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/extension.py +0 -0
  48. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/logging.py +0 -0
  49. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/logging.pyi +0 -0
  50. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/ops.py +0 -0
  51. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/py.typed +0 -0
  52. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/relationships/__init__.py +0 -0
  53. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/relationships/loanbook.py +0 -0
  54. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/results.py +0 -0
  55. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/roles/__init__.py +0 -0
  56. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/roles/borrower.py +0 -0
  57. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/roles/consumer.py +0 -0
  58. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/roles/employer.py +0 -0
  59. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/roles/lender.py +0 -0
  60. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/roles/producer.py +0 -0
  61. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/roles/shareholder.py +0 -0
  62. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/roles/worker.py +0 -0
  63. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/typing.py +0 -0
  64. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/utils.py +0 -0
  65. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine.egg-info/SOURCES.txt +0 -0
  66. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine.egg-info/dependency_links.txt +0 -0
  67. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine.egg-info/requires.txt +0 -0
  68. {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine.egg-info/top_level.txt +0 -0
  69. {bamengine-0.9.1 → bamengine-0.9.2}/src/calibration/__init__.py +0 -0
  70. {bamengine-0.9.1 → bamengine-0.9.2}/src/calibration/__main__.py +0 -0
  71. {bamengine-0.9.1 → bamengine-0.9.2}/src/calibration/analysis.py +0 -0
  72. {bamengine-0.9.1 → bamengine-0.9.2}/src/calibration/cli.py +0 -0
  73. {bamengine-0.9.1 → bamengine-0.9.2}/src/calibration/cost.py +0 -0
  74. {bamengine-0.9.1 → bamengine-0.9.2}/src/calibration/cross_eval.py +0 -0
  75. {bamengine-0.9.1 → bamengine-0.9.2}/src/calibration/grid.py +0 -0
  76. {bamengine-0.9.1 → bamengine-0.9.2}/src/calibration/io.py +0 -0
  77. {bamengine-0.9.1 → bamengine-0.9.2}/src/calibration/morris.py +0 -0
  78. {bamengine-0.9.1 → bamengine-0.9.2}/src/calibration/parameter_space.py +0 -0
  79. {bamengine-0.9.1 → bamengine-0.9.2}/src/calibration/reporting.py +0 -0
  80. {bamengine-0.9.1 → bamengine-0.9.2}/src/calibration/rescreen.py +0 -0
  81. {bamengine-0.9.1 → bamengine-0.9.2}/src/calibration/screening.py +0 -0
  82. {bamengine-0.9.1 → bamengine-0.9.2}/src/calibration/sensitivity.py +0 -0
  83. {bamengine-0.9.1 → bamengine-0.9.2}/src/calibration/stability.py +0 -0
  84. {bamengine-0.9.1 → bamengine-0.9.2}/src/calibration/sweep.py +0 -0
  85. {bamengine-0.9.1 → bamengine-0.9.2}/src/extensions/__init__.py +0 -0
  86. {bamengine-0.9.1 → bamengine-0.9.2}/src/extensions/buffer_stock/__init__.py +0 -0
  87. {bamengine-0.9.1 → bamengine-0.9.2}/src/extensions/buffer_stock/events.py +0 -0
  88. {bamengine-0.9.1 → bamengine-0.9.2}/src/extensions/buffer_stock/role.py +0 -0
  89. {bamengine-0.9.1 → bamengine-0.9.2}/src/extensions/rnd/__init__.py +0 -0
  90. {bamengine-0.9.1 → bamengine-0.9.2}/src/extensions/rnd/events.py +0 -0
  91. {bamengine-0.9.1 → bamengine-0.9.2}/src/extensions/rnd/role.py +0 -0
  92. {bamengine-0.9.1 → bamengine-0.9.2}/src/extensions/taxation/__init__.py +0 -0
  93. {bamengine-0.9.1 → bamengine-0.9.2}/src/extensions/taxation/events.py +0 -0
  94. {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/__init__.py +0 -0
  95. {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/engine.py +0 -0
  96. {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/reporting.py +0 -0
  97. {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/robustness/__init__.py +0 -0
  98. {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/robustness/__main__.py +0 -0
  99. {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/robustness/experiments.py +0 -0
  100. {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/robustness/reference_values.yaml +0 -0
  101. {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/robustness/reporting.py +0 -0
  102. {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/robustness/stats.py +0 -0
  103. {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/robustness/structural.py +0 -0
  104. {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/scenarios/__init__.py +0 -0
  105. {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/scenarios/baseline/__init__.py +0 -0
  106. {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/scenarios/baseline/__main__.py +0 -0
  107. {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/scenarios/buffer_stock/__init__.py +0 -0
  108. {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/scenarios/buffer_stock/__main__.py +0 -0
  109. {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/scenarios/buffer_stock/targets.yaml +0 -0
  110. {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/scenarios/buffer_stock/viz.py +0 -0
  111. {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/scenarios/growth_plus/__init__.py +0 -0
  112. {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/scenarios/growth_plus/__main__.py +0 -0
  113. {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/scenarios/growth_plus/targets.yaml +0 -0
  114. {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/scoring.py +0 -0
  115. {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bamengine
3
- Version: 0.9.1
3
+ Version: 0.9.2
4
4
  Summary: A modular Python framework for the BAM agent-based macroeconomic model
5
5
  Author-email: Kostas Ganitis <ganitiskostas@gmail.com>
6
6
  Maintainer-email: Kostas Ganitis <ganitiskostas@gmail.com>
@@ -83,17 +83,10 @@ Dynamic: license-file
83
83
  <p align="center">
84
84
  A Python implementation of the BAM (Bottom-Up Adaptive Macroeconomics) model
85
85
  from <a href="https://doi.org/10.1007/978-88-470-1971-3"><em>Macroeconomics from the Bottom-up</em></a>
86
- (Delli Gatti et al., 2011). Simulate households, firms, and banks
87
- interacting across labor, credit, and goods markets, where macroeconomic
88
- dynamics emerge from individual agent decisions.
89
- </p>
90
-
91
- <p align="center">
92
- <em>Traditional economics models economies from the top down, assuming
93
- markets automatically reach equilibrium. BAM Engine takes a different approach:
94
- it simulates individual workers, firms, and banks making decisions and
95
- interacting in markets, letting macroeconomic patterns emerge from the
96
- bottom up.</em>
86
+ (Delli Gatti et al., 2011). It runs simulations of individual workers, firms, and
87
+ banks making decisions and interacting in labor, credit and goods markets, letting
88
+ macroeconomic patterns (growth, unemployment, inflation, business cycles) <b>emerge
89
+ from the bottom up</b>, instead of assuming them with aggregate equations.
97
90
  </p>
98
91
 
99
92
  <p align="center">
@@ -172,17 +165,16 @@ See the [Getting Started guide](https://bam-engine.readthedocs.io/en/latest/quic
172
165
 
173
166
  ## Features
174
167
 
175
- - **Complete BAM Model:** Full Chapter 3 implementation: firms, households, and banks interacting across labor, credit, and goods markets
176
- - **ECS Architecture:** Entity-Component-System design separates data (Roles) from behavior (Events) for clean extensibility
177
- - **Vectorized Performance:** All agent operations use NumPy arrays; no Python loops over agents
178
- - **Built-in Extensions:** R&D / Growth+, buffer-stock consumption, and taxation modules
179
- - **Validation Framework:** Three scenario validators with scoring and robustness analysis
180
- - **Calibration Pipeline:** Morris screening, grid search, and tiered stability testing
181
- - **Easy Configuration:** All parameters configurable without code changes via YAML files
168
+ - **Complete BAM Implementation:** Baseline model from *Macroeconomics from the Bottom-up*, Chapter 3 (firms, households, and banks across labor, credit, and goods markets), three built-in extensions (R&D / Growth+, buffer-stock consumption, taxation), and a robustness analysis suite (internal validity, sensitivity, structural experiments).
169
+ - **Vectorized Performance:** Agent state lives in parallel NumPy arrays and behavior is expressed as array transformations, not Python loops over agent objects. Simulations scale to large populations and long horizons.
170
+ - **Pluggable Extension System:** Add custom roles, events, and pipeline hooks via decorators without modifying the engine. The same mechanism powers the built-in extensions.
171
+ - **Parameter Calibration:** Automated pipeline for tuning model parameters against validation targets.
182
172
 
183
173
  ## Architecture
184
174
 
185
- BAM Engine uses an ECS (Entity-Component-System) architecture: agents are lightweight entities, state lives in Role components stored as NumPy arrays, and behavior is defined by Event systems executed via a YAML-configurable pipeline. Custom roles, events, and relationships can be added without modifying core code.
175
+ BAM Engine uses an ECS (Entity-Component-System) architecture: agents are lightweight entities, state lives in **Role** components stored as parallel NumPy arrays, and behavior is defined by **Event** systems composed into a YAML-configurable pipeline. New roles, events, and relationships can be added through decorators, without modifying core code.
176
+
177
+ The trade-off is a mindset shift: you write agent rules as systems that transform whole arrays of state at once, not as methods on per-agent objects. ECS itself does not demand vectorization, but BAM Engine treats it as the default. Every built-in system processes all agents at once, with the goods market's sequential matching rounds as the deliberate exception where strict per-agent ordering matters.
186
178
 
187
179
  See the [User Guide](https://bam-engine.readthedocs.io/en/latest/user_guide/index.html) for a full walkthrough of the model and its architecture.
188
180
 
@@ -13,17 +13,10 @@
13
13
  <p align="center">
14
14
  A Python implementation of the BAM (Bottom-Up Adaptive Macroeconomics) model
15
15
  from <a href="https://doi.org/10.1007/978-88-470-1971-3"><em>Macroeconomics from the Bottom-up</em></a>
16
- (Delli Gatti et al., 2011). Simulate households, firms, and banks
17
- interacting across labor, credit, and goods markets, where macroeconomic
18
- dynamics emerge from individual agent decisions.
19
- </p>
20
-
21
- <p align="center">
22
- <em>Traditional economics models economies from the top down, assuming
23
- markets automatically reach equilibrium. BAM Engine takes a different approach:
24
- it simulates individual workers, firms, and banks making decisions and
25
- interacting in markets, letting macroeconomic patterns emerge from the
26
- bottom up.</em>
16
+ (Delli Gatti et al., 2011). It runs simulations of individual workers, firms, and
17
+ banks making decisions and interacting in labor, credit and goods markets, letting
18
+ macroeconomic patterns (growth, unemployment, inflation, business cycles) <b>emerge
19
+ from the bottom up</b>, instead of assuming them with aggregate equations.
27
20
  </p>
28
21
 
29
22
  <p align="center">
@@ -102,17 +95,16 @@ See the [Getting Started guide](https://bam-engine.readthedocs.io/en/latest/quic
102
95
 
103
96
  ## Features
104
97
 
105
- - **Complete BAM Model:** Full Chapter 3 implementation: firms, households, and banks interacting across labor, credit, and goods markets
106
- - **ECS Architecture:** Entity-Component-System design separates data (Roles) from behavior (Events) for clean extensibility
107
- - **Vectorized Performance:** All agent operations use NumPy arrays; no Python loops over agents
108
- - **Built-in Extensions:** R&D / Growth+, buffer-stock consumption, and taxation modules
109
- - **Validation Framework:** Three scenario validators with scoring and robustness analysis
110
- - **Calibration Pipeline:** Morris screening, grid search, and tiered stability testing
111
- - **Easy Configuration:** All parameters configurable without code changes via YAML files
98
+ - **Complete BAM Implementation:** Baseline model from *Macroeconomics from the Bottom-up*, Chapter 3 (firms, households, and banks across labor, credit, and goods markets), three built-in extensions (R&D / Growth+, buffer-stock consumption, taxation), and a robustness analysis suite (internal validity, sensitivity, structural experiments).
99
+ - **Vectorized Performance:** Agent state lives in parallel NumPy arrays and behavior is expressed as array transformations, not Python loops over agent objects. Simulations scale to large populations and long horizons.
100
+ - **Pluggable Extension System:** Add custom roles, events, and pipeline hooks via decorators without modifying the engine. The same mechanism powers the built-in extensions.
101
+ - **Parameter Calibration:** Automated pipeline for tuning model parameters against validation targets.
112
102
 
113
103
  ## Architecture
114
104
 
115
- BAM Engine uses an ECS (Entity-Component-System) architecture: agents are lightweight entities, state lives in Role components stored as NumPy arrays, and behavior is defined by Event systems executed via a YAML-configurable pipeline. Custom roles, events, and relationships can be added without modifying core code.
105
+ BAM Engine uses an ECS (Entity-Component-System) architecture: agents are lightweight entities, state lives in **Role** components stored as parallel NumPy arrays, and behavior is defined by **Event** systems composed into a YAML-configurable pipeline. New roles, events, and relationships can be added through decorators, without modifying core code.
106
+
107
+ The trade-off is a mindset shift: you write agent rules as systems that transform whole arrays of state at once, not as methods on per-agent objects. ECS itself does not demand vectorization, but BAM Engine treats it as the default. Every built-in system processes all agents at once, with the goods market's sequential matching rounds as the deliberate exception where strict per-agent ordering matters.
116
108
 
117
109
  See the [User Guide](https://bam-engine.readthedocs.io/en/latest/user_guide/index.html) for a full walkthrough of the model and its architecture.
118
110
 
@@ -241,7 +241,11 @@ line-ending = "auto"
241
241
  docstring-code-format = true
242
242
 
243
243
  [tool.mypy]
244
- python_version = "3.11"
244
+ # 3.12 (not the 3.11 floor) so mypy can parse PEP 695 `type` statements that
245
+ # numpy >= 2.5 ships in its stubs; runtime 3.11 support is guarded by the test
246
+ # matrix. The codebase uses `from __future__ import annotations` and no
247
+ # 3.12-only runtime syntax, so this does not weaken our own checks.
248
+ python_version = "3.12"
245
249
  strict = true
246
250
  files = ["src/"]
247
251
  disallow_untyped_decorators = false # Allow @event, @role decorators
@@ -166,7 +166,7 @@ Notes
166
166
 
167
167
  from __future__ import annotations
168
168
 
169
- __version__: str = "0.9.1"
169
+ __version__: str = "0.9.2"
170
170
 
171
171
  # ============================================================================
172
172
  # Standard library imports
@@ -212,7 +212,7 @@ class Relationship(
212
212
  Idx1D
213
213
  Array of edge indices where source_ids == source_id
214
214
  """
215
- return np.where(self.source_ids[: self.size] == source_id)[0]
215
+ return np.flatnonzero(self.source_ids[: self.size] == source_id)
216
216
 
217
217
  def query_targets(self, target_id: int) -> Idx1D:
218
218
  """
@@ -228,7 +228,7 @@ class Relationship(
228
228
  Idx1D
229
229
  Array of edge indices where target_ids == target_id
230
230
  """
231
- return np.where(self.target_ids[: self.size] == target_id)[0]
231
+ return np.flatnonzero(self.target_ids[: self.size] == target_id)
232
232
 
233
233
  def aggregate_by_source(
234
234
  self,
@@ -62,7 +62,7 @@ from collections.abc import Mapping
62
62
  from dataclasses import dataclass, field
63
63
  from importlib import resources
64
64
  from pathlib import Path
65
- from typing import TYPE_CHECKING, Any, Literal
65
+ from typing import TYPE_CHECKING, Any, Literal, get_args
66
66
 
67
67
  import numpy as np
68
68
  import yaml
@@ -92,7 +92,7 @@ from bamengine.roles import (
92
92
  from bamengine.relationships import LoanBook # Must import after roles
93
93
 
94
94
  # isort: on
95
- from bamengine.typing import Float1D
95
+ from bamengine.typing import Bool1D, Float1D, Int1D
96
96
 
97
97
  __all__ = ["Simulation"]
98
98
 
@@ -110,6 +110,36 @@ _ANNOTATION_DTYPE: dict[str, tuple[type[np.generic], int]] = {
110
110
  "Idx1D": (np.intp, -1),
111
111
  }
112
112
 
113
+ # Resolved type-alias objects → (numpy dtype, fill value), used when annotations
114
+ # are real objects (no ``from __future__ import annotations`` in the role's module).
115
+ # Keying on the alias objects is stable across numpy versions, unlike the internal
116
+ # ``__args__`` layout of ``NDArray[...]`` (which changed in numpy 2.5). Idx1D is
117
+ # intentionally absent: it equals Int1D on 64-bit platforms (np.intp is np.int64),
118
+ # so a resolved agent-id field maps to (int, 0); the -1 sentinel is reachable via
119
+ # the Agent class or string annotations.
120
+ _ALIAS_DTYPE: dict[Any, tuple[type[np.generic], int]] = {
121
+ Float1D: (np.float64, 0),
122
+ Int1D: (np.int64, 0),
123
+ Bool1D: (np.bool_, 0),
124
+ }
125
+
126
+
127
+ def _np_scalar_from_annotation(ann: Any) -> type[np.generic] | None:
128
+ """Extract the numpy scalar type from an ``NDArray[...]`` annotation.
129
+
130
+ Robust to the numpy 2.5 layout change where ``NDArray[np.int64].__args__``
131
+ became ``(np.int64,)`` instead of ``(Any, np.dtype[np.int64])``. Returns
132
+ ``None`` when no numpy scalar can be found.
133
+ """
134
+ for arg in get_args(ann):
135
+ if isinstance(arg, type) and issubclass(arg, np.generic):
136
+ return arg # numpy >= 2.5: NDArray[X] -> (X,)
137
+ for inner in get_args(arg):
138
+ if isinstance(inner, type) and issubclass(inner, np.generic):
139
+ return inner # numpy <= 2.4: NDArray[X] -> (Any, np.dtype[X])
140
+ return None
141
+
142
+
113
143
  log = logging.getLogger(__name__)
114
144
 
115
145
 
@@ -1637,33 +1667,51 @@ class Simulation:
1637
1667
  def _resolve_annotation_dtype(ann: Any) -> tuple[type[np.generic], int]:
1638
1668
  """Resolve a role field annotation to (numpy dtype, fill value).
1639
1669
 
1640
- Handles string annotations (from ``__future__`` annotations),
1641
- resolved ``GenericAlias`` / type objects, and the Agent class.
1642
- Agent/Idx1D annotations use -1 fill (unassigned sentinel);
1643
- all others use 0.
1670
+ Handles string annotations (from ``__future__`` annotations), the
1671
+ Agent class, the known resolved type aliases, and arbitrary
1672
+ ``NDArray[np.<scalar>]`` objects. Agent (class / string) annotations
1673
+ use -1 fill (unassigned sentinel); all others use 0.
1674
+
1675
+ Raises
1676
+ ------
1677
+ TypeError
1678
+ If the annotation cannot be mapped to a numpy dtype. Failing
1679
+ loudly avoids silently materialising the wrong dtype (e.g. an
1680
+ ``Int`` field becoming float64), which is what a quiet fallback
1681
+ did before numpy 2.5 changed ``NDArray`` introspection.
1644
1682
  """
1645
1683
  # String annotations (e.g., 'Float', 'Int1D')
1646
1684
  if isinstance(ann, str):
1647
- return _ANNOTATION_DTYPE.get(ann, (np.float64, 0))
1685
+ try:
1686
+ return _ANNOTATION_DTYPE[ann]
1687
+ except KeyError:
1688
+ raise TypeError(
1689
+ f"Cannot resolve a numpy dtype for role field annotation "
1690
+ f"{ann!r}. Use Float, Int, Bool, Agent, or an "
1691
+ f"NDArray[np.<scalar>] annotation."
1692
+ ) from None
1648
1693
 
1649
- # Agent class (bamengine.core.agent.Agent) — special case
1694
+ # Agent class (bamengine.core.agent.Agent) — unassigned sentinel -1
1650
1695
  from bamengine.core.agent import Agent as AgentCls
1651
1696
 
1652
1697
  if ann is AgentCls:
1653
1698
  return np.intp, -1
1654
1699
 
1655
- # Resolved GenericAlias: NDArray[np.float64] extract inner dtype
1656
- args: tuple[Any, ...] = getattr(ann, "__args__", ())
1657
- if len(args) >= 2:
1658
- inner_args: tuple[Any, ...] = getattr(args[1], "__args__", ())
1659
- if (
1660
- inner_args
1661
- and isinstance(inner_args[0], type)
1662
- and issubclass(inner_args[0], np.generic)
1663
- ):
1664
- return inner_args[0], 0
1665
-
1666
- return np.float64, 0 # pragma: no cover - default fallback
1700
+ # Known resolved type aliases (Float1D/Int1D/Bool1D and their friendly
1701
+ # aliases) — version-independent, no NDArray introspection needed.
1702
+ resolved = _ALIAS_DTYPE.get(ann)
1703
+ if resolved is not None:
1704
+ return resolved
1705
+
1706
+ # Arbitrary NDArray[np.<scalar>] — introspect robustly across numpy.
1707
+ scalar = _np_scalar_from_annotation(ann)
1708
+ if scalar is not None:
1709
+ return scalar, 0
1710
+
1711
+ raise TypeError(
1712
+ f"Cannot resolve a numpy dtype for role field annotation {ann!r}. "
1713
+ f"Use Float, Int, Bool, Agent, or an NDArray[np.<scalar>] annotation."
1714
+ )
1667
1715
 
1668
1716
  def get_event(self, name: str) -> Any:
1669
1717
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bamengine
3
- Version: 0.9.1
3
+ Version: 0.9.2
4
4
  Summary: A modular Python framework for the BAM agent-based macroeconomic model
5
5
  Author-email: Kostas Ganitis <ganitiskostas@gmail.com>
6
6
  Maintainer-email: Kostas Ganitis <ganitiskostas@gmail.com>
@@ -83,17 +83,10 @@ Dynamic: license-file
83
83
  <p align="center">
84
84
  A Python implementation of the BAM (Bottom-Up Adaptive Macroeconomics) model
85
85
  from <a href="https://doi.org/10.1007/978-88-470-1971-3"><em>Macroeconomics from the Bottom-up</em></a>
86
- (Delli Gatti et al., 2011). Simulate households, firms, and banks
87
- interacting across labor, credit, and goods markets, where macroeconomic
88
- dynamics emerge from individual agent decisions.
89
- </p>
90
-
91
- <p align="center">
92
- <em>Traditional economics models economies from the top down, assuming
93
- markets automatically reach equilibrium. BAM Engine takes a different approach:
94
- it simulates individual workers, firms, and banks making decisions and
95
- interacting in markets, letting macroeconomic patterns emerge from the
96
- bottom up.</em>
86
+ (Delli Gatti et al., 2011). It runs simulations of individual workers, firms, and
87
+ banks making decisions and interacting in labor, credit and goods markets, letting
88
+ macroeconomic patterns (growth, unemployment, inflation, business cycles) <b>emerge
89
+ from the bottom up</b>, instead of assuming them with aggregate equations.
97
90
  </p>
98
91
 
99
92
  <p align="center">
@@ -172,17 +165,16 @@ See the [Getting Started guide](https://bam-engine.readthedocs.io/en/latest/quic
172
165
 
173
166
  ## Features
174
167
 
175
- - **Complete BAM Model:** Full Chapter 3 implementation: firms, households, and banks interacting across labor, credit, and goods markets
176
- - **ECS Architecture:** Entity-Component-System design separates data (Roles) from behavior (Events) for clean extensibility
177
- - **Vectorized Performance:** All agent operations use NumPy arrays; no Python loops over agents
178
- - **Built-in Extensions:** R&D / Growth+, buffer-stock consumption, and taxation modules
179
- - **Validation Framework:** Three scenario validators with scoring and robustness analysis
180
- - **Calibration Pipeline:** Morris screening, grid search, and tiered stability testing
181
- - **Easy Configuration:** All parameters configurable without code changes via YAML files
168
+ - **Complete BAM Implementation:** Baseline model from *Macroeconomics from the Bottom-up*, Chapter 3 (firms, households, and banks across labor, credit, and goods markets), three built-in extensions (R&D / Growth+, buffer-stock consumption, taxation), and a robustness analysis suite (internal validity, sensitivity, structural experiments).
169
+ - **Vectorized Performance:** Agent state lives in parallel NumPy arrays and behavior is expressed as array transformations, not Python loops over agent objects. Simulations scale to large populations and long horizons.
170
+ - **Pluggable Extension System:** Add custom roles, events, and pipeline hooks via decorators without modifying the engine. The same mechanism powers the built-in extensions.
171
+ - **Parameter Calibration:** Automated pipeline for tuning model parameters against validation targets.
182
172
 
183
173
  ## Architecture
184
174
 
185
- BAM Engine uses an ECS (Entity-Component-System) architecture: agents are lightweight entities, state lives in Role components stored as NumPy arrays, and behavior is defined by Event systems executed via a YAML-configurable pipeline. Custom roles, events, and relationships can be added without modifying core code.
175
+ BAM Engine uses an ECS (Entity-Component-System) architecture: agents are lightweight entities, state lives in **Role** components stored as parallel NumPy arrays, and behavior is defined by **Event** systems composed into a YAML-configurable pipeline. New roles, events, and relationships can be added through decorators, without modifying core code.
176
+
177
+ The trade-off is a mindset shift: you write agent rules as systems that transform whole arrays of state at once, not as methods on per-agent objects. ECS itself does not demand vectorization, but BAM Engine treats it as the default. Every built-in system processes all agents at once, with the goods market's sequential matching rounds as the deliberate exception where strict per-agent ordering matters.
186
178
 
187
179
  See the [User Guide](https://bam-engine.readthedocs.io/en/latest/user_guide/index.html) for a full walkthrough of the model and its architecture.
188
180
 
@@ -330,8 +330,34 @@ def _analyze_seed(
330
330
  ) -> SeedAnalysis:
331
331
  """Compute co-movements, AR fit, and summary stats for one seed."""
332
332
  if ts.collapsed:
333
- # Return a minimal result for collapsed simulations
333
+ # Compute basic scalar stats from pre-collapse data when enough
334
+ # post-burn-in data exists. This lets entry-experiment figures
335
+ # show degradation even when most/all seeds collapse.
334
336
  n_lags = 2 * max_lag + 1
337
+ bi = burn_in
338
+ post_bi_len = len(ts.unemployment) - bi
339
+
340
+ if post_bi_len >= 10:
341
+ unemp_ss = ts.unemployment[bi:]
342
+ gdp_growth_ss = ts.gdp_growth[max(bi - 1, 0) :]
343
+ unemployment_mean = float(np.nanmean(unemp_ss))
344
+ unemployment_std = float(np.nanstd(unemp_ss))
345
+ inflation_mean = float(np.nanmean(ts.inflation[bi:]))
346
+ inflation_std = float(np.nanstd(ts.inflation[bi:]))
347
+ gdp_growth_mean = float(np.nanmean(gdp_growth_ss))
348
+ gdp_growth_std = float(np.nanstd(gdp_growth_ss))
349
+ real_wage_mean = float(np.nanmean(ts.real_wage[bi:]))
350
+ productivity_mean = float(np.nanmean(ts.avg_productivity[bi:]))
351
+ else:
352
+ unemployment_mean = np.nan
353
+ unemployment_std = np.nan
354
+ inflation_mean = np.nan
355
+ inflation_std = np.nan
356
+ gdp_growth_mean = np.nan
357
+ gdp_growth_std = np.nan
358
+ real_wage_mean = np.nan
359
+ productivity_mean = np.nan
360
+
335
361
  return SeedAnalysis(
336
362
  seed=ts.seed,
337
363
  collapsed=True,
@@ -340,14 +366,14 @@ def _analyze_seed(
340
366
  ar_order=ar_order,
341
367
  ar_r_squared=0.0,
342
368
  irf=np.zeros(irf_periods),
343
- unemployment_mean=np.nan,
344
- unemployment_std=np.nan,
345
- inflation_mean=np.nan,
346
- inflation_std=np.nan,
347
- gdp_growth_mean=np.nan,
348
- gdp_growth_std=np.nan,
349
- real_wage_mean=np.nan,
350
- productivity_mean=np.nan,
369
+ unemployment_mean=unemployment_mean,
370
+ unemployment_std=unemployment_std,
371
+ inflation_mean=inflation_mean,
372
+ inflation_std=inflation_std,
373
+ gdp_growth_mean=gdp_growth_mean,
374
+ gdp_growth_std=gdp_growth_std,
375
+ real_wage_mean=real_wage_mean,
376
+ productivity_mean=productivity_mean,
351
377
  phillips_corr=np.nan,
352
378
  okun_corr=np.nan,
353
379
  beveridge_corr=np.nan,
@@ -174,8 +174,13 @@ def _aggregate_seed_analyses(
174
174
 
175
175
  stats_dict: dict[str, dict[str, float]] = {}
176
176
  for attr_name in stat_fields:
177
+ # Use all seeds (not just valid) so that collapsed/degenerate
178
+ # seeds with pre-collapse data contribute to scalar stats.
179
+ # The NaN filter handles seeds that have no usable data.
177
180
  values = [
178
- getattr(a, attr_name) for a in valid if not np.isnan(getattr(a, attr_name))
181
+ getattr(a, attr_name)
182
+ for a in seed_analyses
183
+ if not np.isnan(getattr(a, attr_name))
179
184
  ]
180
185
  if values:
181
186
  mean_val = float(np.mean(values))
@@ -345,16 +345,17 @@ def plot_pa_gdp_comparison(
345
345
  output_dir.mkdir(parents=True, exist_ok=True)
346
346
 
347
347
  # Use log_gdp from the PA-off seed analysis and baseline for comparison
348
- log_gdp_off = pa_result.pa_off_validity.seed_analyses[seed].log_gdp
348
+ burn_in = pa_result.pa_off_validity.burn_in
349
+ log_gdp_off = pa_result.pa_off_validity.seed_analyses[seed].log_gdp[burn_in:]
349
350
  if pa_result.baseline_validity is not None:
350
- log_gdp_on = pa_result.baseline_validity.seed_analyses[seed].log_gdp
351
+ log_gdp_on = pa_result.baseline_validity.seed_analyses[seed].log_gdp[burn_in:]
351
352
  else:
352
353
  raise ValueError(
353
354
  "PA GDP comparison requires baseline; re-run with include_baseline=True"
354
355
  )
355
356
 
356
- fig, ax = plt.subplots(1, 1, figsize=(12, 5))
357
- periods = np.arange(len(log_gdp_on))
357
+ fig, ax = plt.subplots(1, 1, figsize=(8, 6))
358
+ periods = np.arange(burn_in, burn_in + len(log_gdp_on))
358
359
  ax.plot(periods, log_gdp_on, "b-", linewidth=1, alpha=0.8, label="PA on")
359
360
  ax.plot(
360
361
  periods[: len(log_gdp_off)],
@@ -507,7 +508,7 @@ def plot_entry_comparison(
507
508
  ]
508
509
  collapse_rates = [vr.collapse_rate for vr in vrs]
509
510
 
510
- fig, axes = plt.subplots(1, 3, figsize=(14, 5))
511
+ fig, axes = plt.subplots(1, 3, figsize=(10, 7))
511
512
  fig.suptitle(
512
513
  "Entry Neutrality: Impact of Profit Taxation (Section 3.10.2)",
513
514
  fontsize=13,
@@ -2,9 +2,53 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from typing import TYPE_CHECKING
6
+
5
7
  import numpy as np
6
8
  from numpy.typing import NDArray
7
9
 
10
+ if TYPE_CHECKING:
11
+ from matplotlib.axes import Axes
12
+
13
+
14
+ def shade_beyond_extreme(
15
+ ax: Axes, extreme_min: float, extreme_max: float, axis: str = "y"
16
+ ) -> None:
17
+ """Shade areas beyond extreme bounds darker than the transition zone.
18
+
19
+ Creates a visual hierarchy:
20
+ - Normal zone (white): within normal bounds
21
+ - Transition zone (alpha=0.1 red): between extreme and normal bounds
22
+ - Beyond extreme (alpha=0.25 red): outside extreme bounds
23
+
24
+ Must be called AFTER all data is plotted so axis limits are set by the data.
25
+
26
+ Parameters
27
+ ----------
28
+ ax : Axes
29
+ Matplotlib axes to shade.
30
+ extreme_min, extreme_max : float
31
+ Extreme bound values.
32
+ axis : str
33
+ ``"y"`` for horizontal bands, ``"x"`` for vertical bands.
34
+ """
35
+ alpha = 0.25
36
+ color = "red"
37
+ if axis == "y":
38
+ ymin, ymax = ax.get_ylim()
39
+ if ymin < extreme_min:
40
+ ax.axhspan(ymin, extreme_min, alpha=alpha, color=color, zorder=0)
41
+ if ymax > extreme_max:
42
+ ax.axhspan(extreme_max, ymax, alpha=alpha, color=color, zorder=0)
43
+ ax.set_ylim(ymin, ymax)
44
+ else:
45
+ xmin, xmax = ax.get_xlim()
46
+ if xmin < extreme_min:
47
+ ax.axvspan(xmin, extreme_min, alpha=alpha, color=color, zorder=0)
48
+ if xmax > extreme_max:
49
+ ax.axvspan(extreme_max, xmax, alpha=alpha, color=color, zorder=0)
50
+ ax.set_xlim(xmin, xmax)
51
+
8
52
 
9
53
  def compute_detrended_correlation(
10
54
  x: NDArray[np.floating], y: NDArray[np.floating]
@@ -34,10 +34,10 @@ metadata:
34
34
  time_series:
35
35
  unemployment_rate:
36
36
  targets:
37
- normal_min: 0.03
38
- normal_max: 0.08
39
- extreme_min: 0.02
40
- extreme_max: 0.12
37
+ normal_min: 0.02
38
+ normal_max: 0.12
39
+ extreme_min: 0.00
40
+ extreme_max: 0.15
41
41
  mean_target: 0.065 # Book Figure 3.2b extracted
42
42
 
43
43
  inflation_rate:
@@ -50,10 +50,10 @@ metadata:
50
50
 
51
51
  log_gdp:
52
52
  targets:
53
- normal_min: 5.438
54
- normal_max: 5.491
55
- extreme_min: 5.394
56
- extreme_max: 5.501
53
+ normal_min: 5.394
54
+ normal_max: 5.501
55
+ extreme_min: 5.359
56
+ extreme_max: 5.521
57
57
  mean_target: 5.460
58
58
 
59
59
  real_wage:
@@ -22,6 +22,7 @@ import numpy as np
22
22
  from scipy.stats import skew
23
23
 
24
24
  from bamengine import ops
25
+ from validation.scenarios._utils import shade_beyond_extreme
25
26
  from validation.scenarios.baseline import BaselineMetrics
26
27
  from validation.scoring import STATUS_COLORS, check_range
27
28
 
@@ -174,7 +175,6 @@ def visualize_baseline_results(
174
175
  bounds["log_gdp"]["normal_min"],
175
176
  alpha=0.1,
176
177
  color="red",
177
- label="Extreme zone",
178
178
  )
179
179
  ax.axhspan(
180
180
  bounds["log_gdp"]["normal_max"],
@@ -207,7 +207,7 @@ def visualize_baseline_results(
207
207
  ax.set_ylabel("Log output")
208
208
  ax.set_xlabel("t")
209
209
  ax.grid(True, linestyle="--", alpha=0.3)
210
- ax.legend(loc="upper left", fontsize=7)
210
+ ax.legend(loc="lower left", fontsize=7)
211
211
  # Stats box at lower right to avoid legend overlap
212
212
  b = bounds["log_gdp"]
213
213
  actual_mean = np.mean(log_gdp)
@@ -225,6 +225,7 @@ def visualize_baseline_results(
225
225
  horizontalalignment="right",
226
226
  bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.5),
227
227
  )
228
+ shade_beyond_extreme(ax, b["extreme_min"], b["extreme_max"])
228
229
 
229
230
  # Panel (0,1): Unemployment Rate
230
231
  ax = axes[0, 1]
@@ -269,6 +270,11 @@ def visualize_baseline_results(
269
270
  ax.grid(True, linestyle="--", alpha=0.3)
270
271
  ax.set_ylim(bottom=0)
271
272
  add_stats_box(ax, unemployment_pct, "unemployment", is_pct=True)
273
+ shade_beyond_extreme(
274
+ ax,
275
+ bounds["unemployment"]["extreme_min"] * 100,
276
+ bounds["unemployment"]["extreme_max"] * 100,
277
+ )
272
278
 
273
279
  # Panel (1,0): Annual Inflation Rate
274
280
  # NOTE: Unlike other panels, inflation shows ALL periods (no burn-in) with x-axis in years,
@@ -319,6 +325,11 @@ def visualize_baseline_results(
319
325
  ax.grid(True, linestyle="--", alpha=0.3)
320
326
  # Stats box uses full-period data (matching the plot and validation metrics)
321
327
  add_stats_box(ax, inflation_full_pct, "inflation", is_pct=True)
328
+ shade_beyond_extreme(
329
+ ax,
330
+ bounds["inflation"]["extreme_min"] * 100,
331
+ bounds["inflation"]["extreme_max"] * 100,
332
+ )
322
333
 
323
334
  # Panel (1,1): Productivity and Real Wage Co-movement (two-line plot)
324
335
  ax = axes[1, 1]
@@ -377,6 +388,7 @@ def visualize_baseline_results(
377
388
  horizontalalignment="left",
378
389
  bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.5),
379
390
  )
391
+ shade_beyond_extreme(ax, b["extreme_min"], b["extreme_max"])
380
392
 
381
393
  # Bottom 2x2: Macroeconomic curves
382
394
  # --------------------------------
@@ -22,6 +22,7 @@ import numpy as np
22
22
  from scipy.stats import skew
23
23
 
24
24
  from bamengine import ops
25
+ from validation.scenarios._utils import shade_beyond_extreme
25
26
  from validation.scenarios.growth_plus import GrowthPlusMetrics
26
27
  from validation.scoring import (
27
28
  STATUS_COLORS,
@@ -77,34 +78,6 @@ def _save_panels(fig, axes, output_dir, panel_names, dpi=150):
77
78
  print(f"Saved {len(panel_names)} panels + combined figure to {output_dir}/")
78
79
 
79
80
 
80
- def _shade_beyond_extreme(ax, extreme_min, extreme_max, axis="y"):
81
- """Shade areas beyond extreme bounds darker than the transition zone.
82
-
83
- Creates a visual hierarchy:
84
- - Normal zone (white): within normal bounds
85
- - Transition zone (alpha=0.1 red): between extreme and normal bounds
86
- - Beyond extreme (alpha=0.25 red): outside extreme bounds — clearly dangerous
87
-
88
- Must be called AFTER all data is plotted so axis limits are set by the data.
89
- """
90
- alpha = 0.25
91
- color = "red"
92
- if axis == "y":
93
- ymin, ymax = ax.get_ylim()
94
- if ymin < extreme_min:
95
- ax.axhspan(ymin, extreme_min, alpha=alpha, color=color, zorder=0)
96
- if ymax > extreme_max:
97
- ax.axhspan(extreme_max, ymax, alpha=alpha, color=color, zorder=0)
98
- ax.set_ylim(ymin, ymax)
99
- else:
100
- xmin, xmax = ax.get_xlim()
101
- if xmin < extreme_min:
102
- ax.axvspan(xmin, extreme_min, alpha=alpha, color=color, zorder=0)
103
- if xmax > extreme_max:
104
- ax.axvspan(extreme_max, xmax, alpha=alpha, color=color, zorder=0)
105
- ax.set_xlim(xmin, xmax)
106
-
107
-
108
81
  def _add_cv_cyclicality_box(ax, mean, cv, cyclicality_corr, label="pro-cyc"):
109
82
  """Add stats box showing pre-computed mean, CV, and cyclicality correlation."""
110
83
  ax.text(
@@ -308,7 +281,7 @@ def visualize_growth_plus_results(
308
281
  horizontalalignment="right",
309
282
  bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.5),
310
283
  )
311
- _shade_beyond_extreme(ax, b["extreme_min"], b["extreme_max"])
284
+ shade_beyond_extreme(ax, b["extreme_min"], b["extreme_max"])
312
285
 
313
286
  # Panel (0,1): Unemployment Rate
314
287
  ax = axes[0, 1]
@@ -353,7 +326,7 @@ def visualize_growth_plus_results(
353
326
  ax.grid(True, linestyle="--", alpha=0.3)
354
327
  ax.set_ylim(bottom=0)
355
328
  add_stats_box(ax, unemployment_pct, "unemployment", is_pct=True)
356
- _shade_beyond_extreme(
329
+ shade_beyond_extreme(
357
330
  ax,
358
331
  bounds["unemployment"]["extreme_min"] * 100,
359
332
  bounds["unemployment"]["extreme_max"] * 100,
@@ -409,7 +382,7 @@ def visualize_growth_plus_results(
409
382
  # Stats box uses post-burn-in data (matching validation metrics)
410
383
  inflation_ss_pct = metrics.inflation[burn_in:] * 100
411
384
  add_stats_box(ax, inflation_ss_pct, "inflation", is_pct=True)
412
- _shade_beyond_extreme(
385
+ shade_beyond_extreme(
413
386
  ax,
414
387
  bounds["inflation"]["extreme_min"] * 100,
415
388
  bounds["inflation"]["extreme_max"] * 100,
@@ -787,7 +760,7 @@ def visualize_financial_dynamics(
787
760
  verticalalignment="top",
788
761
  bbox=dict(boxstyle="round", facecolor=box_color, alpha=0.7),
789
762
  )
790
- _shade_beyond_extreme(ax, extreme_min, extreme_max, axis="x")
763
+ shade_beyond_extreme(ax, extreme_min, extreme_max, axis="x")
791
764
  ax.set_title("Output Growth Rate Distribution", fontsize=12, fontweight="bold")
792
765
  ax.set_xlabel("Output growth rate")
793
766
  ax.set_ylabel("Log-rank")
@@ -884,7 +857,7 @@ def visualize_financial_dynamics(
884
857
  verticalalignment="top",
885
858
  bbox=dict(boxstyle="round", facecolor=box_color, alpha=0.7),
886
859
  )
887
- _shade_beyond_extreme(ax, extreme_min, extreme_max, axis="x")
860
+ shade_beyond_extreme(ax, extreme_min, extreme_max, axis="x")
888
861
  ax.set_title(
889
862
  "Firms' Asset Growth Rate Distribution", fontsize=12, fontweight="bold"
890
863
  )
@@ -966,7 +939,7 @@ def visualize_financial_dynamics(
966
939
  horizontalalignment="left",
967
940
  bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.5),
968
941
  )
969
- _shade_beyond_extreme(ax, extreme_min * 100, extreme_max * 100)
942
+ shade_beyond_extreme(ax, extreme_min * 100, extreme_max * 100)
970
943
 
971
944
  # Figure 3.6d: Number of Bankruptcies
972
945
  ax = axes[1, 1]
@@ -1064,7 +1037,7 @@ def visualize_financial_dynamics(
1064
1037
  metrics.financial_fragility_cv,
1065
1038
  metrics.fragility_gdp_correlation,
1066
1039
  )
1067
- _shade_beyond_extreme(ax, extreme_min, extreme_max)
1040
+ shade_beyond_extreme(ax, extreme_min, extreme_max)
1068
1041
 
1069
1042
  # Figure 3.7b: Price Ratio (Market Price / Clearing Price)
1070
1043
  ax = axes[2, 1]
@@ -1114,7 +1087,7 @@ def visualize_financial_dynamics(
1114
1087
  metrics.price_ratio_gdp_correlation,
1115
1088
  label="counter-cyc",
1116
1089
  )
1117
- _shade_beyond_extreme(ax, pr_extreme_min, pr_extreme_max)
1090
+ shade_beyond_extreme(ax, pr_extreme_min, pr_extreme_max)
1118
1091
 
1119
1092
  # Figure 3.7c: Price Dispersion
1120
1093
  ax = axes[3, 0]
@@ -1168,7 +1141,7 @@ def visualize_financial_dynamics(
1168
1141
  horizontalalignment="left",
1169
1142
  bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.5),
1170
1143
  )
1171
- _shade_beyond_extreme(ax, pd_extreme_min, pd_extreme_max)
1144
+ shade_beyond_extreme(ax, pd_extreme_min, pd_extreme_max)
1172
1145
 
1173
1146
  # Figure 3.7d: Equity and Sales Dispersion
1174
1147
  ax = axes[3, 1]
@@ -1224,7 +1197,7 @@ def visualize_financial_dynamics(
1224
1197
  horizontalalignment="left",
1225
1198
  bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.5),
1226
1199
  )
1227
- _shade_beyond_extreme(ax, es_extreme_min, es_extreme_max)
1200
+ shade_beyond_extreme(ax, es_extreme_min, es_extreme_max)
1228
1201
 
1229
1202
  plt.tight_layout()
1230
1203
  _save_panels(fig, axes, _OUTPUT_DIR, _FINANCIAL_PANEL_NAMES)
File without changes
File without changes