bamengine 0.9.0__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.0/src/bamengine.egg-info → bamengine-0.9.2}/PKG-INFO +12 -20
  2. {bamengine-0.9.0 → bamengine-0.9.2}/README.md +11 -19
  3. {bamengine-0.9.0 → bamengine-0.9.2}/pyproject.toml +5 -1
  4. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/__init__.py +1 -1
  5. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/config/__init__.py +5 -1
  6. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/core/relationship.py +2 -2
  7. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/logging.py +5 -1
  8. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/simulation.py +86 -30
  9. {bamengine-0.9.0 → bamengine-0.9.2/src/bamengine.egg-info}/PKG-INFO +12 -20
  10. {bamengine-0.9.0 → bamengine-0.9.2}/src/validation/engine.py +1 -1
  11. {bamengine-0.9.0 → bamengine-0.9.2}/src/validation/robustness/internal_validity.py +36 -10
  12. {bamengine-0.9.0 → bamengine-0.9.2}/src/validation/robustness/sensitivity.py +6 -1
  13. {bamengine-0.9.0 → bamengine-0.9.2}/src/validation/robustness/viz.py +6 -5
  14. {bamengine-0.9.0 → bamengine-0.9.2}/src/validation/scenarios/_utils.py +44 -0
  15. {bamengine-0.9.0 → bamengine-0.9.2}/src/validation/scenarios/baseline/__init__.py +1 -1
  16. {bamengine-0.9.0 → bamengine-0.9.2}/src/validation/scenarios/baseline/targets.yaml +8 -8
  17. {bamengine-0.9.0 → bamengine-0.9.2}/src/validation/scenarios/baseline/viz.py +14 -2
  18. {bamengine-0.9.0 → bamengine-0.9.2}/src/validation/scenarios/buffer_stock/__init__.py +1 -1
  19. {bamengine-0.9.0 → bamengine-0.9.2}/src/validation/scenarios/growth_plus/__init__.py +1 -3
  20. {bamengine-0.9.0 → bamengine-0.9.2}/src/validation/scenarios/growth_plus/viz.py +11 -38
  21. {bamengine-0.9.0 → bamengine-0.9.2}/LICENSE +0 -0
  22. {bamengine-0.9.0 → bamengine-0.9.2}/setup.cfg +0 -0
  23. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/config/default_pipeline.yml +0 -0
  24. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/config/defaults.yml +0 -0
  25. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/config/schema.py +0 -0
  26. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/config/validator.py +0 -0
  27. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/core/__init__.py +0 -0
  28. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/core/agent.py +0 -0
  29. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/core/decorators.py +0 -0
  30. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/core/event.py +0 -0
  31. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/core/pipeline.py +0 -0
  32. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/core/registry.py +0 -0
  33. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/core/role.py +0 -0
  34. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/economy.py +0 -0
  35. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/events/__init__.py +0 -0
  36. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/events/_internal/__init__.py +0 -0
  37. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/events/_internal/bankruptcy.py +0 -0
  38. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/events/_internal/credit_market.py +0 -0
  39. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/events/_internal/goods_market.py +0 -0
  40. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/events/_internal/labor_market.py +0 -0
  41. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/events/_internal/planning.py +0 -0
  42. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/events/_internal/production.py +0 -0
  43. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/events/_internal/revenue.py +0 -0
  44. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/events/bankruptcy.py +0 -0
  45. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/events/credit_market.py +0 -0
  46. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/events/economy_stats.py +0 -0
  47. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/events/goods_market.py +0 -0
  48. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/events/labor_market.py +0 -0
  49. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/events/planning.py +0 -0
  50. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/events/production.py +0 -0
  51. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/events/revenue.py +0 -0
  52. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/extension.py +0 -0
  53. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/logging.pyi +0 -0
  54. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/ops.py +0 -0
  55. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/py.typed +0 -0
  56. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/relationships/__init__.py +0 -0
  57. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/relationships/loanbook.py +0 -0
  58. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/results.py +0 -0
  59. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/roles/__init__.py +0 -0
  60. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/roles/borrower.py +0 -0
  61. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/roles/consumer.py +0 -0
  62. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/roles/employer.py +0 -0
  63. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/roles/lender.py +0 -0
  64. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/roles/producer.py +0 -0
  65. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/roles/shareholder.py +0 -0
  66. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/roles/worker.py +0 -0
  67. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/typing.py +0 -0
  68. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine/utils.py +0 -0
  69. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine.egg-info/SOURCES.txt +0 -0
  70. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine.egg-info/dependency_links.txt +0 -0
  71. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine.egg-info/requires.txt +0 -0
  72. {bamengine-0.9.0 → bamengine-0.9.2}/src/bamengine.egg-info/top_level.txt +0 -0
  73. {bamengine-0.9.0 → bamengine-0.9.2}/src/calibration/__init__.py +0 -0
  74. {bamengine-0.9.0 → bamengine-0.9.2}/src/calibration/__main__.py +0 -0
  75. {bamengine-0.9.0 → bamengine-0.9.2}/src/calibration/analysis.py +0 -0
  76. {bamengine-0.9.0 → bamengine-0.9.2}/src/calibration/cli.py +0 -0
  77. {bamengine-0.9.0 → bamengine-0.9.2}/src/calibration/cost.py +0 -0
  78. {bamengine-0.9.0 → bamengine-0.9.2}/src/calibration/cross_eval.py +0 -0
  79. {bamengine-0.9.0 → bamengine-0.9.2}/src/calibration/grid.py +0 -0
  80. {bamengine-0.9.0 → bamengine-0.9.2}/src/calibration/io.py +0 -0
  81. {bamengine-0.9.0 → bamengine-0.9.2}/src/calibration/morris.py +0 -0
  82. {bamengine-0.9.0 → bamengine-0.9.2}/src/calibration/parameter_space.py +0 -0
  83. {bamengine-0.9.0 → bamengine-0.9.2}/src/calibration/reporting.py +0 -0
  84. {bamengine-0.9.0 → bamengine-0.9.2}/src/calibration/rescreen.py +0 -0
  85. {bamengine-0.9.0 → bamengine-0.9.2}/src/calibration/screening.py +0 -0
  86. {bamengine-0.9.0 → bamengine-0.9.2}/src/calibration/sensitivity.py +0 -0
  87. {bamengine-0.9.0 → bamengine-0.9.2}/src/calibration/stability.py +0 -0
  88. {bamengine-0.9.0 → bamengine-0.9.2}/src/calibration/sweep.py +0 -0
  89. {bamengine-0.9.0 → bamengine-0.9.2}/src/extensions/__init__.py +0 -0
  90. {bamengine-0.9.0 → bamengine-0.9.2}/src/extensions/buffer_stock/__init__.py +0 -0
  91. {bamengine-0.9.0 → bamengine-0.9.2}/src/extensions/buffer_stock/events.py +0 -0
  92. {bamengine-0.9.0 → bamengine-0.9.2}/src/extensions/buffer_stock/role.py +0 -0
  93. {bamengine-0.9.0 → bamengine-0.9.2}/src/extensions/rnd/__init__.py +0 -0
  94. {bamengine-0.9.0 → bamengine-0.9.2}/src/extensions/rnd/events.py +0 -0
  95. {bamengine-0.9.0 → bamengine-0.9.2}/src/extensions/rnd/role.py +0 -0
  96. {bamengine-0.9.0 → bamengine-0.9.2}/src/extensions/taxation/__init__.py +0 -0
  97. {bamengine-0.9.0 → bamengine-0.9.2}/src/extensions/taxation/events.py +0 -0
  98. {bamengine-0.9.0 → bamengine-0.9.2}/src/validation/__init__.py +0 -0
  99. {bamengine-0.9.0 → bamengine-0.9.2}/src/validation/reporting.py +0 -0
  100. {bamengine-0.9.0 → bamengine-0.9.2}/src/validation/robustness/__init__.py +0 -0
  101. {bamengine-0.9.0 → bamengine-0.9.2}/src/validation/robustness/__main__.py +0 -0
  102. {bamengine-0.9.0 → bamengine-0.9.2}/src/validation/robustness/experiments.py +0 -0
  103. {bamengine-0.9.0 → bamengine-0.9.2}/src/validation/robustness/reference_values.yaml +0 -0
  104. {bamengine-0.9.0 → bamengine-0.9.2}/src/validation/robustness/reporting.py +0 -0
  105. {bamengine-0.9.0 → bamengine-0.9.2}/src/validation/robustness/stats.py +0 -0
  106. {bamengine-0.9.0 → bamengine-0.9.2}/src/validation/robustness/structural.py +0 -0
  107. {bamengine-0.9.0 → bamengine-0.9.2}/src/validation/scenarios/__init__.py +0 -0
  108. {bamengine-0.9.0 → bamengine-0.9.2}/src/validation/scenarios/baseline/__main__.py +0 -0
  109. {bamengine-0.9.0 → bamengine-0.9.2}/src/validation/scenarios/buffer_stock/__main__.py +0 -0
  110. {bamengine-0.9.0 → bamengine-0.9.2}/src/validation/scenarios/buffer_stock/targets.yaml +0 -0
  111. {bamengine-0.9.0 → bamengine-0.9.2}/src/validation/scenarios/buffer_stock/viz.py +0 -0
  112. {bamengine-0.9.0 → bamengine-0.9.2}/src/validation/scenarios/growth_plus/__main__.py +0 -0
  113. {bamengine-0.9.0 → bamengine-0.9.2}/src/validation/scenarios/growth_plus/targets.yaml +0 -0
  114. {bamengine-0.9.0 → bamengine-0.9.2}/src/validation/scoring.py +0 -0
  115. {bamengine-0.9.0 → 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.0
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.0"
169
+ __version__: str = "0.9.2"
170
170
 
171
171
  # ============================================================================
172
172
  # Standard library imports
@@ -42,7 +42,11 @@ Custom pipeline:
42
42
 
43
43
  >>> sim = be.Simulation.init(n_firms=100, pipeline_path="custom_pipeline.yml", seed=42)
44
44
 
45
- Custom logging:
45
+ Set log level:
46
+
47
+ >>> sim = be.Simulation.init(log_level="WARNING")
48
+
49
+ Advanced logging (per-event levels):
46
50
 
47
51
  >>> log_config = {
48
52
  ... "default_level": "DEBUG",
@@ -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,
@@ -24,9 +24,13 @@ Use logger in events:
24
24
  >>> logger.debug("Detailed debug info")
25
25
  >>> logger.trace("Very verbose output")
26
26
 
27
- Configure per-event log levels:
27
+ Set log level:
28
28
 
29
29
  >>> import bamengine as be
30
+ >>> sim = be.Simulation.init(log_level="WARNING")
31
+
32
+ Configure per-event log levels (advanced):
33
+
30
34
  >>> log_config = {
31
35
  ... "default_level": "INFO",
32
36
  ... "events": {"firms_adjust_price": "DEBUG", "labor_market_round": "WARNING"},
@@ -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
 
@@ -514,7 +544,8 @@ class Simulation:
514
544
  - n_banks : int (default: 10)
515
545
  - seed : int or None (default: 0)
516
546
  - pipeline_path : str or None (default: None)
517
- - logging : dict (default: {"default_level": "INFO"})
547
+ - log_level : str, e.g. "WARNING" (simple global log level)
548
+ - logging : dict (advanced per-event/file configuration)
518
549
  See config/defaults.yml for all parameters.
519
550
 
520
551
  Returns
@@ -562,7 +593,11 @@ class Simulation:
562
593
  ... pipeline_path="custom_pipeline.yml", seed=42
563
594
  ... ) # doctest: +SKIP
564
595
 
565
- Configure logging:
596
+ Configure log level:
597
+
598
+ >>> sim = bam.Simulation.init(log_level="WARNING", seed=42)
599
+
600
+ Advanced logging (per-event levels, file output):
566
601
 
567
602
  >>> log_config = {
568
603
  ... "default_level": "DEBUG",
@@ -573,14 +608,6 @@ class Simulation:
573
608
  ... }
574
609
  >>> sim = bam.Simulation.init(logging=log_config, seed=42)
575
610
 
576
- Configure logging with file output:
577
-
578
- >>> log_config = {
579
- ... "default_level": "DEBUG",
580
- ... "log_file": "simulation.log",
581
- ... }
582
- >>> sim = bam.Simulation.init(logging=log_config, seed=42) # doctest: +SKIP
583
-
584
611
  Notes
585
612
  -----
586
613
  - All configuration is validated before initialization
@@ -599,6 +626,16 @@ class Simulation:
599
626
  cfg_dict.update(_read_yaml(config))
600
627
  cfg_dict.update(overrides)
601
628
 
629
+ # log_level sugar: convert to logging dict
630
+ if "log_level" in overrides:
631
+ if "logging" in overrides:
632
+ raise ValueError(
633
+ "Cannot specify both 'log_level' and 'logging'. "
634
+ "Use 'log_level' for simple level setting, or 'logging' "
635
+ "for advanced configuration (per-event levels, log files)."
636
+ )
637
+ cfg_dict["logging"] = {"default_level": cfg_dict.pop("log_level")}
638
+
602
639
  # Validate configuration (centralized validation)
603
640
  from bamengine.config import ConfigValidator
604
641
 
@@ -942,6 +979,7 @@ class Simulation:
942
979
  "savings_init",
943
980
  "equity_base_init",
944
981
  "pipeline_path",
982
+ "log_level",
945
983
  "logging",
946
984
  "min_wage",
947
985
  "min_wage_rev_period",
@@ -1629,33 +1667,51 @@ class Simulation:
1629
1667
  def _resolve_annotation_dtype(ann: Any) -> tuple[type[np.generic], int]:
1630
1668
  """Resolve a role field annotation to (numpy dtype, fill value).
1631
1669
 
1632
- Handles string annotations (from ``__future__`` annotations),
1633
- resolved ``GenericAlias`` / type objects, and the Agent class.
1634
- Agent/Idx1D annotations use -1 fill (unassigned sentinel);
1635
- 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.
1636
1682
  """
1637
1683
  # String annotations (e.g., 'Float', 'Int1D')
1638
1684
  if isinstance(ann, str):
1639
- 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
1640
1693
 
1641
- # Agent class (bamengine.core.agent.Agent) — special case
1694
+ # Agent class (bamengine.core.agent.Agent) — unassigned sentinel -1
1642
1695
  from bamengine.core.agent import Agent as AgentCls
1643
1696
 
1644
1697
  if ann is AgentCls:
1645
1698
  return np.intp, -1
1646
1699
 
1647
- # Resolved GenericAlias: NDArray[np.float64] extract inner dtype
1648
- args: tuple[Any, ...] = getattr(ann, "__args__", ())
1649
- if len(args) >= 2:
1650
- inner_args: tuple[Any, ...] = getattr(args[1], "__args__", ())
1651
- if (
1652
- inner_args
1653
- and isinstance(inner_args[0], type)
1654
- and issubclass(inner_args[0], np.generic)
1655
- ):
1656
- return inner_args[0], 0
1657
-
1658
- 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
+ )
1659
1715
 
1660
1716
  def get_event(self, name: str) -> Any:
1661
1717
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bamengine
3
- Version: 0.9.0
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
 
@@ -233,7 +233,7 @@ def validate(
233
233
  **scenario.default_config,
234
234
  "n_periods": n_periods,
235
235
  "seed": seed,
236
- "logging": {"default_level": "ERROR"},
236
+ "log_level": "ERROR",
237
237
  **config_overrides,
238
238
  }
239
239
 
@@ -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,
@@ -569,7 +595,7 @@ def _run_seed(
569
595
  sim = bam.Simulation.init(
570
596
  seed=seed,
571
597
  n_periods=n_periods,
572
- logging={"default_level": "ERROR"},
598
+ log_level="ERROR",
573
599
  **config,
574
600
  )
575
601
  if setup_hook is not None:
@@ -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]
@@ -621,7 +621,7 @@ def run_scenario(
621
621
  sim = bam.Simulation.init(
622
622
  n_periods=n_periods,
623
623
  seed=seed,
624
- logging={"default_level": "ERROR"},
624
+ log_level="ERROR",
625
625
  )
626
626
 
627
627
  print("Initialized baseline scenario with:")
@@ -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: