OpenFisca-Core 44.2.2__tar.gz → 44.3.0__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 (214) hide show
  1. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/CHANGELOG.md +14 -0
  2. {openfisca_core-44.2.2 → openfisca_core-44.3.0/OpenFisca_Core.egg-info}/PKG-INFO +1 -1
  3. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/OpenFisca_Core.egg-info/SOURCES.txt +15 -0
  4. {openfisca_core-44.2.2/OpenFisca_Core.egg-info → openfisca_core-44.3.0}/PKG-INFO +1 -1
  5. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/entities/_core_entity.py +40 -0
  6. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/indexed_enums/enum_array.py +2 -4
  7. openfisca_core-44.3.0/openfisca_core/links/__init__.py +36 -0
  8. openfisca_core-44.3.0/openfisca_core/links/implicit.py +73 -0
  9. openfisca_core-44.3.0/openfisca_core/links/link.py +109 -0
  10. openfisca_core-44.3.0/openfisca_core/links/many2one.py +227 -0
  11. openfisca_core-44.3.0/openfisca_core/links/one2many.py +242 -0
  12. openfisca_core-44.3.0/openfisca_core/links/tests/__init__.py +58 -0
  13. openfisca_core-44.3.0/openfisca_core/links/tests/test_benchmark.py +93 -0
  14. openfisca_core-44.3.0/openfisca_core/links/tests/test_edge_cases.py +467 -0
  15. openfisca_core-44.3.0/openfisca_core/links/tests/test_implicit.py +76 -0
  16. openfisca_core-44.3.0/openfisca_core/links/tests/test_integration.py +192 -0
  17. openfisca_core-44.3.0/openfisca_core/links/tests/test_many2one.py +121 -0
  18. openfisca_core-44.3.0/openfisca_core/links/tests/test_one2many.py +112 -0
  19. openfisca_core-44.3.0/openfisca_core/links/tests/test_one2many_logic.py +398 -0
  20. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/populations/group_population.py +81 -21
  21. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/populations/population.py +7 -5
  22. openfisca_core-44.3.0/openfisca_core/populations/tests/test_members_position.py +116 -0
  23. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/simulations/simulation.py +48 -0
  24. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/taxscales/abstract_rate_tax_scale.py +1 -1
  25. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/taxscales/abstract_tax_scale.py +2 -2
  26. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/taxscales/tax_scale_like.py +2 -2
  27. openfisca_core-44.3.0/openfisca_web_api/scripts/__init__.py +0 -0
  28. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/pyproject.toml +1 -1
  29. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/setup.py +1 -1
  30. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/LICENSE +0 -0
  31. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/MANIFEST.in +0 -0
  32. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/OpenFisca_Core.egg-info/dependency_links.txt +0 -0
  33. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/OpenFisca_Core.egg-info/entry_points.txt +0 -0
  34. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/OpenFisca_Core.egg-info/requires.txt +0 -0
  35. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/OpenFisca_Core.egg-info/top_level.txt +0 -0
  36. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/README.md +0 -0
  37. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/__init__.py +0 -0
  38. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/commons/__init__.py +0 -0
  39. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/commons/dummy.py +0 -0
  40. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/commons/formulas.py +0 -0
  41. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/commons/misc.py +0 -0
  42. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/commons/py.typed +0 -0
  43. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/commons/rates.py +0 -0
  44. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/commons/tests/__init__.py +0 -0
  45. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/commons/tests/test_dummy.py +0 -0
  46. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/commons/tests/test_formulas.py +0 -0
  47. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/commons/tests/test_rates.py +0 -0
  48. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/data_storage/__init__.py +0 -0
  49. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/data_storage/in_memory_storage.py +0 -0
  50. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/data_storage/on_disk_storage.py +0 -0
  51. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/data_storage/types.py +0 -0
  52. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/entities/__init__.py +0 -0
  53. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/entities/_description.py +0 -0
  54. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/entities/entity.py +0 -0
  55. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/entities/group_entity.py +0 -0
  56. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/entities/helpers.py +0 -0
  57. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/entities/py.typed +0 -0
  58. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/entities/role.py +0 -0
  59. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/entities/tests/__init__.py +0 -0
  60. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/entities/tests/test_entity.py +0 -0
  61. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/entities/tests/test_group_entity.py +0 -0
  62. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/entities/tests/test_role.py +0 -0
  63. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/entities/types.py +0 -0
  64. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/errors/__init__.py +0 -0
  65. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/errors/cycle_error.py +0 -0
  66. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/errors/empty_argument_error.py +0 -0
  67. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/errors/nan_creation_error.py +0 -0
  68. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/errors/parameter_not_found_error.py +0 -0
  69. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/errors/parameter_parsing_error.py +0 -0
  70. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/errors/period_mismatch_error.py +0 -0
  71. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/errors/situation_parsing_error.py +0 -0
  72. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/errors/spiral_error.py +0 -0
  73. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/errors/variable_name_config_error.py +0 -0
  74. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/errors/variable_not_found_error.py +0 -0
  75. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/experimental/__init__.py +0 -0
  76. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/experimental/_errors.py +0 -0
  77. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/experimental/_memory_config.py +0 -0
  78. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/formula_helpers.py +0 -0
  79. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/holders/__init__.py +0 -0
  80. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/holders/helpers.py +0 -0
  81. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/holders/holder.py +0 -0
  82. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/holders/tests/__init__.py +0 -0
  83. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/holders/tests/test_helpers.py +0 -0
  84. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/holders/types.py +0 -0
  85. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/indexed_enums/__init__.py +0 -0
  86. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/indexed_enums/_enum_type.py +0 -0
  87. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/indexed_enums/_errors.py +0 -0
  88. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/indexed_enums/_guards.py +0 -0
  89. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/indexed_enums/_utils.py +0 -0
  90. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/indexed_enums/config.py +0 -0
  91. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/indexed_enums/enum.py +0 -0
  92. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/indexed_enums/py.typed +0 -0
  93. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/indexed_enums/tests/__init__.py +0 -0
  94. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/indexed_enums/tests/test_enum.py +0 -0
  95. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/indexed_enums/tests/test_enum_array.py +0 -0
  96. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/indexed_enums/types.py +0 -0
  97. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/memory_config.py +0 -0
  98. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/model_api.py +0 -0
  99. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/parameters/__init__.py +0 -0
  100. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/parameters/at_instant_like.py +0 -0
  101. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/parameters/config.py +0 -0
  102. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/parameters/helpers.py +0 -0
  103. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/parameters/parameter.py +0 -0
  104. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/parameters/parameter_at_instant.py +0 -0
  105. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/parameters/parameter_node.py +0 -0
  106. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/parameters/parameter_node_at_instant.py +0 -0
  107. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/parameters/parameter_scale.py +0 -0
  108. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/parameters/parameter_scale_bracket.py +0 -0
  109. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/parameters/values_history.py +0 -0
  110. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/parameters/vectorial_parameter_node_at_instant.py +0 -0
  111. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/periods/__init__.py +0 -0
  112. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/periods/_errors.py +0 -0
  113. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/periods/_parsers.py +0 -0
  114. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/periods/config.py +0 -0
  115. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/periods/date_unit.py +0 -0
  116. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/periods/helpers.py +0 -0
  117. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/periods/instant_.py +0 -0
  118. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/periods/period_.py +0 -0
  119. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/periods/py.typed +0 -0
  120. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/periods/tests/__init__.py +0 -0
  121. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/periods/tests/helpers/__init__.py +0 -0
  122. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/periods/tests/helpers/test_helpers.py +0 -0
  123. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/periods/tests/helpers/test_instant.py +0 -0
  124. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/periods/tests/helpers/test_period.py +0 -0
  125. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/periods/tests/test_instant.py +0 -0
  126. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/periods/tests/test_parsers.py +0 -0
  127. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/periods/tests/test_period.py +0 -0
  128. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/populations/__init__.py +0 -0
  129. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/populations/_core_population.py +0 -0
  130. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/populations/_errors.py +0 -0
  131. {openfisca_core-44.2.2/openfisca_core/scripts/assets → openfisca_core-44.3.0/openfisca_core/populations/tests}/__init__.py +0 -0
  132. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/populations/types.py +0 -0
  133. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/projectors/__init__.py +0 -0
  134. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/projectors/entity_to_person_projector.py +0 -0
  135. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/projectors/first_person_to_entity_projector.py +0 -0
  136. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/projectors/helpers.py +0 -0
  137. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/projectors/projector.py +0 -0
  138. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/projectors/typing.py +0 -0
  139. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/projectors/unique_role_to_entity_projector.py +0 -0
  140. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/rates.py +0 -0
  141. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/reforms/__init__.py +0 -0
  142. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/reforms/in_yaml_test_reform.py +0 -0
  143. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/reforms/reform.py +0 -0
  144. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/reforms/reform_excel.py +0 -0
  145. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/scripts/__init__.py +0 -0
  146. {openfisca_core-44.2.2/openfisca_core/scripts/migrations → openfisca_core-44.3.0/openfisca_core/scripts/assets}/__init__.py +0 -0
  147. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/scripts/assets/index.html +0 -0
  148. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/scripts/find_placeholders.py +0 -0
  149. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/scripts/measure_numpy_condition_notations.py +0 -0
  150. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/scripts/measure_performances.py +0 -0
  151. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/scripts/measure_performances_fancy_indexing.py +0 -0
  152. {openfisca_core-44.2.2/openfisca_core/scripts/migrations/v16_2_to_v17 → openfisca_core-44.3.0/openfisca_core/scripts/migrations}/__init__.py +0 -0
  153. {openfisca_core-44.2.2/openfisca_core/variables/tests → openfisca_core-44.3.0/openfisca_core/scripts/migrations/v16_2_to_v17}/__init__.py +0 -0
  154. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/scripts/migrations/v16_2_to_v17/legislation.xsd +0 -0
  155. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_country_template.py +0 -0
  156. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_extension_template.py +0 -0
  157. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/scripts/migrations/v24_to_25.py +0 -0
  158. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/scripts/openfisca_command.py +0 -0
  159. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/scripts/remove_fuzzy.py +0 -0
  160. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/scripts/run_test.py +0 -0
  161. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/scripts/simulation_generator.py +0 -0
  162. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/simulation_builder.py +0 -0
  163. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/simulations/__init__.py +0 -0
  164. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/simulations/_build_default_simulation.py +0 -0
  165. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/simulations/_build_from_variables.py +0 -0
  166. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/simulations/_type_guards.py +0 -0
  167. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/simulations/helpers.py +0 -0
  168. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/simulations/simulation_builder.py +0 -0
  169. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/simulations/typing.py +0 -0
  170. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/taxbenefitsystems/__init__.py +0 -0
  171. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/taxbenefitsystems/tax_benefit_system.py +0 -0
  172. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/taxscales/__init__.py +0 -0
  173. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/taxscales/amount_tax_scale_like.py +0 -0
  174. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/taxscales/helpers.py +0 -0
  175. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/taxscales/linear_average_rate_tax_scale.py +0 -0
  176. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/taxscales/marginal_amount_tax_scale.py +0 -0
  177. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/taxscales/marginal_rate_tax_scale.py +0 -0
  178. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/taxscales/rate_tax_scale_like.py +0 -0
  179. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/taxscales/single_amount_tax_scale.py +0 -0
  180. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/tools/__init__.py +0 -0
  181. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/tools/parallel_plugin.py +0 -0
  182. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/tools/simulation_dumper.py +0 -0
  183. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/tools/test_runner.py +0 -0
  184. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/tracers/__init__.py +0 -0
  185. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/tracers/computation_log.py +0 -0
  186. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/tracers/flat_trace.py +0 -0
  187. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/tracers/full_tracer.py +0 -0
  188. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/tracers/performance_log.py +0 -0
  189. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/tracers/simple_tracer.py +0 -0
  190. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/tracers/trace_node.py +0 -0
  191. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/tracers/tracing_parameter_node_at_instant.py +0 -0
  192. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/types.py +0 -0
  193. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/variables/__init__.py +0 -0
  194. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/variables/config.py +0 -0
  195. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/variables/helpers.py +0 -0
  196. {openfisca_core-44.2.2/openfisca_web_api → openfisca_core-44.3.0/openfisca_core/variables/tests}/__init__.py +0 -0
  197. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/variables/tests/test_definition_period.py +0 -0
  198. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/variables/variable.py +0 -0
  199. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/warnings/__init__.py +0 -0
  200. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/warnings/libyaml_warning.py +0 -0
  201. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_core/warnings/tempfile_warning.py +0 -0
  202. {openfisca_core-44.2.2/openfisca_web_api/scripts → openfisca_core-44.3.0/openfisca_web_api}/__init__.py +0 -0
  203. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_web_api/app.py +0 -0
  204. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_web_api/errors.py +0 -0
  205. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_web_api/handlers.py +0 -0
  206. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_web_api/loader/__init__.py +0 -0
  207. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_web_api/loader/entities.py +0 -0
  208. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_web_api/loader/parameters.py +0 -0
  209. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_web_api/loader/spec.py +0 -0
  210. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_web_api/loader/tax_benefit_system.py +0 -0
  211. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_web_api/loader/variables.py +0 -0
  212. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_web_api/openAPI.yml +0 -0
  213. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/openfisca_web_api/scripts/serve.py +0 -0
  214. {openfisca_core-44.2.2 → openfisca_core-44.3.0}/setup.cfg +0 -0
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 44.3.0
4
+
5
+ #### New Features
6
+
7
+ - **Generic Entity Links (Phase 1-6)**: Introduced a new Liam2-inspired generic entity linking system avoiding rigid hierarchies like `Person -> Household`.
8
+ - Added new `Many2OneLink` and `One2ManyLink` models to create powerful inter-entity networks (e.g., `Person -> Employer`).
9
+ - Added implicit links directly binding members arrays. This powers the new `population.links` property natively inside `TaxBenefitSystem.instantiate_entities()`.
10
+ - Full capability to chain relationships via python: `person.mother.household.get("rent", period)`.
11
+ - Powerful vectorized declarative aggregations out-of-the-box (e.g., `households.persons.sum("salary", period, condition=is_female)`).
12
+
13
+ #### Technical Changes
14
+
15
+ - Backward compatibility is 100% maintained. Existing syntax via Projectors natively redirects to implicit links via modified `__getattr__`.
16
+
3
17
  ## 44.2.2
4
18
 
5
19
  #### Bug fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: OpenFisca-Core
3
- Version: 44.2.2
3
+ Version: 44.3.0
4
4
  Summary: A versatile microsimulation free software
5
5
  Home-page: https://github.com/openfisca/openfisca-core
6
6
  Author: OpenFisca Team
@@ -78,6 +78,19 @@ openfisca_core/indexed_enums/types.py
78
78
  openfisca_core/indexed_enums/tests/__init__.py
79
79
  openfisca_core/indexed_enums/tests/test_enum.py
80
80
  openfisca_core/indexed_enums/tests/test_enum_array.py
81
+ openfisca_core/links/__init__.py
82
+ openfisca_core/links/implicit.py
83
+ openfisca_core/links/link.py
84
+ openfisca_core/links/many2one.py
85
+ openfisca_core/links/one2many.py
86
+ openfisca_core/links/tests/__init__.py
87
+ openfisca_core/links/tests/test_benchmark.py
88
+ openfisca_core/links/tests/test_edge_cases.py
89
+ openfisca_core/links/tests/test_implicit.py
90
+ openfisca_core/links/tests/test_integration.py
91
+ openfisca_core/links/tests/test_many2one.py
92
+ openfisca_core/links/tests/test_one2many.py
93
+ openfisca_core/links/tests/test_one2many_logic.py
81
94
  openfisca_core/parameters/__init__.py
82
95
  openfisca_core/parameters/at_instant_like.py
83
96
  openfisca_core/parameters/config.py
@@ -113,6 +126,8 @@ openfisca_core/populations/_errors.py
113
126
  openfisca_core/populations/group_population.py
114
127
  openfisca_core/populations/population.py
115
128
  openfisca_core/populations/types.py
129
+ openfisca_core/populations/tests/__init__.py
130
+ openfisca_core/populations/tests/test_members_position.py
116
131
  openfisca_core/projectors/__init__.py
117
132
  openfisca_core/projectors/entity_to_person_projector.py
118
133
  openfisca_core/projectors/first_person_to_entity_projector.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: OpenFisca-Core
3
- Version: 44.2.2
3
+ Version: 44.3.0
4
4
  Summary: A versatile microsimulation free software
5
5
  Home-page: https://github.com/openfisca/openfisca-core
6
6
  Author: OpenFisca Team
@@ -47,6 +47,9 @@ class CoreEntity:
47
47
  #: A ``TaxBenefitSystem`` instance.
48
48
  _tax_benefit_system: None | t.TaxBenefitSystem = None
49
49
 
50
+ #: Named links to other entities (Many2One, One2Many, etc.).
51
+ _links: dict
52
+
50
53
  @abc.abstractmethod
51
54
  def __init__(self, *__args: object, **__kwargs: object) -> None: ...
52
55
 
@@ -57,6 +60,43 @@ class CoreEntity:
57
60
  """A ``CoreEntity`` belongs to a ``TaxBenefitSystem``."""
58
61
  self._tax_benefit_system = tax_benefit_system
59
62
 
63
+ # -- Link management --------------------------------------------------
64
+
65
+ def add_link(self, link) -> None:
66
+ """Register a named link on this entity.
67
+
68
+ Args:
69
+ link: A ``Link`` instance (Many2OneLink, One2ManyLink, etc.).
70
+
71
+ Example::
72
+
73
+ from openfisca_core.links import Many2OneLink
74
+
75
+ mother = Many2OneLink(
76
+ name="mother",
77
+ link_field="mother_id",
78
+ target_entity_key="person",
79
+ )
80
+ person_entity.add_link(mother)
81
+
82
+ """
83
+ if not hasattr(self, "_links") or self._links is None:
84
+ self._links = {}
85
+ self._links[link.name] = link
86
+
87
+ def get_link(self, name: str):
88
+ """Retrieve a link by name, or ``None`` if not found."""
89
+ if not hasattr(self, "_links") or self._links is None:
90
+ return None
91
+ return self._links.get(name)
92
+
93
+ @property
94
+ def links(self) -> dict:
95
+ """All links registered on this entity."""
96
+ if not hasattr(self, "_links") or self._links is None:
97
+ self._links = {}
98
+ return self._links
99
+
60
100
  def get_variable(
61
101
  self,
62
102
  variable_name: t.VariableName,
@@ -266,8 +266,7 @@ class EnumArray(t.EnumArray):
266
266
  result: t.ObjArray
267
267
  if self.possible_values is None:
268
268
  msg = (
269
- f"The possible values of the {self.__class__.__name__} are "
270
- f"not defined."
269
+ f"The possible values of the {self.__class__.__name__} are not defined."
271
270
  )
272
271
  raise TypeError(msg)
273
272
  array = self.reshape(1).astype(t.EnumDType) if self.ndim == 0 else self
@@ -301,8 +300,7 @@ class EnumArray(t.EnumArray):
301
300
  result: t.StrArray
302
301
  if self.possible_values is None:
303
302
  msg = (
304
- f"The possible values of the {self.__class__.__name__} are "
305
- f"not defined."
303
+ f"The possible values of the {self.__class__.__name__} are not defined."
306
304
  )
307
305
  raise TypeError(msg)
308
306
  array = self.reshape(1).astype(t.EnumDType) if self.ndim == 0 else self
@@ -0,0 +1,36 @@
1
+ """Links between entities (Many2One, One2Many, chaining).
2
+
3
+ This module implements a generic link system inspired by LIAM2, enriched
4
+ with OpenFisca's role semantics. Links are orthogonal to the existing
5
+ GroupEntity/Projector machinery: current code keeps working unchanged,
6
+ and links provide additional capabilities (intra-entity links, chaining,
7
+ arbitrary named links).
8
+
9
+ Usage in a country package::
10
+
11
+ from openfisca_core.links import Many2OneLink, One2ManyLink
12
+
13
+ # Declare a person→person link (intra-entity)
14
+ mother = Many2OneLink(
15
+ name="mother",
16
+ link_field="mother_id",
17
+ target_entity_key="person",
18
+ )
19
+
20
+ # Declare a person→employer link (inter-entity)
21
+ employer = Many2OneLink(
22
+ name="employer",
23
+ link_field="employer_id",
24
+ target_entity_key="employer",
25
+ )
26
+ """
27
+
28
+ from .link import Link
29
+ from .many2one import Many2OneLink
30
+ from .one2many import One2ManyLink
31
+
32
+ __all__ = [
33
+ "Link",
34
+ "Many2OneLink",
35
+ "One2ManyLink",
36
+ ]
@@ -0,0 +1,73 @@
1
+ """Implicit links dynamically inferred from OpenFisca's GroupPopulation system."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy
6
+
7
+ from .many2one import Many2OneLink
8
+ from .one2many import One2ManyLink
9
+
10
+
11
+ class ImplicitMany2OneLink(Many2OneLink):
12
+ """A person → group link using GroupPopulation's internal arrays.
13
+
14
+ This bypasses the usual Variable lookup (``simulation.calculate``)
15
+ and directly reads ``members_entity_id`` and ``members_role`` from
16
+ the target GroupPopulation.
17
+ """
18
+
19
+ def __init__(self, group_entity_key: str):
20
+ super().__init__(
21
+ name=group_entity_key,
22
+ link_field="", # Not used
23
+ target_entity_key=group_entity_key,
24
+ )
25
+
26
+ def _get_target_ids(self, period) -> numpy.ndarray:
27
+ return self._target_population.members_entity_id
28
+
29
+ @property
30
+ def role(self) -> numpy.ndarray | None:
31
+ return self._target_population.members_role
32
+
33
+ def _project_implicit(self, result: numpy.ndarray) -> numpy.ndarray:
34
+ # Fully compatible with old Projector logic
35
+ return self._target_population.project(result)
36
+
37
+
38
+ class ImplicitOne2ManyLink(One2ManyLink):
39
+ """A group → person link using GroupPopulation's internal arrays."""
40
+
41
+ def __init__(self, name: str, group_entity_key: str):
42
+ super().__init__(
43
+ name=name,
44
+ link_field="", # Not used
45
+ target_entity_key="person", # The target of the O2M is persons
46
+ )
47
+ self._group_entity_key = group_entity_key
48
+
49
+ def _source_rows(self, period) -> numpy.ndarray:
50
+ # For a group->person O2M, the source is the group, the target is the person.
51
+ # members_entity_id is an array of length person.count, containing the group index.
52
+ # So members_entity_id IS the array of source rows for each target member.
53
+ return self._source_population.members_entity_id
54
+
55
+ def _apply_filters(self, period, values, role, condition):
56
+ source_rows = self._source_rows(period)
57
+ mask = numpy.ones(len(source_rows), dtype=bool)
58
+
59
+ if role is not None:
60
+ roles = self._source_population.members_role
61
+ mask &= roles == role
62
+
63
+ if condition is not None:
64
+ mask &= condition
65
+
66
+ source_rows = source_rows[mask]
67
+ values = values[mask]
68
+
69
+ valid = source_rows >= 0
70
+ return source_rows[valid], values[valid]
71
+
72
+
73
+ __all__ = ["ImplicitMany2OneLink", "ImplicitOne2ManyLink"]
@@ -0,0 +1,109 @@
1
+ """Base Link class for entity relationships."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ import numpy
9
+
10
+
11
+ class Link:
12
+ """A named, directed relationship between two entity populations.
13
+
14
+ A Link is defined by:
15
+
16
+ - A *name* used to access the link from formulas (e.g. "mother", "household").
17
+ - A *link_field*: the name of the Variable (on the source entity) that holds
18
+ the target entity IDs.
19
+ - A *target_entity_key*: the entity key of the target population.
20
+ - Optionally, a *role_field* and *position_field* to attach OpenFisca-style
21
+ role and position metadata to the relationship.
22
+
23
+
24
+ Sub-classes ``Many2OneLink`` and ``One2ManyLink`` add resolution logic.
25
+
26
+ Parameters
27
+ ----------
28
+ name : str
29
+ Human-readable name of the link (e.g. ``"mother"``).
30
+ link_field : str
31
+ Variable name on the **source** entity that holds IDs pointing to
32
+ the target entity.
33
+ target_entity_key : str
34
+ The ``Entity.key`` of the target entity.
35
+ role_field : str or None
36
+ Optional variable name holding the role of each source member
37
+ within the target group.
38
+ position_field : str or None
39
+ Optional variable name holding the positional index of each
40
+ source member within the target group.
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ name: str,
46
+ link_field: str,
47
+ target_entity_key: str,
48
+ *,
49
+ role_field: str | None = None,
50
+ position_field: str | None = None,
51
+ ) -> None:
52
+ self.name = name
53
+ self.link_field = link_field
54
+ self.target_entity_key = target_entity_key
55
+ self.role_field = role_field
56
+ self.position_field = position_field
57
+
58
+ # Resolved after simulation setup
59
+ self._source_population = None
60
+ self._target_population = None
61
+
62
+ # -- lifecycle ----------------------------------------------------------
63
+
64
+ def attach(self, source_population) -> None:
65
+ """Bind this link to its source population."""
66
+ self._source_population = source_population
67
+
68
+ def resolve(self, populations: dict) -> None:
69
+ """Resolve ``target_entity_key`` to an actual population object.
70
+
71
+ Parameters
72
+ ----------
73
+ populations : dict[str, Population]
74
+ All populations in the simulation, keyed by entity key.
75
+ """
76
+ if self.target_entity_key not in populations:
77
+ msg = (
78
+ f"Link '{self.name}': target entity "
79
+ f"'{self.target_entity_key}' not found in populations "
80
+ f"{list(populations.keys())}"
81
+ )
82
+ raise KeyError(msg)
83
+ self._target_population = populations[self.target_entity_key]
84
+
85
+ # -- helpers ------------------------------------------------------------
86
+
87
+ @property
88
+ def is_resolved(self) -> bool:
89
+ return (
90
+ self._source_population is not None and self._target_population is not None
91
+ )
92
+
93
+ def _get_link_ids(self, period) -> numpy.ndarray:
94
+ """Return the array of target IDs for every source member.
95
+
96
+ This reads the ``link_field`` variable from the source population.
97
+ """
98
+ return self._source_population(self.link_field, period)
99
+
100
+ def __repr__(self) -> str:
101
+ return (
102
+ f"{self.__class__.__name__}("
103
+ f"name={self.name!r}, "
104
+ f"link_field={self.link_field!r}, "
105
+ f"target={self.target_entity_key!r})"
106
+ )
107
+
108
+
109
+ __all__ = ["Link"]
@@ -0,0 +1,227 @@
1
+ """Many-to-one link: N source members → 1 target entity."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ import numpy
8
+
9
+ from .link import Link
10
+
11
+ if TYPE_CHECKING:
12
+ pass
13
+
14
+
15
+ class Many2OneLink(Link):
16
+ """Navigate from many source members to one target entity.
17
+
18
+ Example: ``person.household`` (each person belongs to one household),
19
+ or ``person.mother`` (each person has one mother).
20
+
21
+ The resolution follows the LIAM2 pattern::
22
+
23
+ target_ids = source_pop(link_field) # e.g. [0, 0, 1, 2, 0]
24
+ target_values = target_pop(variable) # e.g. [800, 650, 900]
25
+ result = target_values[target_ids] # e.g. [800, 800, 650, 900, 800]
26
+ """
27
+
28
+ def get(self, variable_name: str, period) -> numpy.ndarray:
29
+ """Get a target variable's value for each source member.
30
+
31
+ Parameters
32
+ ----------
33
+ variable_name : str
34
+ Name of the variable defined on the target entity.
35
+ period : Period
36
+ The period for which to compute the variable.
37
+
38
+ Returns
39
+ -------
40
+ numpy.ndarray
41
+ Array of shape ``(n_source,)`` containing the target variable
42
+ value for each source member. Members with an invalid link
43
+ (target_id < 0) receive the variable's default value.
44
+ """
45
+ source_pop = self._source_population
46
+ simulation = source_pop.simulation
47
+
48
+ # 1. Target IDs for each source member
49
+ target_ids = self._get_target_ids(period)
50
+
51
+ # 2. Variable values on the target entity
52
+ target_values = simulation.calculate(variable_name, period)
53
+
54
+ # 3. Resolve IDs to row positions (handles id_to_rownum if needed)
55
+ target_rows = self._resolve_ids(target_ids)
56
+
57
+ # 4. Gather with missing-value handling
58
+ variable = simulation.tax_benefit_system.get_variable(variable_name)
59
+ default = variable.default_value if variable else 0
60
+
61
+ from openfisca_core.indexed_enums import Enum, EnumArray
62
+
63
+ if isinstance(default, Enum):
64
+ default = default.index
65
+
66
+ result = numpy.full(
67
+ source_pop.count,
68
+ default,
69
+ dtype=target_values.dtype,
70
+ )
71
+ valid = target_rows >= 0
72
+ result[valid] = target_values[target_rows[valid]]
73
+
74
+ if isinstance(target_values, EnumArray):
75
+ result = EnumArray(result, target_values.possible_values)
76
+
77
+ return result
78
+
79
+ # -- syntactic sugar ----------------------------------------------------
80
+
81
+ def __call__(self, variable_name: str, period) -> numpy.ndarray:
82
+ """Shorthand: ``person.mother("age", period)``."""
83
+ return self.get(variable_name, period)
84
+
85
+ def __getattr__(self, name: str):
86
+ """Chain links: ``person.mother.household``."""
87
+ if name.startswith("_"):
88
+ raise AttributeError(name)
89
+
90
+ target_pop = self._target_population
91
+ if target_pop is None:
92
+ raise AttributeError("Link is not bound to a simulation")
93
+
94
+ if hasattr(target_pop, "links") and name in target_pop.links:
95
+ target_link = target_pop.links[name]
96
+ return _ChainedGetter(self, target_link)
97
+
98
+ target_attr = getattr(target_pop, name, None)
99
+ if target_attr is not None:
100
+ if hasattr(target_attr, "projectable"):
101
+
102
+ def projector_function(*args, **kwargs):
103
+ result = target_attr(*args, **kwargs)
104
+ return self._project_result(result)
105
+
106
+ return projector_function
107
+ return target_attr
108
+
109
+ target_entity = target_pop.entity
110
+ msg = f"Entity '{target_entity.key}' has no link named '{name}'"
111
+ raise AttributeError(msg)
112
+
113
+ def _project_result(self, result: numpy.ndarray) -> numpy.ndarray:
114
+ if hasattr(self, "_project_implicit"):
115
+ return self._project_implicit(result)
116
+ msg = "Chained method calls computing arrays on explicit links are not supported because the period cannot be inferred."
117
+ raise NotImplementedError(msg)
118
+
119
+ # -- role helpers -------------------------------------------------------
120
+
121
+ @property
122
+ def role(self) -> numpy.ndarray | None:
123
+ """Role of each source member, if ``role_field`` is set."""
124
+ if self.role_field is None:
125
+ return None
126
+ return self._source_population.simulation.calculate(
127
+ self.role_field,
128
+ "eternity",
129
+ )
130
+
131
+ def has_role(self, role_value) -> numpy.ndarray:
132
+ """Boolean mask: does each source member have the given role?"""
133
+ r = self.role
134
+ if r is None:
135
+ msg = f"Link '{self.name}' has no role_field"
136
+ raise ValueError(msg)
137
+ return r == role_value
138
+
139
+ # -- ID resolution ------------------------------------------------------
140
+
141
+ def _get_target_ids(self, period) -> numpy.ndarray:
142
+ """Fetch the target IDs from the link_field variable."""
143
+ return self._source_population.simulation.calculate(
144
+ self.link_field,
145
+ period,
146
+ )
147
+
148
+ def _resolve_ids(self, target_ids: numpy.ndarray) -> numpy.ndarray:
149
+ """Convert target IDs to row indices.
150
+
151
+ If the target population has an ``_id_to_rownum`` mapping
152
+ (e.g. for intra-entity links where IDs ≠ row positions), use it.
153
+ Otherwise treat IDs as direct row indices (the OpenFisca convention
154
+ for GroupPopulation.members_entity_id).
155
+ """
156
+ target_pop = self._target_population
157
+ rows = numpy.full_like(target_ids, -1, dtype=numpy.intp)
158
+
159
+ if (
160
+ hasattr(target_pop, "_id_to_rownum")
161
+ and target_pop._id_to_rownum is not None
162
+ ):
163
+ id_to_rownum = target_pop._id_to_rownum
164
+ valid = (target_ids >= 0) & (target_ids < len(id_to_rownum))
165
+ rows[valid] = id_to_rownum[target_ids[valid]]
166
+ else:
167
+ valid = (target_ids >= 0) & (target_ids < target_pop.count)
168
+ rows[valid] = target_ids[valid]
169
+
170
+ return rows
171
+
172
+
173
+ # ---------------------------------------------------------------------------
174
+ # Chained link getter
175
+ # ---------------------------------------------------------------------------
176
+
177
+
178
+ class _ChainedGetter:
179
+ """Intermediate object for link chaining: ``person.mother.household``."""
180
+
181
+ def __init__(self, outer_link: Many2OneLink, inner_link: Link) -> None:
182
+ self._outer = outer_link
183
+ self._inner = inner_link
184
+
185
+ def get(self, variable_name: str, period) -> numpy.ndarray:
186
+ """Resolve ``person.mother.household.get("rent", period)``."""
187
+ # 1. Resolve inner link value on inner entity
188
+ inner_values = self._inner.get(variable_name, period)
189
+
190
+ # 2. Map back through outer link
191
+ target_ids = self._outer._source_population.simulation.calculate(
192
+ self._outer.link_field,
193
+ period,
194
+ )
195
+ target_rows = self._outer._resolve_ids(target_ids)
196
+
197
+ result = numpy.full(
198
+ self._outer._source_population.count,
199
+ 0,
200
+ dtype=inner_values.dtype,
201
+ )
202
+ valid = target_rows >= 0
203
+ result[valid] = inner_values[target_rows[valid]]
204
+ return result
205
+
206
+ def __call__(self, variable_name: str, period) -> numpy.ndarray:
207
+ """Shorthand for get(): ``person.mother.household("rent", period)``."""
208
+ return self.get(variable_name, period)
209
+
210
+ def __getattr__(self, name: str):
211
+ """Continue chaining: ``person.mother.household.region``."""
212
+ if name.startswith("_"):
213
+ raise AttributeError(name)
214
+
215
+ target_pop = self._inner._target_population
216
+ if target_pop is None:
217
+ raise AttributeError("Link is not bound to a simulation")
218
+
219
+ if hasattr(target_pop, "links") and name in target_pop.links:
220
+ next_link = target_pop.links[name]
221
+ return _ChainedGetter(self._outer, next_link)
222
+
223
+ target_entity = target_pop.entity
224
+ raise AttributeError(f"Entity '{target_entity.key}' has no link named '{name}'")
225
+
226
+
227
+ __all__ = ["Many2OneLink"]