diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 92c740362..a86c3bd0d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,7 +9,7 @@ jobs: max-parallel: 4 matrix: os: [ubuntu-latest, macOS-latest, windows-latest] - python-version: ["3.11", "3.12"] + python-version: ["3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v2 diff --git a/axelrod/fingerprint.py b/axelrod/fingerprint.py index 140e88604..aa58609f8 100644 --- a/axelrod/fingerprint.py +++ b/axelrod/fingerprint.py @@ -48,7 +48,7 @@ def _create_points(step: float, progress_bar: bool = True) -> List[Point]: points = [] for x in np.linspace(0, 1, num): for y in np.linspace(0, 1, num): - points.append(Point(x, y)) + points.append(Point(float(x), float(y))) if progress_bar: p_bar.update() @@ -177,11 +177,13 @@ def _generate_data(interactions: dict, points: list, edges: list) -> dict: the values are the mean score for the corresponding interactions. """ edge_scores = [ - np.mean( - [ - compute_final_score_per_turn(scores)[0] - for scores in interactions[edge] - ] + float( + np.mean( + [ + compute_final_score_per_turn(scores)[0] + for scores in interactions[edge] + ] + ) ) for edge in edges ] @@ -212,7 +214,7 @@ def _reshape_data(data: dict, points: list, size: int) -> np.ndarray: the standard origin. """ ordered_data = [data[point] for point in points] - shaped_data = np.reshape(ordered_data, (size, size), order="F") + shaped_data: np.ndarray = np.reshape(ordered_data, (size, size), order="F") plotting_data = np.flipud(shaped_data) return plotting_data @@ -525,9 +527,9 @@ def analyse_cooperation_ratio(filename): opponent in each turn. The ith row corresponds to the ith opponent and the jth column the jth turn. """ - did_c = np.vectorize( - lambda actions: [int(action == "C") for action in actions] - ) + + def did_c(actions): + return [int(action == "C") for action in actions] cooperation_rates = {} df = dd.read_csv(filename) diff --git a/axelrod/game.py b/axelrod/game.py index c2b4ffc44..143e89202 100644 --- a/axelrod/game.py +++ b/axelrod/game.py @@ -75,7 +75,10 @@ def get_value(x): row, col = map(get_value, pair) - return (self.A[row][col], self.B[row][col]) + # Use `.item()` to return native Python scalars rather than NumPy + # scalars, whose repr (`np.int64(3)`) would otherwise leak into the + # string representation of scores. + return (self.A[row][col].item(), self.B[row][col].item()) def __repr__(self) -> str: return "Axelrod game with matrices: {}".format((self.A, self.B)) diff --git a/axelrod/moran.py b/axelrod/moran.py index 539d3d5db..0b7ea7320 100644 --- a/axelrod/moran.py +++ b/axelrod/moran.py @@ -189,6 +189,7 @@ def fitness_proportionate_selection( An index of the above list selected at random proportionally to the list element divided by the total. """ + csums: np.ndarray if fitness_transformation is None: csums = np.cumsum(scores) else: diff --git a/axelrod/plot.py b/axelrod/plot.py index 23f5b7c9c..8e5647a6d 100644 --- a/axelrod/plot.py +++ b/axelrod/plot.py @@ -5,7 +5,7 @@ import matplotlib.pyplot as plt import matplotlib.transforms as transforms import tqdm -from numpy import arange, median, nan_to_num +from numpy import arange, median, nan_to_num, ndarray from .load_data_ import axl_filename from .result_set import ResultSet @@ -46,7 +46,7 @@ def _violinplot( width = max(self.num_players / 3, 12) height = width / 2 spacing = 4 - positions = spacing * arange(1, self.num_players + 1, 1) + positions: ndarray = spacing * arange(1, self.num_players + 1, 1) figure.set_size_inches(width, height) ax.violinplot( data, diff --git a/axelrod/result_set.py b/axelrod/result_set.py index 3d0934a94..4143cb395 100644 --- a/axelrod/result_set.py +++ b/axelrod/result_set.py @@ -297,14 +297,15 @@ def _build_summary_matrix(self, attribute, func=np.mean): for player_index, opponent_index in pairs: utilities = attribute[player_index][opponent_index] if utilities: - matrix[player_index][opponent_index] = func(utilities) + matrix[player_index][opponent_index] = float(func(utilities)) return matrix @update_progress_bar def _build_payoff_diffs_means(self): payoff_diffs_means = [ - [np.mean(diff) for diff in player] for player in self.score_diffs + [float(np.mean(diff)) for diff in player] + for player in self.score_diffs ] return payoff_diffs_means @@ -432,7 +433,7 @@ def _build_normalised_cooperation(self): with warnings.catch_warnings(): warnings.simplefilter("ignore") normalised_cooperation = [ - list(np.nan_to_num(row)) + [float(value) for value in np.nan_to_num(row)] for row in np.array(self.cooperation) / sum(map(np.array, self.match_lengths)) ] @@ -448,12 +449,13 @@ def _build_initial_cooperation_rate(self, interactions_series): ) with warnings.catch_warnings(): warnings.simplefilter("ignore") - initial_cooperation_rate = list( - np.nan_to_num( + initial_cooperation_rate = [ + float(rate) + for rate in np.nan_to_num( np.array(self.initial_cooperation_count) / interactions_array ) - ) + ] return initial_cooperation_rate @update_progress_bar @@ -706,8 +708,10 @@ def summarise(self): """ - median_scores = map(np.nanmedian, self.normalised_scores) - median_wins = map(np.nanmedian, self.wins) + median_scores = [ + float(np.nanmedian(scores)) for scores in self.normalised_scores + ] + median_wins = [float(np.nanmedian(wins)) for wins in self.wins] original_index = [index for index, _player in enumerate(self.players)] @@ -756,7 +760,7 @@ def summarise(self): ] if len(counts) > 0: - rate = np.mean(counts) + rate = float(np.mean(counts)) else: rate = 0 diff --git a/axelrod/strategies/ann.py b/axelrod/strategies/ann.py index 4fd764c67..5b93b2fc5 100644 --- a/axelrod/strategies/ann.py +++ b/axelrod/strategies/ann.py @@ -210,7 +210,7 @@ def __init__( def _process_weights(self, weights, num_features, num_hidden): self.weights = list(weights) - (i2h, h2o, bias) = split_weights(weights, num_features, num_hidden) + i2h, h2o, bias = split_weights(weights, num_features, num_hidden) self.input_to_hidden_layer_weights = np.array(i2h) self.hidden_to_output_layer_weights = np.array(h2o) self.bias_weights = np.array(bias) diff --git a/axelrod/strategies/cycler.py b/axelrod/strategies/cycler.py index 2eb27f206..9e7ad7823 100644 --- a/axelrod/strategies/cycler.py +++ b/axelrod/strategies/cycler.py @@ -95,6 +95,28 @@ def strategy(self, opponent: Player) -> Action: """Actual strategy definition that determines player's action.""" return next(self.cycle_iter) + def __getstate__(self): + """Used for pickling. + + The `cycle_iter` attribute is an `itertools.cycle` object, which + cannot be pickled from Python 3.14 onwards. We drop it here and + rebuild it in `__setstate__`.""" + state = self.__dict__.copy() + del state["cycle_iter"] + return state + + def __setstate__(self, state): + """Used for unpickling, rebuilding the dropped `cycle_iter`. + + A fresh `itertools.cycle` starts at the beginning of the cycle, so we + advance it to the position reached before pickling. The strategy + consumes one action per turn, hence the position is the number of + turns played modulo the cycle length.""" + self.__dict__.update(state) + self.set_cycle(cycle=self.cycle) + for _ in range(len(self.history) % len(self.cycle)): + next(self.cycle_iter) + def set_cycle(self, cycle: str): """Set or change the cycle.""" self.cycle = cycle diff --git a/axelrod/strategies/memoryone.py b/axelrod/strategies/memoryone.py index 6fe03c151..17f29a1a0 100644 --- a/axelrod/strategies/memoryone.py +++ b/axelrod/strategies/memoryone.py @@ -197,7 +197,7 @@ def set_initial_four_vector(self, four_vector): pass def receive_match_attributes(self): - (R, P, S, T) = self.match_attributes["game"].RPST() + R, P, S, T = self.match_attributes["game"].RPST() if self.p is None: self.p = min(1 - (T - R) / (R - S), (R - P) / (T - P)) four_vector = [1, self.p, 1, self.p] diff --git a/axelrod/strategies/qlearner.py b/axelrod/strategies/qlearner.py index 1a81ae227..50719654b 100644 --- a/axelrod/strategies/qlearner.py +++ b/axelrod/strategies/qlearner.py @@ -52,7 +52,7 @@ def __init__(self) -> None: self.prev_state = "" def receive_match_attributes(self): - (R, P, S, T) = self.match_attributes["game"].RPST() + R, P, S, T = self.match_attributes["game"].RPST() self.payoff_matrix = {C: {C: R, D: S}, D: {C: T, D: P}} def strategy(self, opponent: Player) -> Action: diff --git a/axelrod/strategies/zero_determinant.py b/axelrod/strategies/zero_determinant.py index 88e6cd22e..1bcef8158 100644 --- a/axelrod/strategies/zero_determinant.py +++ b/axelrod/strategies/zero_determinant.py @@ -134,7 +134,7 @@ def __init__(self, phi: float = 1 / 9, s: float = 0.5) -> None: super().__init__(phi, s, None) def receive_match_attributes(self): - (R, P, S, T) = self.match_attributes["game"].RPST() + R, P, S, T = self.match_attributes["game"].RPST() self.l = P super().receive_match_attributes() @@ -228,7 +228,7 @@ def __init__(self, phi: float = 0.25, s: float = 0.5) -> None: super().__init__(phi, s, None) def receive_match_attributes(self): - (R, P, S, T) = self.match_attributes["game"].RPST() + R, P, S, T = self.match_attributes["game"].RPST() self.l = R super().receive_match_attributes() diff --git a/axelrod/tests/property.py b/axelrod/tests/property.py index 81f960706..5ae8740f3 100644 --- a/axelrod/tests/property.py +++ b/axelrod/tests/property.py @@ -377,10 +377,10 @@ def games(draw, prisoners_dilemma=True, max_value=100): p = draw(integers(min_value=p_lower_bound, max_value=p_upper_bound)) else: - s = draw(integers(max_value=max_value)) - t = draw(integers(max_value=max_value)) - r = draw(integers(max_value=max_value)) - p = draw(integers(max_value=max_value)) + s = draw(integers(min_value=-max_value, max_value=max_value)) + t = draw(integers(min_value=-max_value, max_value=max_value)) + r = draw(integers(min_value=-max_value, max_value=max_value)) + p = draw(integers(min_value=-max_value, max_value=max_value)) game = axl.Game(r=r, s=s, t=t, p=p) return game diff --git a/axelrod/tests/strategies/test_memoryone.py b/axelrod/tests/strategies/test_memoryone.py index 4ae9d9340..6d6338b8b 100644 --- a/axelrod/tests/strategies/test_memoryone.py +++ b/axelrod/tests/strategies/test_memoryone.py @@ -81,7 +81,7 @@ def test_strategy2(self): ) def test_four_vector(self): - (R, P, S, T) = axl.Game().RPST() + R, P, S, T = axl.Game().RPST() p = min(1 - (T - R) / (R - S), (R - P) / (T - P)) expected_dictionary = {(C, C): 1.0, (C, D): p, (D, C): 1.0, (D, D): p} test_four_vector(self, expected_dictionary) diff --git a/axelrod/tests/strategies/test_qlearner.py b/axelrod/tests/strategies/test_qlearner.py index 07f3eb30e..a5354c9a9 100644 --- a/axelrod/tests/strategies/test_qlearner.py +++ b/axelrod/tests/strategies/test_qlearner.py @@ -22,7 +22,7 @@ class TestRiskyQLearner(TestPlayer): } def test_payoff_matrix(self): - (R, P, S, T) = axl.Game().RPST() + R, P, S, T = axl.Game().RPST() payoff_matrix = {C: {C: R, D: S}, D: {C: T, D: P}} player = self.player() self.assertEqual(player.payoff_matrix, payoff_matrix) diff --git a/axelrod/tests/unit/test_game.py b/axelrod/tests/unit/test_game.py index e124c9e18..a32cae237 100644 --- a/axelrod/tests/unit/test_game.py +++ b/axelrod/tests/unit/test_game.py @@ -43,7 +43,12 @@ def test_not_default_equality(self): def test_wrong_class_equality(self): self.assertNotEqual(axl.Game(), "wrong class") - @given(r=integers(), p=integers(), s=integers(), t=integers()) + @given( + r=integers(min_value=-1000, max_value=1000), + p=integers(min_value=-1000, max_value=1000), + s=integers(min_value=-1000, max_value=1000), + t=integers(min_value=-1000, max_value=1000), + ) @settings(max_examples=5) def test_random_init(self, r, p, s, t): """Test init with random scores using the hypothesis library.""" @@ -56,14 +61,24 @@ def test_random_init(self, r, p, s, t): game = axl.Game(r, s, t, p) self.assertEqual(game.scores, expected_scores) - @given(r=integers(), p=integers(), s=integers(), t=integers()) + @given( + r=integers(min_value=-1000, max_value=1000), + p=integers(min_value=-1000, max_value=1000), + s=integers(min_value=-1000, max_value=1000), + t=integers(min_value=-1000, max_value=1000), + ) @settings(max_examples=5) def test_random_RPST(self, r, p, s, t): """Test RPST method with random scores using the hypothesis library.""" game = axl.Game(r, s, t, p) self.assertEqual(game.RPST(), (r, p, s, t)) - @given(r=integers(), p=integers(), s=integers(), t=integers()) + @given( + r=integers(min_value=-1000, max_value=1000), + p=integers(min_value=-1000, max_value=1000), + s=integers(min_value=-1000, max_value=1000), + t=integers(min_value=-1000, max_value=1000), + ) @settings(max_examples=5) def test_random_score(self, r, p, s, t): """Test score method with random scores using the hypothesis library.""" diff --git a/axelrod/tests/unit/test_pickling.py b/axelrod/tests/unit/test_pickling.py index dff35ba96..c22f1c2ae 100644 --- a/axelrod/tests/unit/test_pickling.py +++ b/axelrod/tests/unit/test_pickling.py @@ -392,7 +392,11 @@ class LocalCooperator(axl.Cooperator): un_transformed = LocalCooperator() - self.assertRaises(AttributeError, pickle.dumps, un_transformed) + # Pickling a local class raises AttributeError up to Python 3.12 and + # PicklingError from Python 3.13 onwards. + self.assertRaises( + (AttributeError, pickle.PicklingError), pickle.dumps, un_transformed + ) player = axl.strategy_transformers.FlipTransformer()(LocalCooperator)() pickled = pickle.dumps(player) diff --git a/docs/requirements.txt b/docs/requirements.txt index 0f4be075a..31f3a0022 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ docutils>=0.18.1 -numpy==1.24.3 # numpy isn't mocked due to complex use in doctests +numpy>=2.1 # numpy isn't mocked due to complex use in doctests mock>=5.1.0 torch>=2.6.0 \ No newline at end of file diff --git a/docs/tutorials/running_axelrods_first_tournament/index.rst b/docs/tutorials/running_axelrods_first_tournament/index.rst index 7f49e30cd..f2bee2409 100644 --- a/docs/tutorials/running_axelrods_first_tournament/index.rst +++ b/docs/tutorials/running_axelrods_first_tournament/index.rst @@ -123,29 +123,28 @@ given in [Axelrod1980]_ and there is no source code to base this on. This leads to some strategies being ambiguous. These are all clearly explained in the strategy docstrings. For example:: - >>> print(axl.FirstByAnonymous.__doc__) + >>> import inspect + >>> print(inspect.getdoc(axl.FirstByAnonymous)) + Submitted to Axelrod's first tournament by a graduate student whose name was + withheld. - Submitted to Axelrod's first tournament by a graduate student whose name was - withheld. + The description written in [Axelrod1980]_ is: - The description written in [Axelrod1980]_ is: + > "This rule has a probability of cooperating, P, which is initially 30% and + > is updated every 10 moves. P is adjusted if the other player seems random, + > very cooperative, or very uncooperative. P is also adjusted after move 130 + > if the rule has a lower score than the other player. Unfortunately, the + > complex process of adjustment frequently left the probability of cooperation + > in the 30% to 70% range, and therefore the rule appeared random to many + > other players." - > "This rule has a probability of cooperating, P, which is initially 30% and - > is updated every 10 moves. P is adjusted if the other player seems random, - > very cooperative, or very uncooperative. P is also adjusted after move 130 - > if the rule has a lower score than the other player. Unfortunately, the - > complex process of adjustment frequently left the probability of cooperation - > in the 30% to 70% range, and therefore the rule appeared random to many - > other players." + Given the lack of detail this strategy is implemented based on the final + sentence of the description which is to have a cooperation probability that + is uniformly random in the 30 to 70% range. - Given the lack of detail this strategy is implemented based on the final - sentence of the description which is to have a cooperation probability that - is uniformly random in the 30 to 70% range. - - Names: - - - (Name withheld): [Axelrod1980]_ + Names: + - (Name withheld): [Axelrod1980]_ Other outcomes -------------- diff --git a/pyproject.toml b/pyproject.toml index 3d8ad555c..5c7bd475b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,8 @@ classifiers = [ "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "License :: OSI Approved :: MIT License", ] dependencies = [ @@ -26,7 +28,7 @@ dependencies = [ "dask[dataframe]>=2.9.2", "fsspec>=0.6.0", "matplotlib>=3.0.3", - "numpy>=1.26.4", + "numpy>=2.1", "pandas>=1.0.0", "pyyaml>=5.1", "scipy>=1.3.3", diff --git a/tox.ini b/tox.ini index 5df968b98..ab46d7c7d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,13 @@ [tox] isolated_build = True -envlist = py311, py312 +envlist = py311, py312, py313, py314 [gh-actions] python = 3.11: py311 3.12: py312 + 3.13: py313 + 3.14: py314 [flake8] per-file-ignores = @@ -27,14 +29,14 @@ deps = pytest-sugar isort black - numpy==1.26.4 - torch==2.6.0 + numpy>=2.1 + torch>=2.6.0 mypy types-setuptools commands = - python -m pytest --cov-report term-missing --cov=axelrod --cov-fail-under=100 . --doctest-glob="*.md" --doctest-glob="*.rst" python -m black -l 80 . --check python -m isort --check-only axelrod/. python run_mypy.py python run_strategy_indexer.py + python -m pytest --cov-report term-missing --cov=axelrod --cov-fail-under=100 . --doctest-glob="*.md" --doctest-glob="*.rst" \ No newline at end of file