diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ccca6262f..7efad94fc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,16 +30,16 @@ repos: rev: v2.4.1 hooks: - id: codespell - args: ["-L", "larg"] exclude: > (?x)^( .*\.txt| + .*\.svg| .*\.ipynb )$ # Clang format - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v20.1.7 + rev: v20.1.8 hooks: - id: clang-format types_or: [c++, c, cuda] @@ -55,15 +55,16 @@ repos: # Ruff, the Python auto-correcting linter/formatter written in Rust - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.2 + rev: v0.12.3 hooks: - id: ruff - args: ["--fix", "--show-fixes", "--exclude=__init__.py"] + args: ["--fix", "--show-fixes"] - id: ruff-format - # Black format Python and notebooks - - repo: https://github.com/psf/black - rev: 25.1.0 + # Format docstrings + - repo: https://github.com/DanielNoord/pydocstringformatter + rev: v0.7.3 hooks: - - id: black-jupyter + - id: pydocstringformatter + args: ["--style=numpydoc"] ... diff --git a/pyproject.toml b/pyproject.toml index fed528d4a..1520940ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,82 @@ [build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta" + +[tool.ruff] +indent-width = 4 +line-length = 88 +target-version = "py39" +extend-include = ["*.ipynb"] + +lint.select = [ + "ANN", # flake8-annotations + "ARG", # flake8-unused-arguments + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "C90", # mccabe +# "D", # pydocstyle + "DTZ", # flake8-datetimez + "E", # pycodestyle + "EM", # flake8-errmsg + "ERA", # eradicate + "F", # Pyflakes + "I", # isort + "ICN", # flake8-import-conventions + "N", # pep8-naming + "PD", # pandas-vet + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PL", # Pylint + "PLC", # Pylint + "PLE", # Pylint + "PLR", # Pylint + "PLW", # Pylint + "PT", # flake8-pytest-style + "Q", # flake8-quotes + "RET", # flake8-return + "RUF100", # Ruff-specific + "S", # flake8-bandit + "SIM", # flake8-simplify +# "T20", # flake8-print + "TID", # flake8-tidy-imports + "UP", # pyupgrade + "W", # pycodestyle + "YTT", # flake8-2020 +] + +exclude = [ + "__init__.py", + "setup.py", +] + +lint.ignore = [ + "S301", # pickle + "S311", # Standard pseudo-random generators +] + +[tool.ruff.lint.pep8-naming] +extend-ignore-names = [ + "X", "X1", "X_train", "X_val", "X_test", "X_predict", +] + +[tool.ruff.lint.pydocstyle] +convention = "numpy" + +[tool.ruff.format] +docstring-code-format = true +docstring-code-line-length = 80 + +[tool.ruff.lint.extend-per-file-ignores] +"test/**" = ["S101", "PLR2004", "ANN"] +"python/example_rmux.py" = ["PLR2004"] +"python/example_maze.py" = ["PLR2004"] +"python/notebooks/example_cartpole.ipynb" = ["E501", "SIM115"] +"python/notebooks/example_maze.ipynb" = ["E501", "PLR2004"] +"python/notebooks/example_regression.ipynb" = ["PLR2004"] +"python/notebooks/example_rmux.ipynb" = ["PLR2004"] +"python/notebooks/example_tuning.ipynb" = ["E501", "ANN201"] + +[tool.codespell] +ignore-words-list = [ + "larg", +] diff --git a/python/example_cartpole.py b/python/example_cartpole.py index c5b37735a..07cae850d 100644 --- a/python/example_cartpole.py +++ b/python/example_cartpole.py @@ -117,7 +117,7 @@ def replay(replay_size: int = 5000) -> None: - """Performs experience replay updates""" + """Performs experience replay updates.""" batch_size: int = min(len(memory), replay_size) batch = random.sample(memory, batch_size) for state, action, reward, next_state, done in batch: @@ -133,7 +133,7 @@ def replay(replay_size: int = 5000) -> None: def egreedy_action(state: np.ndarray) -> int: - """Selects an action using an epsilon greedy policy""" + """Selects an action using an epsilon greedy policy.""" if np.random.rand() < epsilon: return random.randrange(N_ACTIONS) prediction_array = xcs.predict(state.reshape(1, -1))[0] @@ -143,7 +143,7 @@ def egreedy_action(state: np.ndarray) -> int: def episode() -> tuple[float, int]: - """Executes a single episode, saving to memory buffer""" + """Executes a single episode, saving to memory buffer.""" episode_score: float = 0 episode_steps: int = 0 state: np.ndarray = env.reset()[0] diff --git a/python/example_maze.py b/python/example_maze.py index e24b35e90..cfe139af3 100644 --- a/python/example_maze.py +++ b/python/example_maze.py @@ -283,7 +283,7 @@ def run_experiment(env: Maze) -> None: bar.close() -def plot_performance(env: Maze): +def plot_performance(env: Maze) -> None: """Plots learning performance.""" plt.figure(figsize=(10, 6)) plt.plot(trials, steps) diff --git a/python/example_rmux.py b/python/example_rmux.py index 7336a3d36..e82e19904 100644 --- a/python/example_rmux.py +++ b/python/example_rmux.py @@ -101,13 +101,12 @@ def execute(self, act: int) -> tuple[bool, float]: reward += 0.1 else: reward = self.max_payoff + elif self.payoff_map: + reward = (pos - self.pos_bits) * 0.2 + if answer == 1: + reward += 0.1 else: - if self.payoff_map: - reward = (pos - self.pos_bits) * 0.2 - if answer == 1: - reward += 0.1 - else: - reward = 0 + reward = 0 return correct, reward diff --git a/python/notebooks/example_cartpole.ipynb b/python/notebooks/example_cartpole.ipynb index 75868fed4..c02890d59 100644 --- a/python/notebooks/example_cartpole.ipynb +++ b/python/notebooks/example_cartpole.ipynb @@ -33,12 +33,12 @@ "import random\n", "from collections import deque\n", "\n", - "from matplotlib import rcParams\n", - "import matplotlib.pyplot as plt\n", - "import imageio\n", "import gymnasium as gym\n", + "import imageio\n", + "import matplotlib.pyplot as plt\n", "import numpy as np\n", - "from IPython.display import display, Image\n", + "from IPython.display import Image, display\n", + "from matplotlib import rcParams\n", "from tqdm import tqdm\n", "\n", "import xcsf\n", @@ -625,12 +625,12 @@ } ], "source": [ - "annotated_frames = list()\n", + "annotated_frames = []\n", "\n", "if SAVE_GIF:\n", " # add score and episode nr\n", " rcParams[\"font.family\"] = \"monospace\"\n", - " bbox = dict(boxstyle=\"round\", fc=\"0.8\")\n", + " bbox = {\"boxstyle\": \"round\", \"fc\": \"0.8\"}\n", " bar = tqdm(total=len(frames), position=0, leave=True)\n", " for i in range(len(frames)):\n", " fig = plt.figure(dpi=90)\n", diff --git a/python/notebooks/example_classification.ipynb b/python/notebooks/example_classification.ipynb index b54b499bf..58075916d 100644 --- a/python/notebooks/example_classification.ipynb +++ b/python/notebooks/example_classification.ipynb @@ -21,8 +21,11 @@ "import matplotlib.pyplot as plt\n", "import numpy as np\n", "from sklearn.datasets import fetch_openml\n", - "from sklearn.metrics import ConfusionMatrixDisplay\n", - "from sklearn.metrics import confusion_matrix, classification_report\n", + "from sklearn.metrics import (\n", + " ConfusionMatrixDisplay,\n", + " classification_report,\n", + " confusion_matrix,\n", + ")\n", "from sklearn.model_selection import train_test_split\n", "from sklearn.preprocessing import MinMaxScaler, OneHotEncoder\n", "\n", diff --git a/python/notebooks/example_maze.ipynb b/python/notebooks/example_maze.ipynb index ff4508b8e..35fde4e94 100644 --- a/python/notebooks/example_maze.ipynb +++ b/python/notebooks/example_maze.ipynb @@ -486,7 +486,7 @@ } ], "source": [ - "def plot_performance(env: Maze):\n", + "def plot_performance(env: Maze) -> None:\n", " \"\"\"Plots learning performance.\"\"\"\n", " plt.figure(figsize=(10, 6))\n", " plt.plot(trials, steps)\n", @@ -534,9 +534,10 @@ ], "source": [ "import io\n", - "from PIL import Image\n", "from turtle import Screen, Turtle\n", + "\n", "from IPython.display import display\n", + "from PIL import Image\n", "\n", "GRID_WIDTH: int = maze.x_size\n", "GRID_HEIGHT: int = maze.y_size\n", diff --git a/python/notebooks/example_regression.ipynb b/python/notebooks/example_regression.ipynb index 8f447a42d..664cf0c00 100644 --- a/python/notebooks/example_regression.ipynb +++ b/python/notebooks/example_regression.ipynb @@ -21,7 +21,7 @@ "import graphviz\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", - "from IPython.display import display, Image\n", + "from IPython.display import Image, display\n", "from sklearn.datasets import fetch_openml\n", "from sklearn.ensemble import RandomForestRegressor\n", "from sklearn.gaussian_process import GaussianProcessRegressor\n", diff --git a/python/notebooks/example_rmux.ipynb b/python/notebooks/example_rmux.ipynb index fe006d0e9..1e86b71a4 100644 --- a/python/notebooks/example_rmux.ipynb +++ b/python/notebooks/example_rmux.ipynb @@ -109,13 +109,12 @@ " reward += 0.1\n", " else:\n", " reward = self.max_payoff\n", + " elif self.payoff_map:\n", + " reward = (pos - self.pos_bits) * 0.2\n", + " if answer == 1:\n", + " reward += 0.1\n", " else:\n", - " if self.payoff_map:\n", - " reward = (pos - self.pos_bits) * 0.2\n", - " if answer == 1:\n", - " reward += 0.1\n", - " else:\n", - " reward = 0\n", + " reward = 0\n", " return correct, reward" ] }, diff --git a/python/notebooks/example_seeding.ipynb b/python/notebooks/example_seeding.ipynb index 4c6133335..14f098afa 100644 --- a/python/notebooks/example_seeding.ipynb +++ b/python/notebooks/example_seeding.ipynb @@ -14,6 +14,7 @@ "outputs": [], "source": [ "import json\n", + "\n", "import numpy as np\n", "\n", "import xcsf\n", diff --git a/python/notebooks/example_tuning.ipynb b/python/notebooks/example_tuning.ipynb index 044d85064..96100488c 100644 --- a/python/notebooks/example_tuning.ipynb +++ b/python/notebooks/example_tuning.ipynb @@ -693,12 +693,12 @@ "source": [ "# General parameters can be searched in the usual way\n", "\n", - "parameters = dict(beta=[0.1, 0.5])\n", + "parameters = {\"beta\": [0.1, 0.5]}\n", "\n", "# EA parameters require specifying a dict,\n", "# but individual values can still be set because the other values are not changed.\n", "\n", - "parameters = dict(ea=[{\"lambda\": 2}, {\"lambda\": 10}])\n", + "parameters = {\"ea\": [{\"lambda\": 2}, {\"lambda\": 10}]}\n", "\n", "# However, for actions, conditions, and predictions, the WHOLE dict must be specified\n", "# for each value to try in the search. See Optuna below.\n", @@ -752,7 +752,7 @@ } ], "source": [ - "def objective(trial):\n", + "def objective(trial: optuna.trial.Trial) -> float:\n", " \"\"\"Measure a new hyperparameter combination.\"\"\"\n", " # get new parameters to try\n", " e0: float = trial.suggest_float(\"e0\", 0.08, 0.12, step=0.01)\n", diff --git a/test/python/test_xcsf.py b/test/python/test_xcsf.py index 15fd1a0f9..78b42f919 100644 --- a/test/python/test_xcsf.py +++ b/test/python/test_xcsf.py @@ -20,18 +20,18 @@ from __future__ import annotations -from collections import namedtuple - import json +import numbers import os import pickle -import numbers +from collections import namedtuple +from copy import deepcopy + import numpy as np import pytest -from copy import deepcopy +from sklearn.datasets import make_regression from sklearn.model_selection import train_test_split from sklearn.preprocessing import MinMaxScaler -from sklearn.datasets import make_regression import xcsf @@ -99,9 +99,8 @@ def dicts_equal(d1: dict, d2: dict) -> bool: if isinstance(value, dict): if not isinstance(d2[key], dict) or not dicts_equal(value, d2[key]): return False - else: - if d2[key] != value: - return False + elif d2[key] != value: + return False return True @@ -322,13 +321,15 @@ def _compare_dicts(d1, d2, path=""): return diffs -def _test_pop_replace(tmp_path, pop_init, clean, fitinbetween, warm_start): - N = 500 - DX = 3 - X = np.random.random((N, DX)) - y = np.random.randn(N, 1) +def _test_pop_replace( + tmp_path: str, pop_init: bool, clean: bool, fitinbetween: bool, warm_start: bool +) -> bool: + n = 500 + dx = 3 + X = np.random.random((n, dx)) + y = np.random.randn(n, 1) - xcs = xcsf.XCS(x_dim=DX, pop_size=5, max_trials=1000, pop_init=pop_init) + xcs = xcsf.XCS(x_dim=dx, pop_size=5, max_trials=1000, pop_init=pop_init) xcs.fit(X, y, verbose=False) # Initial, “too large” population. @@ -342,7 +343,7 @@ def _test_pop_replace(tmp_path, pop_init, clean, fitinbetween, warm_start): (tmp_path / "pset1.json").write_text(json1) if fitinbetween: - xcs.fit(X, y, warm_start=True, verbose=False) + xcs.fit(X, y, warm_start=warm_start, verbose=False) xcs.json_read(str(tmp_path / "pset1.json"), clean=clean) @@ -354,18 +355,17 @@ def _test_pop_replace(tmp_path, pop_init, clean, fitinbetween, warm_start): if len(list1) != len(list2): return False - else: - unequal = False - for cl1, cl2 in zip(list1, list2): - # If there is any difference, … - if _compare_dicts(cl1, cl2): - unequal = True - break - return not unequal + unequal = False + for cl1, cl2 in zip(list1, list2): + # If there is any difference, … + if _compare_dicts(cl1, cl2): + unequal = True + break + return not unequal @pytest.mark.parametrize( - "pop_init,clean,fitinbetween,warm_start", + ("pop_init", "clean", "fitinbetween", "warm_start"), [ (False, True, False, False), (False, True, True, False), @@ -375,9 +375,11 @@ def _test_pop_replace(tmp_path, pop_init, clean, fitinbetween, warm_start): (True, True, True, True), ], ) -def test_pop_replace(tmp_path, pop_init, clean, fitinbetween, warm_start): +def test_pop_replace( + tmp_path: str, pop_init: bool, clean: bool, fitinbetween: bool, warm_start: bool +): for seed in range(19): np.random.seed(seed) - assert _test_pop_replace( - tmp_path, pop_init, clean, fitinbetween, warm_start - ), f"failed at seed {seed}" + assert _test_pop_replace(tmp_path, pop_init, clean, fitinbetween, warm_start), ( + f"failed at seed {seed}" + ) diff --git a/xcsf/utils/viz.py b/xcsf/utils/viz.py index 28df6c3a7..4356d61b3 100644 --- a/xcsf/utils/viz.py +++ b/xcsf/utils/viz.py @@ -69,7 +69,7 @@ def label(self, symbol: str) -> str: if start == "feature" and int(end) < len(self.feature_names): return self.feature_names[int(end)] elif isinstance(symbol, float): - return "%.5f" % symbol + return f"{symbol:.5f}" return str(symbol) def read_function(self) -> str: @@ -132,8 +132,8 @@ def __init__( self.gviz = graphviz.Digraph("G", filename=filename + ".gv") self.draw() label: str = "" if note is None else note - label += "\nN = %d\n" % graph["n"] - label += "T = %d\n" % graph["t"] + label += "\nN = {graph['n']}\n" + label += f"T = {graph['t']}\n" label += "match node shaded\n" self.gviz.attr(label=label) self.gviz.view() @@ -145,7 +145,7 @@ def label(self, symbol: str) -> str: if start == "feature" and int(end) < len(self.feature_names): return self.feature_names[int(end)] elif isinstance(symbol, float): - return "%.5f" % symbol + return f"{symbol:.5f}" return str(symbol) def draw(self) -> None: @@ -157,7 +157,7 @@ def draw(self) -> None: for j in range(n_inputs): src = self.connectivity[(i * self.k) + j] if src < self.n_inputs: - feature = "feature_%d" % src + feature = f"feature_{src}" self.gviz.node(feature, label=self.label(feature), shape="square") self.gviz.edge(feature, str(i)) else: