diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..e04df56b --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,28 @@ +name: lint + +# Controls when the workflow will run +on: [push, pull_request, workflow_dispatch] + +jobs: + ruff: + runs-on: ubuntu-latest + + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v4 + + # Lint (includes import sorting). Ruff version is pinned so that the rule set + # does not change under our feet when a new ruff release adds default rules. + # Keep in sync with the ruff pin in pyproject.toml (dev extra). + - name: Ruff check + uses: astral-sh/ruff-action@v3 + with: + version: "0.17.3" + args: "check" + + # Fail if any file is not formatted + - name: Ruff format + uses: astral-sh/ruff-action@v3 + with: + version: "0.17.3" + args: "format --check --diff" diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 21c8fa9e..40c04682 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -88,7 +88,7 @@ jobs: set -o pipefail git config --global http.sslBackend schannel source /usr/local/Modules/init/sh - pytest --cov=. -cov-config=jade.coveragerc --cov-report xml + pytest --cov=. --cov-report xml env: ACCESS_TOKEN_GITHUB: ${{ secrets.ACCESS_TOKEN_GITHUB }} F4E_GITLAB_TOKEN: ${{ secrets.F4E_GITLAB_TOKEN }} diff --git a/AGENTS.md b/AGENTS.md index aced5992..b638d67b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -75,8 +75,8 @@ The same pattern applies to `Library`/`LibraryFactory`. `Input` subclasses are s ### Config construction: always via named classmethods ```python -run_cfg = RunConfig.from_root(paths_tree) # reads 4 YAML files -run_cfg = RunConfig.from_yamls(env_path, ...) # explicit paths +run_cfg = RunConfig.from_root(paths_tree) # reads 4 YAML files +run_cfg = RunConfig.from_yamls(env_path, ...) # explicit paths ``` Never construct config dataclasses directly. diff --git a/docs/source/conf.py b/docs/source/conf.py index 6bb94bb1..614c80c6 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -14,7 +14,6 @@ import sys sys.path.insert(0, os.path.abspath("../..")) -import importlib.metadata # -- Project information ----------------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index bb64c353..2f2e7b2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ classifiers = [ ] dependencies = [ "xlsxwriter < 4", + "python-docx < 2", "matplotlib < 4", "requests < 3", "f4enix >= 1.2.1, < 2", @@ -32,7 +33,7 @@ dev = [ "pytest < 10", "pytest-cov < 8", "pytest-xvfb < 4", # needed for headless display - "ruff", # best formatter that should be used by everybody + "ruff == 0.17.3", # keep in sync with .github/workflows/lint.yml ] docs = [ "sphinx < 10", @@ -40,7 +41,8 @@ docs = [ "sphinx-book-theme < 2", "sphinx-copybutton < 0.6", "sphinx-togglebutton < 0.5", - "sphinx-design < 0.8" + "sphinx-design < 0.8", + "pydata-sphinx-theme < 1", ] ui = [ "ttkthemes < 4" @@ -63,3 +65,7 @@ jade = 'src/jade' [tool.setuptools.package-data] "*" = ["*.txt", '*.csv', '*.sh', '*.cmd', '*.docx', '*.ico'] +[tool.ruff.lint] +# Import sorting is in the default rule set of the pinned ruff, but older +# versions (and editor-bundled ruff) do not enable it. Keep it explicit. +extend-select = ["I"] diff --git a/src/jade/app/app.py b/src/jade/app/app.py index 68b787fc..4de99d21 100644 --- a/src/jade/app/app.py +++ b/src/jade/app/app.py @@ -18,7 +18,7 @@ from jade.config.raw_config import ConfigRawProcessor from jade.config.run_config import RunConfig, RunMode from jade.config.status import GlobalStatus -from jade.helper.__optionals__ import TKINTER_AVAIL, OMC_AVAIL +from jade.helper.__optionals__ import OMC_AVAIL, TKINTER_AVAIL if TKINTER_AVAIL: from jade.gui.post_config_gui import PostConfigGUI @@ -147,8 +147,8 @@ def restore_default_cfg(self, msg: str = ""): def rmv_runtpe(self): """Remove the runtpe files from the simulation folders of mcnp.""" # Walk through the root folder and remove the runtpe files - for pathroot, folder, files in os.walk(self.tree.simulations): - if len(files) > 0 and len(folder) == 0: # only files and no folders + for pathroot, folder, filenames in os.walk(self.tree.simulations): + if len(filenames) > 0 and len(folder) == 0: # only files and no folders # get the code codelib = Path(pathroot).parent.parent.name code, _ = get_code_lib(codelib) @@ -203,7 +203,7 @@ def run_benchmarks(self, testing: bool = False) -> list[str] | None: def continue_run(self, testing: bool = False): """Continue the run of the benchmarks that were not completed.""" commands = [] - for bench_name, cfg in self.run_cfg.benchmarks.items(): + for cfg in self.run_cfg.benchmarks.values(): benchmark = BenchmarkRunFactory.create( cfg, self.tree.simulations, diff --git a/src/jade/app/fetch.py b/src/jade/app/fetch.py index 24284f71..a27e9acd 100644 --- a/src/jade/app/fetch.py +++ b/src/jade/app/fetch.py @@ -71,7 +71,7 @@ def _extract_zip(zip_content, dest: PathLike) -> PathLike: def fetch_from_gitlab( - url: str, path: str, branch: str, authorization_token: str = None + url: str, path: str, branch: str, authorization_token: str | None = None ) -> PathLike | bool: """Download a repository from GitLab and extract it to a temporary folder. It can also deal with authentication. Supported @@ -103,7 +103,11 @@ def fetch_from_gitlab( logger.error("Gitlab authentication failed") logger.error(e) return False - except requests.exceptions.RequestException as e: + # python-gitlab lets requests errors (e.g. unreachable host) propagate as they are + except ( + gitlab.exceptions.GitlabConnectionError, + requests.exceptions.RequestException, + ) as e: logger.error(f"Could not connect to {url}") logger.error(e) return False diff --git a/src/jade/config/atlas_config.py b/src/jade/config/atlas_config.py index b9c3b9e7..f20ec748 100644 --- a/src/jade/config/atlas_config.py +++ b/src/jade/config/atlas_config.py @@ -160,23 +160,23 @@ def __post_init__(self): # Ensure correct format of additional labels if present if self.additional_labels is not None: try: - for key in self.additional_labels.keys(): + for key in self.additional_labels: assert key in ["major", "minor"] assert isinstance(self.additional_labels[key], list) for label, position in self.additional_labels[key]: assert isinstance(label, str) - assert isinstance(position, float) or isinstance(position, int) + assert isinstance(position, (float, int)) except AssertionError: raise ConfigError("Invalid format for 'additional_labels'") # Ensure correct format of v_lines if present if self.v_lines is not None: try: - for key in self.v_lines.keys(): + for key in self.v_lines: assert key in ["major", "minor"] assert isinstance(self.v_lines[key], list) for line in self.v_lines[key]: - assert isinstance(line, float) or isinstance(line, int) + assert isinstance(line, (float, int)) except AssertionError: raise ConfigError("Invalid format for 'v_lines'") @@ -187,8 +187,8 @@ def __post_init__(self): assert len(rec) == 4 assert isinstance(rec[0], str) assert isinstance(rec[1], str) - assert isinstance(rec[2], float) or isinstance(rec[2], int) - assert isinstance(rec[3], float) or isinstance(rec[3], int) + assert isinstance(rec[2], (float, int)) + assert isinstance(rec[3], (float, int)) except AssertionError: raise ConfigError("Invalid format for 'recs'") diff --git a/src/jade/config/excel_config.py b/src/jade/config/excel_config.py index 902d2d5c..18d4cb55 100644 --- a/src/jade/config/excel_config.py +++ b/src/jade/config/excel_config.py @@ -116,7 +116,7 @@ def __post_init__(self): if self.conditional_formatting: assert len(self.conditional_formatting) == 3 for key in ["red", "yellow", "orange"]: - assert key in self.conditional_formatting.keys() + assert key in self.conditional_formatting @classmethod def from_dict(cls, dictionary: dict, name) -> TableConfig: diff --git a/src/jade/config/pp_config.py b/src/jade/config/pp_config.py index 72f22a47..2a31016d 100644 --- a/src/jade/config/pp_config.py +++ b/src/jade/config/pp_config.py @@ -13,14 +13,14 @@ def __init__(self, root_cfg_pp: PathLike): # get all available config excel processors excel_cfgs = {} for file in os.listdir(Path(root_cfg_pp, "excel")): - if file.endswith(".yaml") or file.endswith(".yml"): + if file.endswith((".yaml", ".yml")): cfg = ConfigExcelProcessor.from_yaml(Path(root_cfg_pp, "excel", file)) excel_cfgs[cfg.benchmark] = cfg self.excel_cfgs = excel_cfgs # Get all available config atlas processors atlas_cfgs = {} for file in os.listdir(Path(root_cfg_pp, "atlas")): - if file.endswith(".yaml") or file.endswith(".yml"): + if file.endswith((".yaml", ".yml")): cfg = ConfigAtlasProcessor.from_yaml(Path(root_cfg_pp, "atlas", file)) atlas_cfgs[cfg.benchmark] = cfg self.atlas_cfgs = atlas_cfgs diff --git a/src/jade/config/raw_config.py b/src/jade/config/raw_config.py index 2808a19d..877bf012 100644 --- a/src/jade/config/raw_config.py +++ b/src/jade/config/raw_config.py @@ -1,12 +1,12 @@ from __future__ import annotations +import re from dataclasses import dataclass from enum import Enum import yaml from jade.helper.aux_functions import PathLike -import re APPLY_TO_TAG = re.compile(r"apply_to#\d+") diff --git a/src/jade/config/run_config.py b/src/jade/config/run_config.py index 8d9dd0b0..102cb526 100644 --- a/src/jade/config/run_config.py +++ b/src/jade/config/run_config.py @@ -248,7 +248,6 @@ def get_lib_zaids(self) -> list[str]: list[str] list of zaids available in the library. Zaid numbers to be used """ - pass @dataclass diff --git a/src/jade/config/status.py b/src/jade/config/status.py index 8526cf30..4a2d24be 100644 --- a/src/jade/config/status.py +++ b/src/jade/config/status.py @@ -199,7 +199,7 @@ def get_all_raw(self) -> tuple[set[str], set[str]]: """ code_libs = [] benchmarks = [] - for code, lib, benchmark in self.raw_data.keys(): + for code, lib, benchmark in self.raw_data: code_libs.append(print_code_lib(code, lib)) benchmarks.append(benchmark) return set(code_libs), set(benchmarks) @@ -222,7 +222,7 @@ def get_codelibs_from_raw_benchmark(self, benchmarks: str | list[str]) -> set[st benchmarks = [benchmarks] codelibs = [] - for code, lib, bench in self.raw_data.keys(): + for code, lib, bench in self.raw_data: if bench in benchmarks: codelibs.append(print_code_lib(code, lib)) return set(codelibs) @@ -242,7 +242,7 @@ def get_benchmark_from_raw_codelib(self, codelibs: str | list[str]) -> set[str]: list of benchmarks for which the raw data of the requested codelib is available. """ benchmarks = [] - for code, lib, bench in self.raw_data.keys(): + for code, lib, bench in self.raw_data: if print_code_lib(code, lib) in codelibs: benchmarks.append(bench) return set(benchmarks) @@ -262,9 +262,7 @@ def is_raw_available(self, codelib: str, benchmark: str) -> bool: bool True if the raw data is available, False otherwise. """ - if codelib in self.get_codelibs_from_raw_benchmark(benchmark): - return True - return False + return codelib in self.get_codelibs_from_raw_benchmark(benchmark) def _validate_libs_processing( self, code: CODE, benchmark: str, libs: list[str] diff --git a/src/jade/helper/__optionals__.py b/src/jade/helper/__optionals__.py index be758274..413a3d04 100644 --- a/src/jade/helper/__optionals__.py +++ b/src/jade/helper/__optionals__.py @@ -4,19 +4,17 @@ # Handle OpenMC optional dependency with global flag. try: - import openmc + import openmc # noqa: F401 OMC_AVAIL = True except ImportError: OMC_AVAIL = False - logger.warning( - "OpenMC has not been installed - see JADE installation instructions" - ) + logger.warning("OpenMC has not been installed - see JADE installation instructions") try: - import tkinter as tk + import tkinter as tk # noqa: F401 - import ttkthemes + import ttkthemes # noqa: F401 TKINTER_AVAIL = True except ImportError: diff --git a/src/jade/helper/aux_functions.py b/src/jade/helper/aux_functions.py index a89ebc1c..2286388c 100644 --- a/src/jade/helper/aux_functions.py +++ b/src/jade/helper/aux_functions.py @@ -5,7 +5,7 @@ import re from abc import ABC, abstractmethod from pathlib import Path -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING import numpy as np import pandas as pd @@ -16,7 +16,7 @@ from jade.helper.constants import CODE from jade.post.sim_output import MCNPSimOutput, OpenMCSimOutput -PathLike = Union[str, os.PathLike, Path] +PathLike = str | os.PathLike | Path CODE_PATTERN = re.compile(r"_.+_-") LIB_PATTERN = re.compile(r"-_.+_") @@ -96,7 +96,6 @@ def check_success(self, files: list[str]) -> bool: bool True if the simulation completed successfully, False otherwise. """ - pass class MCNPChecker(SimulationChecker): @@ -208,6 +207,6 @@ def same_index( continue else: return False - except Exception: + except (ValueError, TypeError): return False return True diff --git a/src/jade/helper/openmc.py b/src/jade/helper/openmc.py index 9fcdfe70..6683fe00 100644 --- a/src/jade/helper/openmc.py +++ b/src/jade/helper/openmc.py @@ -107,9 +107,9 @@ def from_volume_density( cell_volumes: dict[int, float], cell_densities: dict[int, float] ) -> dict[int, float]: cell_masses = {} - for cell in cell_volumes: + for cell, volume in cell_volumes.items(): if cell in cell_densities: - cell_masses[cell] = cell_volumes[cell] * cell_densities[cell] + cell_masses[cell] = volume * cell_densities[cell] return cell_masses def volumes(self, cells: iter | None = None) -> dict[int, float]: @@ -121,9 +121,7 @@ def volumes(self, cells: iter | None = None) -> dict[int, float]: volumes : dict Dictionary of volumes, indexed by cell number """ - volumes = dict( - (k, self.cell_volumes[k]) for k in cells if k in self.cell_volumes - ) + volumes = {k: self.cell_volumes[k] for k in cells if k in self.cell_volumes} return volumes def densities(self, cells: iter | None = None) -> dict[int, float]: @@ -135,9 +133,9 @@ def densities(self, cells: iter | None = None) -> dict[int, float]: densities : dict Dictionary of densities, indexed by cell number """ - densities = dict( - (k, self.cell_densities[k]) for k in cells if k in self.cell_densities - ) + densities = { + k: self.cell_densities[k] for k in cells if k in self.cell_densities + } return densities def masses(self, cells: iter | None = None) -> dict[int, float]: @@ -149,7 +147,7 @@ def masses(self, cells: iter | None = None) -> dict[int, float]: masses : dict Dictionary of masses, indexed by cell number """ - masses = dict((k, self.cell_masses[k]) for k in cells if k in self.cell_masses) + masses = {k: self.cell_masses[k] for k in cells if k in self.cell_masses} return masses @@ -279,8 +277,7 @@ def add_stopCard(self, nps: int, batches: int = 100) -> None: ------- None """ - if nps < batches: - batches = nps + batches = min(batches, nps) self.settings.batches = batches self.settings.particles = int(nps / batches) @@ -436,7 +433,7 @@ def initialise( self.statepoint = openmc.StatePoint(spfile_path) self.tally_numbers = [] self.tally_comments = [] - for _, tally in self.statepoint.tallies.items(): + for tally in self.statepoint.tallies.values(): self.tally_numbers.append(tally.id) self.tally_comments.append(tally.name) except (FileNotFoundError, KeyError): @@ -555,24 +552,27 @@ def _combine_heating_tallies(self, heating_tallies: dict) -> dict: heating_tallies_df[id] = self._get_tally_data(tally) for id, photon_tally in photon_tallies.items(): photon_cell_filter = photon_tally.find_filter(openmc.CellFilter) - for _, tally in heating_tallies.items(): + for tally in heating_tallies.values(): try: particle_filter = tally.find_filter(openmc.ParticleFilter) except ValueError: particle_filter = None cell_filter = tally.find_filter(openmc.CellFilter) - if particle_filter and cell_filter: - if photon_cell_filter == cell_filter: - if ( - "electron" in particle_filter.bins - or "positron" in particle_filter.bins - ): - tally_df = self._get_tally_data(tally) - heating_tallies_df[id]["mean"] += tally_df["mean"] - heating_tallies_df[id]["std. dev."] = ( - heating_tallies_df[id]["std. dev."].pow(2) - + tally_df["std. dev."].pow(2) - ).pow(0.5) + if ( + particle_filter + and cell_filter + and photon_cell_filter == cell_filter + and ( + "electron" in particle_filter.bins + or "positron" in particle_filter.bins + ) + ): + tally_df = self._get_tally_data(tally) + heating_tallies_df[id]["mean"] += tally_df["mean"] + heating_tallies_df[id]["std. dev."] = ( + heating_tallies_df[id]["std. dev."].pow(2) + + tally_df["std. dev."].pow(2) + ).pow(0.5) return heating_tallies_df def tallies_to_dataframes(self) -> dict: @@ -585,7 +585,7 @@ def tallies_to_dataframes(self) -> dict: """ tallies = {} heating_tallies = {} - for _, tally in self.statepoint.tallies.items(): + for tally in self.statepoint.tallies.values(): if "heating" in tally.scores: heating_tallies[tally.id] = tally else: diff --git a/src/jade/post/atlas.py b/src/jade/post/atlas.py index 6c6b970d..d56b67d2 100644 --- a/src/jade/post/atlas.py +++ b/src/jade/post/atlas.py @@ -5,6 +5,7 @@ # import win32com.client # import aspose.words import docx +import matplotlib.pyplot as plt # from docx.enum.table import WD_ALIGN_VERTICAL from docx.enum.text import WD_ALIGN_PARAGRAPH @@ -13,10 +14,11 @@ # from docx.oxml.ns import nsdecls, qn from docx.shared import Inches from matplotlib.figure import Figure -import matplotlib.pyplot as plt from jade.helper.aux_functions import PathLike +DEFAULT_IMG_WIDTH = Inches(7.5) + logger = logging.getLogger(__name__) @@ -44,7 +46,7 @@ def __init__(self, template: PathLike, name: str): self.outname = "atlas_" + name # Name for the outfile self.doc = doc # Word Document - def insert_img(self, figure: Figure, width=Inches(7.5)): + def insert_img(self, figure: Figure, width=DEFAULT_IMG_WIDTH): """Insert an image in the word document Parameters diff --git a/src/jade/post/excel_routines.py b/src/jade/post/excel_routines.py index b4fa915c..40d1c1c7 100644 --- a/src/jade/post/excel_routines.py +++ b/src/jade/post/excel_routines.py @@ -157,7 +157,6 @@ def add_sheets(self): def _get_sheet(self) -> list[pd.DataFrame]: """this is the core of the table, its data. It should return a list of dataframes that will be added to the workbook (one per sheet).""" - pass @staticmethod def _rename_columns(df: pd.DataFrame, names: dict[str, str]) -> None: @@ -251,7 +250,7 @@ def _get_sheet(self) -> list[pd.DataFrame]: class SpherePivotTable(PivotTable): """Pivot table with custom sorting for Sphere benchmark. - + Sorts the index by: 1. Isotopes first (sorted by Z number extracted from the case name) 2. Materials second (sorted alphabetically) @@ -260,27 +259,27 @@ class SpherePivotTable(PivotTable): def _get_sheet(self) -> list[pd.DataFrame]: # Get the parent class results dfs = super()._get_sheet() - + # Apply sorting to all dataframes sorted_dfs = [] for df in dfs: sorted_df = self._sort_sphere_index(df) sorted_dfs.append(sorted_df) - + return sorted_dfs @staticmethod def _sort_sphere_index(df: pd.DataFrame) -> pd.DataFrame: """Sort the index of the dataframe for Sphere benchmark. - + Isotopes (cases starting with numbers) are sorted by their Z number, and materials (cases starting with 'M') are sorted alphabetically. - + Parameters ---------- df : pd.DataFrame DataFrame with index to sort - + Returns ------- pd.DataFrame @@ -288,7 +287,7 @@ def _sort_sphere_index(df: pd.DataFrame) -> pd.DataFrame: """ if df.empty: return df - + # Get the index name(s) if isinstance(df.index, pd.MultiIndex): # For multi-index, we need to sort by the first level (Case) @@ -297,16 +296,16 @@ def _sort_sphere_index(df: pd.DataFrame) -> pd.DataFrame: else: # For single index cases = df.index.unique() - + # Separate isotopes and materials isotopes = [] materials = [] - + for case in cases: case_str = str(case) # Check if it starts with a digit (isotope) or 'M' (material) # Expected format: "Sphere_1001_H-1" for isotopes or "Sphere_M101" for materials - parts = case_str.split('_') + parts = case_str.split("_") if len(parts) >= 2: identifier = parts[1] # e.g., "1001" or "M101" if identifier[0].isdigit(): @@ -321,7 +320,7 @@ def _sort_sphere_index(df: pd.DataFrame) -> pd.DataFrame: # Unknown format, use the identifier as-is for sorting z_number = int(identifier) if identifier.isdigit() else 0 isotopes.append((z_number, case)) - elif identifier.startswith('M'): + elif identifier.startswith("M"): materials.append(case) else: # Unknown format, treat as material @@ -329,14 +328,14 @@ def _sort_sphere_index(df: pd.DataFrame) -> pd.DataFrame: else: # Unknown format, treat as material materials.append(case) - + # Sort isotopes by Z number, materials alphabetically isotopes.sort(key=lambda x: x[0]) materials.sort() - + # Create the sorted order sorted_order = [case for _, case in isotopes] + materials - + # Reindex the dataframe if isinstance(df.index, pd.MultiIndex): # For multi-index, we need to reindex by the first level @@ -351,7 +350,7 @@ def _sort_sphere_index(df: pd.DataFrame) -> pd.DataFrame: else: # Simple reindex for single index df = df.reindex(sorted_order) - + return df diff --git a/src/jade/post/manipulate_tally.py b/src/jade/post/manipulate_tally.py index 8b405525..174e8ebd 100644 --- a/src/jade/post/manipulate_tally.py +++ b/src/jade/post/manipulate_tally.py @@ -123,7 +123,7 @@ def condense_groups( def scale( - tally: pd.DataFrame, factor: int | float | list = 1, column: str = "Value" + tally: pd.DataFrame, factor: float | list = 1, column: str = "Value" ) -> pd.DataFrame: """Scale the tally values.""" if isinstance(factor, list): @@ -357,7 +357,7 @@ def gaussian_broadening( "Length of fwhm_frac list must match number of rows in tally." ) else: - raise ValueError("fwhm_frac must be a float or a list of floats.") + raise TypeError("fwhm_frac must be a float or a list of floats.") Eb = tally["Energy"].values.astype(float) Yb = np.zeros_like(tally["Value"]) diff --git a/src/jade/post/plotter.py b/src/jade/post/plotter.py index 25ffd726..13d44d21 100644 --- a/src/jade/post/plotter.py +++ b/src/jade/post/plotter.py @@ -158,7 +158,7 @@ def _add_vlines(self, ax: Axes) -> None: if self.cfg.v_lines is None: return - if "major" in self.cfg.v_lines.keys(): + if "major" in self.cfg.v_lines: lines = self.cfg.v_lines["major"] for line in lines: # Check first if the value is in the axis limits @@ -166,7 +166,7 @@ def _add_vlines(self, ax: Axes) -> None: ax.axvline(line, color="black") # minor lines - if "minor" in self.cfg.v_lines.keys(): + if "minor" in self.cfg.v_lines: lines = self.cfg.v_lines["minor"] for line in lines: # Check first if the value is in the axis limits @@ -186,7 +186,7 @@ def _add_labels(self, ax: Axes) -> None: if self.cfg.additional_labels is None: return - if "major" in self.cfg.additional_labels.keys(): + if "major" in self.cfg.additional_labels: labels = self.cfg.additional_labels["major"] bbox_dic = { "boxstyle": "round,pad=0.5", @@ -195,7 +195,7 @@ def _add_labels(self, ax: Axes) -> None: } _place_labels(labels, ax, 0.93, bbox=bbox_dic) - if "minor" in self.cfg.additional_labels.keys(): + if "minor" in self.cfg.additional_labels: # minor labels labels = self.cfg.additional_labels["minor"] _place_labels(labels, ax, 0.87) @@ -204,7 +204,7 @@ def _add_labels(self, ax: Axes) -> None: def _place_labels( labels: list[tuple[str, float | int]], ax: Axes, - ypos_perc: float | int, + ypos_perc: float, bbox: dict | None = None, ) -> None: """Helper function to place labels on the correct position in the plot""" @@ -1031,8 +1031,8 @@ def create_plot( # Aux functions def _get_limits( - lowerlimit: float | int, - upperlimit: float | int, + lowerlimit: float, + upperlimit: float, ydata: list | np.ndarray, xdata: list | np.ndarray, ): @@ -1095,8 +1095,8 @@ def _get_error_x_pos(x_array: np.ndarray) -> np.ndarray: def _apply_CE_limits( - min: float | int, - max: float | int, + min: float, + max: float, ydata: np.ndarray, xdata: np.ndarray, ax: Axes, diff --git a/src/jade/post/raw_processor.py b/src/jade/post/raw_processor.py index 8a620605..1bf5baae 100644 --- a/src/jade/post/raw_processor.py +++ b/src/jade/post/raw_processor.py @@ -59,7 +59,7 @@ def __init__( if self.code in (CODE.MCNP, CODE.D1S): self.sim_output = MCNPSimOutput(sim_folder) elif self.code == CODE.OPENMC: - if self.metadata.get("benchmark_name") =="Sphere": + if self.metadata.get("benchmark_name") == "Sphere": self.sim_output = OpenMCSphereSimOutput(sim_folder) else: self.sim_output = OpenMCSimOutput(sim_folder) diff --git a/src/jade/post/sim_output.py b/src/jade/post/sim_output.py index e5bb0d70..1ab368d2 100644 --- a/src/jade/post/sim_output.py +++ b/src/jade/post/sim_output.py @@ -49,7 +49,6 @@ def tallydata(self) -> dict[int, pd.DataFrame]: dict[str, pd.DataFrame] Dictionary of tally dataframes, indexed by tally number. """ - pass @property @abstractmethod @@ -73,7 +72,6 @@ def tally_numbers(self) -> list[int]: list[int] List of tally numbers. """ - pass @property @abstractmethod @@ -85,7 +83,6 @@ def tally_comments(self) -> list[str]: list[str] List of tally comments. """ - pass @abstractmethod def _read_code_version(self) -> str: @@ -96,7 +93,6 @@ def _read_code_version(self) -> str: str The code version. """ - pass class MCNPSimOutput(AbstractSimOutput): @@ -327,7 +323,6 @@ def retrieve_file( file1 = None file2 = None file3 = None - file4 = None for file_name in os.listdir(results_path): if file_name.endswith(".out"): @@ -356,11 +351,13 @@ def is_successfully_simulated(files: list[str]) -> bool: if file.startswith("statepoint") and file.endswith(".h5"): statepoint_found = True return statepoint_found - - def _prep_tally(self, filter_lookup: dict[str, str], tally: pd.DataFrame) -> pd.DataFrame: - ''' + + def _prep_tally( + self, filter_lookup: dict[str, str], tally: pd.DataFrame + ) -> pd.DataFrame: + """ Function to prepare the tally dataframe for JADE formatting, by renaming the columns and sorting by the filters. - + Parameters ---------- filter_lookup : dict @@ -373,7 +370,7 @@ def _prep_tally(self, filter_lookup: dict[str, str], tally: pd.DataFrame) -> pd. sorted_tally : pd.DataFrame The sorted and renamed tally dataframe ready for JADE formatting - ''' + """ filters = [] new_columns = {} if "cell" in tally.columns: @@ -384,8 +381,7 @@ def _prep_tally(self, filter_lookup: dict[str, str], tally: pd.DataFrame) -> pd. filters.append("energy high [eV]") if "time" in tally.columns: filters.append("time") - new_columns = dict( - (k, filter_lookup[k]) for k in filters if k in filter_lookup) + new_columns = {k: filter_lookup[k] for k in filters if k in filter_lookup} new_columns["mean"] = filter_lookup["mean"] new_columns["std. dev."] = filter_lookup["std. dev."] sorted_tally = tally.sort_values(filters) @@ -423,25 +419,12 @@ def _create_dataframes( "mean": "Value", "std. dev.": "Error", } - columns = [ - "Cells", - "User", - "Segments", - "Cosine", - "Energy", - "Time", - "Cor C", - "Cor B", - "Cor A", - "Value", - "Error", - ] for id, tally in tallies.items(): sorted_tally = self._prep_tally(filter_lookup, tally) - + if "Value" in sorted_tally.columns and "Error" in sorted_tally.columns: sorted_tally["Error"] = sorted_tally["Error"] / sorted_tally["Value"] - + tallydata[id] = sorted_tally totalbin[id] = None return tallydata, totalbin @@ -483,8 +466,8 @@ def __init__( None. """ - _, statefile, volfile = self.retrieve_file(sim_folder) - + self.retrieve_file(sim_folder) + # Retrieving atomic density for normalisation of the DPA, He and T production tallies self.input = omc.OpenMCInputFiles(sim_folder) materials = self.input.geometry.get_all_materials() @@ -522,19 +505,6 @@ def _create_dataframes( "mean": "Value", "std. dev.": "Error", } - columns = [ - "Cells", - "User", - "Segments", - "Cosine", - "Energy", - "Time", - "Cor C", - "Cor B", - "Cor A", - "Value", - "Error", - ] for id, tally in tallies.items(): sorted_tally = self._prep_tally(filter_lookup, tally) @@ -546,14 +516,15 @@ def _create_dataframes( sorted_tally["Error"] = sorted_tally["Error"] / self.atomic_density else: pass - + if "Value" in sorted_tally.columns and "Error" in sorted_tally.columns: sorted_tally["Error"] = sorted_tally["Error"] / sorted_tally["Value"] - + tallydata[id] = sorted_tally totalbin[id] = None return tallydata, totalbin + def _remove_constant_columns(df: pd.DataFrame) -> pd.DataFrame: """eliminate unnecessary columns from OpenMC tally data""" # Drop constant axes (if len is > 1) diff --git a/src/jade/run/benchmark.py b/src/jade/run/benchmark.py index cc656985..a92e7fb3 100644 --- a/src/jade/run/benchmark.py +++ b/src/jade/run/benchmark.py @@ -96,7 +96,6 @@ def _build_command(self, config: EnvironmentVariables) -> list[str]: """ # This method needs to build the command to run the code, will be different # depending on codes - pass @abstractmethod def _get_lib_data_command(self) -> tuple[str, str]: @@ -252,9 +251,6 @@ def _submit_job( """ # store cwd to get back to it after job submission cwd = os.getcwd() - user = ( - subprocess.run("whoami", capture_output=True).stdout.decode("utf-8").strip() - ) os.chdir(directory) job_script = os.path.join( directory, os.path.basename(directory) + "_job_script" @@ -275,7 +271,9 @@ def _submit_job( # Submit the job using the specified batch system (checked for existence in post_init) if not test: - subprocess.run([env_vars.scheduler_command, job_script], cwd=directory) + subprocess.run( + [env_vars.scheduler_command, job_script], cwd=directory, check=False + ) # return to the original directory os.chdir(cwd) @@ -582,7 +580,7 @@ def _get_continue_run_command( # if MCNP or similar, check for output file and remove if found if code == CODE.MCNP or code == CODE.D1S: for file in os.listdir(single_run_root): - if file.endswith(".o") or file.endswith(".r"): + if file.endswith((".o", ".r")): os.remove(Path(single_run_root, file)) # create the single run using the mock input @@ -1038,8 +1036,11 @@ def replace_template_vars( """ job_template = env_vars.code_job_template[code] - # store cwd to get back to it after job submission - user = subprocess.run("whoami", capture_output=True).stdout.decode("utf-8").strip() + user = ( + subprocess.run("whoami", capture_output=True, check=False) + .stdout.decode("utf-8") + .strip() + ) job_script = os.path.join(directory, os.path.basename(directory) + "_job_script") if not os.path.isfile(job_template): @@ -1049,7 +1050,7 @@ def replace_template_vars( with open(job_template) as fin: # Replace placeholders in batch file template with actual values contents = fin.read() - contents = contents.replace("INITIAL_DIR", f'"{str(directory)}"') + contents = contents.replace("INITIAL_DIR", f'"{directory!s}"') contents = contents.replace("OUT_FILE", f'"{job_script + ".out"}"') contents = contents.replace("ERROR_FILE", f'"{job_script + ".err"}"') contents = contents.replace("MPI_TASKS", str(env_vars.mpi_tasks)) @@ -1106,7 +1107,9 @@ def launch_global_jobs( if not test: with open(job_script, "w") as fout: fout.write(contents) - subprocess.run([env_vars.scheduler_command, job_script], cwd=cwd) + subprocess.run( + [env_vars.scheduler_command, job_script], cwd=cwd, check=False + ) # return to the original directory os.chdir(cwd) return scripts diff --git a/src/jade/run/input.py b/src/jade/run/input.py index 2b139f0a..aa9d3965 100644 --- a/src/jade/run/input.py +++ b/src/jade/run/input.py @@ -8,12 +8,12 @@ from copy import deepcopy from pathlib import Path +from f4enix.core.irradiation import Nuclide from f4enix.input.d1suned import IrradiationFile, Reaction, ReactionFile from f4enix.input.libmanager import LibManager from f4enix.input.materials import MatCardsList, Material, Zaid from f4enix.input.MCNPinput import D1S_Input from f4enix.input.MCNPinput import Input as MCNPInput -from f4enix.core.irradiation import Nuclide from jade.config.run_config import Library, LibraryACTINV, LibraryD1S, LibraryMCNP from jade.helper.__optionals__ import OMC_AVAIL @@ -36,19 +36,16 @@ def __init__(self, template_folder: PathLike, lib: Library): @abstractmethod def code(self) -> CODE: """Code of the input file.""" - pass @property @abstractmethod def name(self) -> str: """Name of the input file. E.g. ITER_1D""" - pass @property @abstractmethod def lib(self) -> Library: """Library used for the input file.""" - pass @abstractmethod def set_nps(self, nps: int): @@ -59,7 +56,6 @@ def set_nps(self, nps: int): nps : int number of particles to simulate. """ - pass @abstractmethod def translate(self): @@ -70,13 +66,11 @@ def translate(self): lib : Library library to which the input needs to be translated to. """ - pass @abstractmethod def _write(self, output_folder: PathLike): """Write the input files to the output folder. This should handle also additional files such as the wwinp for MCNP.""" - pass def write(self, output_folder: PathLike): """Write the input files to the output folder. Specific files are hendled diff --git a/tests/app/test_app.py b/tests/app/test_app.py index c3edf2f3..e5765d21 100644 --- a/tests/app/test_app.py +++ b/tests/app/test_app.py @@ -5,8 +5,6 @@ from importlib.resources import files from pathlib import Path -import pytest - import tests from jade.app.app import JadeApp from jade.config.run_config import ( @@ -228,9 +226,9 @@ def test_continue_run_sphere(self, tmp_path): assert "Sphere_dummy1" in command_sorted[0][1] assert "Sphere_m101" in command_sorted[1][1] - #assert "#!/bin/sh\n\n#SBATCH" in command[0][1] - #assert "Sphere_dummy1" in command[1][1] - #assert "Sphere_m101" in command[0][1] + # assert "#!/bin/sh\n\n#SBATCH" in command[0][1] + # assert "Sphere_dummy1" in command[1][1] + # assert "Sphere_m101" in command[0][1] app.run_cfg.env_vars.run_mode = RunMode.GLOBAL_JOB command = app.continue_run(testing=True) diff --git a/tests/app/test_fetch.py b/tests/app/test_fetch.py index 1a135bb9..846295d5 100644 --- a/tests/app/test_fetch.py +++ b/tests/app/test_fetch.py @@ -3,12 +3,21 @@ import os import tempfile +import gitlab import pytest +import requests -from jade.app.fetch import fetch_f4e_inputs, fetch_iaea_inputs, fetch_nonIAEA_exp_data +from jade.app.fetch import ( + fetch_f4e_inputs, + fetch_from_gitlab, + fetch_iaea_inputs, + fetch_nonIAEA_exp_data, +) # By default this should set the token to None if not found F4E_GITLAB_TOKEN = os.getenv("F4E_GITLAB_TOKEN") +# The F4E GitLab is not reachable from GitHub runners +ON_CI = os.getenv("GITHUB_ACTIONS") == "true" def test_fetch_iaea_inputs(tmpdir): @@ -52,8 +61,20 @@ def test_wrong_fetch_f4e_inputs(tmpdir): assert not success +def test_fetch_from_gitlab_unreachable(monkeypatch): + """An unreachable GitLab host must not crash the fetch, just return False.""" + + def raise_connection_error(self): + raise requests.exceptions.ConnectionError("unreachable host") + + monkeypatch.setattr(gitlab.Gitlab, "auth", raise_connection_error) + assert not fetch_from_gitlab( + "https://nonexistent.invalid/", "some/repo", "main", "token" + ) + + @pytest.mark.skipif( - F4E_GITLAB_TOKEN is None or os.environ.get("GITHUB_ACTIONS") == "true", + F4E_GITLAB_TOKEN is None or ON_CI, reason="No token found or running on GitHub CI", ) def test_fetch_f4e_inputs(tmpdir): diff --git a/tests/config/test_atlas_config.py b/tests/config/test_atlas_config.py index de37fee2..58cfa5d4 100644 --- a/tests/config/test_atlas_config.py +++ b/tests/config/test_atlas_config.py @@ -12,5 +12,5 @@ class TestConfigAtlasProcessor: def test_all_default_files(self): for path in os.listdir(EXCEL_ROOT): - if path.endswith(".yml") or path.endswith(".yaml"): + if path.endswith((".yml", ".yaml")): ConfigAtlasProcessor.from_yaml(EXCEL_ROOT.joinpath(path)) diff --git a/tests/config/test_excel_config.py b/tests/config/test_excel_config.py index 69cb6600..e51c275a 100644 --- a/tests/config/test_excel_config.py +++ b/tests/config/test_excel_config.py @@ -12,5 +12,5 @@ class TestConfigExcelProcessor: def test_all_default_files(self): for path in os.listdir(EXCEL_ROOT): - if path.endswith(".yml") or path.endswith(".yaml"): + if path.endswith((".yml", ".yaml")): ConfigExcelProcessor.from_yaml(EXCEL_ROOT.joinpath(path)) diff --git a/tests/config/test_raw_config.py b/tests/config/test_raw_config.py index 2c3d7007..c9a04d00 100644 --- a/tests/config/test_raw_config.py +++ b/tests/config/test_raw_config.py @@ -1,7 +1,7 @@ from __future__ import annotations import os -from importlib.resources import files, as_file +from importlib.resources import as_file, files from pathlib import Path import jade.resources.default_cfg.benchmarks_pp as res @@ -36,7 +36,7 @@ def test_from_yaml(self): for code in os.listdir(CFG_ROOT): code_folder = Path(CFG_ROOT, code) for file in os.listdir(code_folder): - cfg = ConfigRawProcessor.from_yaml(Path(code_folder, file)) + ConfigRawProcessor.from_yaml(Path(code_folder, file)) def test_aliases(self): file = Path(CFG_ROOT, "mcnp", "ITER_1D.yaml") diff --git a/tests/config/test_run_config.py b/tests/config/test_run_config.py index 3b9fd09b..daac9938 100644 --- a/tests/config/test_run_config.py +++ b/tests/config/test_run_config.py @@ -84,7 +84,7 @@ def test_from_yamls(self, tmpdir): run_cfg_file = paths_tree.cfg.run_cfg lib_cfg_file = paths_tree.cfg.libs_cfg additional_settings_root = paths_tree.cfg.bench_additional_files - run_cfg = RunConfig.from_yamls( + RunConfig.from_yamls( env_vars_file, paths_tree.cfg.exe_cfg, run_cfg_file, diff --git a/tests/gui/test_config_gui.py b/tests/gui/test_config_gui.py index fa236992..e94a4c93 100644 --- a/tests/gui/test_config_gui.py +++ b/tests/gui/test_config_gui.py @@ -2,22 +2,24 @@ from importlib.resources import files from pathlib import Path +from unittest.mock import patch import pytest import jade.resources -from unittest.mock import patch - from jade.helper.__optionals__ import TKINTER_AVAIL if TKINTER_AVAIL: from tkinter import filedialog, messagebox, ttk + from ttkthemes import ThemedTk + from jade.gui.run_config_gui import ConfigGUI DEFAULT_CFG = files(jade.resources).joinpath("default_cfg") + @pytest.mark.skipif(not TKINTER_AVAIL, reason="tkinter is not available") class TestConfigGui: @pytest.fixture(scope="session") diff --git a/tests/gui/test_post_config_gui.py b/tests/gui/test_post_config_gui.py index 51fb6a63..a15bcc1b 100644 --- a/tests/gui/test_post_config_gui.py +++ b/tests/gui/test_post_config_gui.py @@ -3,13 +3,12 @@ from importlib.resources import files from unittest.mock import patch -import jade.resources -from jade.config.status import GlobalStatus -from tests import dummy_structure - import pytest +import jade.resources +from jade.config.status import GlobalStatus from jade.helper.__optionals__ import TKINTER_AVAIL +from tests import dummy_structure if TKINTER_AVAIL: from jade.gui.post_config_gui import PostConfigGUI @@ -18,6 +17,7 @@ DEFAULT_CFG = files(jade.resources).joinpath("default_cfg") + @pytest.mark.skipif(not TKINTER_AVAIL, reason="tkinter is not available") class TestPostConfigGui: def test_init(self): diff --git a/tests/helper/compatibility_scripts/convert_oktavian.py b/tests/helper/compatibility_scripts/convert_oktavian.py index 8e1567c3..ea9ace70 100644 --- a/tests/helper/compatibility_scripts/convert_oktavian.py +++ b/tests/helper/compatibility_scripts/convert_oktavian.py @@ -1,7 +1,6 @@ from __future__ import annotations import os -import shutil from pathlib import Path import pandas as pd diff --git a/tests/helper/compatibility_scripts/derive_ITER1D_z_coordinates.py b/tests/helper/compatibility_scripts/derive_iter1d_z_coordinates.py similarity index 100% rename from tests/helper/compatibility_scripts/derive_ITER1D_z_coordinates.py rename to tests/helper/compatibility_scripts/derive_iter1d_z_coordinates.py diff --git a/tests/helper/test_auxiliary_func.py b/tests/helper/test_auxiliary_func.py index e053748a..efe71b13 100644 --- a/tests/helper/test_auxiliary_func.py +++ b/tests/helper/test_auxiliary_func.py @@ -1,19 +1,20 @@ from __future__ import annotations +import os +import shutil from importlib.resources import as_file, files from pathlib import Path -from f4enix import Input + import pandas as pd -import os +from f4enix import Input from jade.config.run_config import LibraryOpenMC from jade.helper.aux_functions import ( + add_rmode0, get_code_lib, print_code_lib, same_index, - add_rmode0, ) -import shutil from jade.helper.constants import CODE from tests.config import resources as conf_res from tests.helper import res as helper_res @@ -55,6 +56,6 @@ def test_add_rmode0(tmp_path: Path): for file in os.listdir(dst): if file.endswith(".i"): inp = Input.from_input(dst.joinpath(file)) - assert "RMODE" in inp.other_data.keys(), ( + assert "RMODE" in inp.other_data, ( f"RMODE card not found in the input file {file}" ) diff --git a/tests/helper/test_openmc.py b/tests/helper/test_openmc.py index c433fb82..c5e2676e 100644 --- a/tests/helper/test_openmc.py +++ b/tests/helper/test_openmc.py @@ -1,8 +1,9 @@ from __future__ import annotations +import os from importlib.resources import files -import pytest, os +import pytest from jade.helper.__optionals__ import OMC_AVAIL from tests.post.resources import openmc as resources diff --git a/tests/post/test_excel_routines.py b/tests/post/test_excel_routines.py index c92c806b..63078a98 100644 --- a/tests/post/test_excel_routines.py +++ b/tests/post/test_excel_routines.py @@ -1,3 +1,5 @@ +from typing import ClassVar + import numpy as np import pandas as pd import pytest @@ -167,7 +169,8 @@ def test_sort_sphere_index_empty(self): def test_get_sheet_integration(self): """Test the full _get_sheet method with sorting applied""" import tempfile - from jade.config.excel_config import ComparisonType, TableConfig + + from jade.config.excel_config import ComparisonType # Create test data data = { @@ -186,8 +189,8 @@ def test_get_sheet_integration(self): # Create a mock config class MockConfig: - x = ["Case"] - y = ["Energy"] + x: ClassVar[list[str]] = ["Case"] + y: ClassVar[list[str]] = ["Energy"] value = "Value" add_error = False comparison_type = ComparisonType.PERCENTAGE diff --git a/tests/post/test_sim_output.py b/tests/post/test_sim_output.py index a7cc866d..ff8c35a0 100644 --- a/tests/post/test_sim_output.py +++ b/tests/post/test_sim_output.py @@ -8,7 +8,7 @@ import tests.dummy_structure as dummy_struct from jade.helper.__optionals__ import OMC_AVAIL -from jade.post.sim_output import MCNPSimOutput, OpenMCSimOutput, OpenMCSphereSimOutput +from jade.post.sim_output import MCNPSimOutput, OpenMCSphereSimOutput SIMULATION_FOLDER = files(dummy_struct).joinpath("simulations") @@ -45,24 +45,30 @@ def test_mcnp_tally_comments(self, mcnp_sim_output): isinstance(comment, str) for comment in mcnp_sim_output.tally_comments ) + @pytest.fixture def openmc_sphere_sim_output() -> OpenMCSphereSimOutput: folder = Path(SIMULATION_FOLDER, "_openmc_-_FENDL 3.2b_", "Sphere", "Sphere_m101") # Create dummy files for OpenMC Sphere return OpenMCSphereSimOutput(folder) + @pytest.mark.skipif(not OMC_AVAIL, reason="OpenMC is not available") class TestOpenMCSimoutput: pass + @pytest.mark.skipif(not OMC_AVAIL, reason="OpenMC is not available") class TestOpenMCSphereSimOutput: def test_openmc_sphere_tallydata(self, openmc_sphere_sim_output): assert isinstance(openmc_sphere_sim_output.tallydata, dict) assert all( - isinstance(df, pd.DataFrame) for df in openmc_sphere_sim_output.tallydata.values() + isinstance(df, pd.DataFrame) + for df in openmc_sphere_sim_output.tallydata.values() ) def test_atomic_density(self, openmc_sphere_sim_output): assert isinstance(openmc_sphere_sim_output.atomic_density, float) - assert openmc_sphere_sim_output.atomic_density == pytest.approx(0.042305713026896896) + assert openmc_sphere_sim_output.atomic_density == pytest.approx( + 0.042305713026896896 + )