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.
- {bamengine-0.9.1/src/bamengine.egg-info → bamengine-0.9.2}/PKG-INFO +12 -20
- {bamengine-0.9.1 → bamengine-0.9.2}/README.md +11 -19
- {bamengine-0.9.1 → bamengine-0.9.2}/pyproject.toml +5 -1
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/__init__.py +1 -1
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/core/relationship.py +2 -2
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/simulation.py +68 -20
- {bamengine-0.9.1 → bamengine-0.9.2/src/bamengine.egg-info}/PKG-INFO +12 -20
- {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/robustness/internal_validity.py +35 -9
- {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/robustness/sensitivity.py +6 -1
- {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/robustness/viz.py +6 -5
- {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/scenarios/_utils.py +44 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/scenarios/baseline/targets.yaml +8 -8
- {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/scenarios/baseline/viz.py +14 -2
- {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/scenarios/growth_plus/viz.py +11 -38
- {bamengine-0.9.1 → bamengine-0.9.2}/LICENSE +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/setup.cfg +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/config/__init__.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/config/default_pipeline.yml +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/config/defaults.yml +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/config/schema.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/config/validator.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/core/__init__.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/core/agent.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/core/decorators.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/core/event.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/core/pipeline.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/core/registry.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/core/role.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/economy.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/__init__.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/_internal/__init__.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/_internal/bankruptcy.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/_internal/credit_market.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/_internal/goods_market.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/_internal/labor_market.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/_internal/planning.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/_internal/production.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/_internal/revenue.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/bankruptcy.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/credit_market.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/economy_stats.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/goods_market.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/labor_market.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/planning.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/production.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/events/revenue.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/extension.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/logging.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/logging.pyi +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/ops.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/py.typed +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/relationships/__init__.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/relationships/loanbook.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/results.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/roles/__init__.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/roles/borrower.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/roles/consumer.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/roles/employer.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/roles/lender.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/roles/producer.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/roles/shareholder.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/roles/worker.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/typing.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine/utils.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine.egg-info/SOURCES.txt +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine.egg-info/dependency_links.txt +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine.egg-info/requires.txt +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/bamengine.egg-info/top_level.txt +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/calibration/__init__.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/calibration/__main__.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/calibration/analysis.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/calibration/cli.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/calibration/cost.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/calibration/cross_eval.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/calibration/grid.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/calibration/io.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/calibration/morris.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/calibration/parameter_space.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/calibration/reporting.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/calibration/rescreen.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/calibration/screening.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/calibration/sensitivity.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/calibration/stability.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/calibration/sweep.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/extensions/__init__.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/extensions/buffer_stock/__init__.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/extensions/buffer_stock/events.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/extensions/buffer_stock/role.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/extensions/rnd/__init__.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/extensions/rnd/events.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/extensions/rnd/role.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/extensions/taxation/__init__.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/extensions/taxation/events.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/__init__.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/engine.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/reporting.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/robustness/__init__.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/robustness/__main__.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/robustness/experiments.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/robustness/reference_values.yaml +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/robustness/reporting.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/robustness/stats.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/robustness/structural.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/scenarios/__init__.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/scenarios/baseline/__init__.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/scenarios/baseline/__main__.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/scenarios/buffer_stock/__init__.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/scenarios/buffer_stock/__main__.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/scenarios/buffer_stock/targets.yaml +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/scenarios/buffer_stock/viz.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/scenarios/growth_plus/__init__.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/scenarios/growth_plus/__main__.py +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/scenarios/growth_plus/targets.yaml +0 -0
- {bamengine-0.9.1 → bamengine-0.9.2}/src/validation/scoring.py +0 -0
- {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.
|
|
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).
|
|
87
|
-
interacting
|
|
88
|
-
|
|
89
|
-
</
|
|
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
|
|
176
|
-
- **
|
|
177
|
-
- **
|
|
178
|
-
- **
|
|
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
|
|
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).
|
|
17
|
-
interacting
|
|
18
|
-
|
|
19
|
-
</
|
|
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
|
|
106
|
-
- **
|
|
107
|
-
- **
|
|
108
|
-
- **
|
|
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
|
|
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
|
-
|
|
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
|
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
1642
|
-
|
|
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
|
-
|
|
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) —
|
|
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
|
-
#
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
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.
|
|
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).
|
|
87
|
-
interacting
|
|
88
|
-
|
|
89
|
-
</
|
|
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
|
|
176
|
-
- **
|
|
177
|
-
- **
|
|
178
|
-
- **
|
|
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
|
|
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
|
-
#
|
|
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=
|
|
344
|
-
unemployment_std=
|
|
345
|
-
inflation_mean=
|
|
346
|
-
inflation_std=
|
|
347
|
-
gdp_growth_mean=
|
|
348
|
-
gdp_growth_std=
|
|
349
|
-
real_wage_mean=
|
|
350
|
-
productivity_mean=
|
|
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)
|
|
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
|
-
|
|
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=(
|
|
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=(
|
|
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.
|
|
38
|
-
normal_max: 0.
|
|
39
|
-
extreme_min: 0.
|
|
40
|
-
extreme_max: 0.
|
|
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.
|
|
54
|
-
normal_max: 5.
|
|
55
|
-
extreme_min: 5.
|
|
56
|
-
extreme_max: 5.
|
|
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="
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|