diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index b8cef9230..94d08903c 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -34,15 +34,22 @@ jobs: strategy: matrix: os: [ ubuntu-latest, windows-latest ] + python-version: [ "3.12", "3.13" ] fail-fast: false runs-on: ${{ matrix.os }} steps: - name: Checkout repo uses: actions/checkout@v3 - - name: Set up Python + - name: Enable long paths in Git (Windows Only) + if: runner.os == 'Windows' + run: git config --system core.longpaths true + - name: Enable Win32 long paths via registry (Windows Only) + if: runner.os == 'Windows' + run: reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FileSystem" /v LongPathsEnabled /t REG_DWORD /d 1 /f + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: - python-version: "3.12" + python-version: ${{ matrix.python-version }} - name: Install package run: make install - name: Run tests diff --git a/Makefile b/Makefile index 33da57efa..957e2adb4 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ format: install: pip install -e ".[dev]" --config-settings editable_mode=compat - pip install policyengine-us + pip install git+https://github.com/noman404/policyengine-us.git@noman404/python3.13 pip install policyengine-uk test-country-template: diff --git a/changelog_entry.yaml b/changelog_entry.yaml index e69de29bb..7ed71d796 100644 --- a/changelog_entry.yaml +++ b/changelog_entry.yaml @@ -0,0 +1,5 @@ +- bump: major + changes: + changed: + - python 3.13.0 + - numpy 2.1.0 \ No newline at end of file diff --git a/policyengine_core/commons/formulas.py b/policyengine_core/commons/formulas.py index 4695cf9c6..7d316c364 100644 --- a/policyengine_core/commons/formulas.py +++ b/policyengine_core/commons/formulas.py @@ -92,6 +92,11 @@ def concat(this: ArrayLike[str], that: ArrayLike[str]) -> ArrayType[str]: array(['this1.0', 'that2.5']...) """ + if isinstance(this, tuple): + raise TypeError("First argument must not be a tuple.") + + if isinstance(that, tuple): + raise TypeError("Second argument must not be a tuple.") if isinstance(this, numpy.ndarray) and not numpy.issubdtype( this.dtype, numpy.str_ diff --git a/policyengine_core/enums/enum_array.py b/policyengine_core/enums/enum_array.py index 1bf5148b6..c518add8c 100644 --- a/policyengine_core/enums/enum_array.py +++ b/policyengine_core/enums/enum_array.py @@ -96,7 +96,8 @@ def decode_to_str(self) -> numpy.str_: """ return numpy.select( [self == item.index for item in self.possible_values], - [item.name for item in self.possible_values], + [str(item.name) for item in self.possible_values], + default="unknown", ) def __repr__(self) -> str: diff --git a/policyengine_core/parameters/vectorial_parameter_node_at_instant.py b/policyengine_core/parameters/vectorial_parameter_node_at_instant.py index dbd1d14b9..0b50d928b 100644 --- a/policyengine_core/parameters/vectorial_parameter_node_at_instant.py +++ b/policyengine_core/parameters/vectorial_parameter_node_at_instant.py @@ -203,13 +203,15 @@ def __getitem__(self, key: str) -> Any: enum = type(key[0]) key = numpy.select( [key == item for item in enum], - [item.name for item in enum], + [str(item.name) for item in enum], + default="unknown", ) elif isinstance(key, EnumArray): enum = key.possible_values key = numpy.select( [key == item.index for item in enum], [item.name for item in enum], + default="unknown", ) else: key = key.astype("str") diff --git a/policyengine_core/populations/group_population.py b/policyengine_core/populations/group_population.py index e201d0f50..fd6b1bee2 100644 --- a/policyengine_core/populations/group_population.py +++ b/policyengine_core/populations/group_population.py @@ -232,7 +232,7 @@ def max(self, array: ArrayLike, role: Role = None) -> ArrayLike: return self.reduce( array, reducer=numpy.maximum, - neutral_element=-numpy.infty, + neutral_element=-numpy.inf, role=role, ) @@ -256,7 +256,7 @@ def min(self, array: ArrayLike, role: Role = None) -> ArrayLike: return self.reduce( array, reducer=numpy.minimum, - neutral_element=numpy.infty, + neutral_element=numpy.inf, role=role, ) diff --git a/policyengine_core/populations/population.py b/policyengine_core/populations/population.py index a04b8de63..a6d992cbb 100644 --- a/policyengine_core/populations/population.py +++ b/policyengine_core/populations/population.py @@ -253,7 +253,11 @@ def get_rank( # We double-argsort all lines of the matrix. # Double-argsorting gets the rank of each value once sorted # For instance, if x = [3,1,6,4,0], y = numpy.argsort(x) is [4, 1, 0, 3, 2] (because the value with index 4 is the smallest one, the value with index 1 the second smallest, etc.) and z = numpy.argsort(y) is [2, 1, 4, 3, 0], the rank of each value. - sorted_matrix = numpy.argsort(numpy.argsort(matrix)) + + # because of the infinities the first sort creates positional indices + # The second argsort converts these positions to ranks, thus fixes the broken sort issue + first_argsort = numpy.argsort(matrix, axis=1, kind="stable") + sorted_matrix = numpy.argsort(first_argsort, axis=1, kind="stable") # Build the result vector by taking for each person the value in the right line (corresponding to its household id) and the right column (corresponding to its position) result = sorted_matrix[ids, positions] diff --git a/policyengine_core/taxscales/marginal_rate_tax_scale.py b/policyengine_core/taxscales/marginal_rate_tax_scale.py index 2da91c53f..3453f0199 100644 --- a/policyengine_core/taxscales/marginal_rate_tax_scale.py +++ b/policyengine_core/taxscales/marginal_rate_tax_scale.py @@ -66,12 +66,12 @@ def calc( # # numpy.finfo(float_).eps thresholds1 = numpy.outer( - factor + numpy.finfo(numpy.float_).eps, + factor + numpy.finfo(numpy.float64).eps, numpy.array(self.thresholds + [numpy.inf]), ) if round_base_decimals is not None: - thresholds1 = numpy.round_(thresholds1, round_base_decimals) + thresholds1 = numpy.round(thresholds1, round_base_decimals) a = numpy.maximum( numpy.minimum(base1, thresholds1[:, 1:]) - thresholds1[:, :-1], 0 @@ -82,8 +82,8 @@ def calc( else: r = numpy.tile(self.rates, (len(tax_base), 1)) - b = numpy.round_(a, round_base_decimals) - return numpy.round_(r * b, round_base_decimals).sum(axis=1) + b = numpy.round(a, round_base_decimals) + return numpy.round(r * b, round_base_decimals).sum(axis=1) def combine_bracket( self, diff --git a/policyengine_core/taxscales/rate_tax_scale_like.py b/policyengine_core/taxscales/rate_tax_scale_like.py index b598f4470..79d39728b 100644 --- a/policyengine_core/taxscales/rate_tax_scale_like.py +++ b/policyengine_core/taxscales/rate_tax_scale_like.py @@ -175,12 +175,12 @@ def bracket_indices( # # numpy.finfo(float_).eps thresholds1 = numpy.outer( - +factor + numpy.finfo(numpy.float_).eps, + +factor + numpy.finfo(numpy.float64).eps, numpy.array(self.thresholds), ) if round_decimals is not None: - thresholds1 = numpy.round_(thresholds1, round_decimals) + thresholds1 = numpy.round(thresholds1, round_decimals) return (base1 - thresholds1 >= 0).sum(axis=1) - 1 diff --git a/policyengine_core/tools/simulation_dumper.py b/policyengine_core/tools/simulation_dumper.py index dd0c0ad6b..4e2f8a183 100644 --- a/policyengine_core/tools/simulation_dumper.py +++ b/policyengine_core/tools/simulation_dumper.py @@ -95,7 +95,8 @@ def _dump_entity(population, directory): else: encoded_roles = np.select( [population.members_role == role for role in flattened_roles], - [role.key for role in flattened_roles], + [str(role.key) for role in flattened_roles], + default="unknown", ) np.save(os.path.join(path, "members_role.npy"), encoded_roles) diff --git a/setup.py b/setup.py index f74e5d010..49a611ba5 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ general_requirements = [ "pytest>=8,<9", - "numpy~=1.26.4", + "numpy~=2.1.0", "sortedcontainers<3", "numexpr<3", "dpath<3", @@ -25,6 +25,7 @@ "pyvis>=0.3.2", "microdf_python>=0.4.3", "huggingface_hub>=0.25.1", + "standard-imghdr", ] dev_requirements = [ @@ -60,6 +61,7 @@ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering :: Information Analysis", ], description="Core microsimulation engine enabling country-specific policy models.", diff --git a/tests/core/commons/test_formulas.py b/tests/core/commons/test_formulas.py index db22fc477..ecd3e1828 100644 --- a/tests/core/commons/test_formulas.py +++ b/tests/core/commons/test_formulas.py @@ -79,3 +79,13 @@ def test_switch_when_values_are_empty(): with pytest.raises(AssertionError): assert commons.switch(conditions, value_by_condition) + + +def test_concat_tuple_inputs(): + with pytest.raises(TypeError, match="First argument must not be a tuple."): + commons.concat(("a", "b"), numpy.array(["c", "d"])) + + with pytest.raises( + TypeError, match="Second argument must not be a tuple." + ): + commons.concat(numpy.array(["a", "b"]), ("c", "d")) diff --git a/tests/core/parameters_fancy_indexing/test_fancy_indexing.py b/tests/core/parameters_fancy_indexing/test_fancy_indexing.py index d753586f0..476a4f383 100644 --- a/tests/core/parameters_fancy_indexing/test_fancy_indexing.py +++ b/tests/core/parameters_fancy_indexing/test_fancy_indexing.py @@ -114,5 +114,10 @@ class TypesZone(Enum): z1 = "Zone 1" z2 = "Zone 2" - zone = np.asarray([TypesZone.z1, TypesZone.z2, TypesZone.z2, TypesZone.z1]) + zone = np.asarray( + [ + z.name + for z in [TypesZone.z1, TypesZone.z2, TypesZone.z2, TypesZone.z1] + ] + ) assert_near(P.single.owner[zone], [100, 200, 200, 100]) diff --git a/tests/core/test_tracers.py b/tests/core/test_tracers.py index 266b79823..66410692f 100644 --- a/tests/core/test_tracers.py +++ b/tests/core/test_tracers.py @@ -403,7 +403,8 @@ def test_log_aggregate(tracer): lines = tracer.computation_log.lines(aggregate=True) assert ( - lines[0] == " A<2017, (default)> = {'avg': 1.0, 'max': 1, 'min': 1}" + lines[0].strip() + == "A<2017, (default)> = {'avg': np.float64(1.0), 'max': np.int64(1), 'min': np.int64(1)}" )