diff --git a/pyproject.toml b/pyproject.toml index 9a73486..bf3e59b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,32 +73,39 @@ isort.lines-after-imports = 2 select = [ "C", # Complexity checks (e.g., McCabe complexity, comprehensions) # "ANN001", "ANN201", "ANN401", # flake8-annotations (required strict type annotations for public functions) - # "S", # flake8-bandit (checks basic security issues in code) + "S", # flake8-bandit (checks basic security issues in code) # "BLE", # flake8-blind-except (checks the except blocks that do not specify exception) # "FBT", # flake8-boolean-trap (ensure that boolean args can be used with kw only) - # "E", # pycodestyle errors (PEP 8 style guide violations) + "E", # pycodestyle errors (PEP 8 style guide violations) "W", # pycodestyle warnings (e.g., extra spaces, indentation issues) # "DOC", # pydoclint issues (e.g., extra or missing return, yield, warnings) - # "A", # flake8-buitins (check variable and function names to not shadow builtins) - # "N", # Naming convention checks (e.g., PEP 8 variable and function names) + "A", # flake8-buitins (check variable and function names to not shadow builtins) + "N", # Naming convention checks (e.g., PEP 8 variable and function names) "F", # Pyflakes errors (e.g., unused imports, undefined variables) - # "I", # isort (Ensures imports are sorted properly) - # "B", # flake8-bugbear (Detects likely bugs and bad practices) + "I", # isort (Ensures imports are sorted properly) + "B", # flake8-bugbear (Detects likely bugs and bad practices) "TID", # flake8-tidy-imports (Checks for banned or misplaced imports) "UP", # pyupgrade (Automatically updates old Python syntax) - # "YTT", # flake8-2020 (Detects outdated Python 2/3 compatibility issues) - # "FLY", # flynt (Converts old-style string formatting to f-strings) - # "PIE", # flake8-pie + "YTT", # flake8-2020 (Detects outdated Python 2/3 compatibility issues) + "FLY", # flynt (Converts old-style string formatting to f-strings) + "PIE", # flake8-pie # "PL", # pylint # "RUF", # Ruff-specific rules (Additional optimizations and best practices) ] ignore = [ - "PLR2004", # [magic-value-comparision](https://docs.astral.sh/ruff/rules/magic-value-comparison) "C90", # [mccabe](https://docs.astral.sh/ruff/rules/#mccabe-c90) + "PLR2004", # [magic-value-comparision](https://docs.astral.sh/ruff/rules/magic-value-comparison) + "S311", # [suspicious-non-cryptographic-random-usage](https://docs.astral.sh/ruff/rules/suspicious-non-cryptographic-random-usage/) + "S404", # [suspicious-subprocess-import](https://docs.astral.sh/ruff/rules/suspicious-subprocess-import/) + "S603", # [subprocess-without-shell-equals-true](https://docs.astral.sh/ruff/rules/subprocess-without-shell-equals-true/) ] [tool.ruff.lint.per-file-ignores] +# non-lowercase-variable-in-function (N806) +"{femzip_api,femzip_mapper,d3plot,}.py" = ["N806"] +# error-suffix-on-exception-name (N818) +"{femzip_api}.py" = ["N818"] [tool.ruff.format] docstring-code-format = true diff --git a/src/lasso/__init__.py b/src/lasso/__init__.py index 3ef6419..ab9d95a 100644 --- a/src/lasso/__init__.py +++ b/src/lasso/__init__.py @@ -1,4 +1,5 @@ -from importlib.metadata import version, PackageNotFoundError +from importlib.metadata import PackageNotFoundError, version + try: __version__ = version("lasso-python") diff --git a/src/lasso/diffcrash/__init__.py b/src/lasso/diffcrash/__init__.py index 74c9976..1ead786 100644 --- a/src/lasso/diffcrash/__init__.py +++ b/src/lasso/diffcrash/__init__.py @@ -1,3 +1,4 @@ from .diffcrash_run import DiffcrashRun + __all__ = ["DiffcrashRun"] diff --git a/src/lasso/diffcrash/diffcrash_run.py b/src/lasso/diffcrash/diffcrash_run.py index aa2b685..553af7a 100644 --- a/src/lasso/diffcrash/diffcrash_run.py +++ b/src/lasso/diffcrash/diffcrash_run.py @@ -10,13 +10,14 @@ import time import typing from concurrent import futures -from typing import Union from pathlib import Path +from typing import Union + import psutil -# from ..logging import str_error, str_info, str_running, str_success, str_warn from lasso.logging import str_error, str_info, str_running, str_success, str_warn + # pylint: disable = too-many-lines DC_STAGE_SETUP = "SETUP" @@ -337,9 +338,7 @@ def _parse_crash_code(self, crash_code) -> str: self.logger.info(self._msg_option.format("crash-code", crash_code)) if not crash_code_ok: - err_msg = ( - f"Invalid crash code '{crash_code}'. Please use one of: {str(valid_crash_codes)}" - ) + err_msg = f"Invalid crash code '{crash_code}'. Please use one of: {valid_crash_codes!s}" self.logger.error(err_msg) raise RuntimeError(str_error(err_msg)) @@ -647,22 +646,21 @@ def run_import(self, pool: futures.ThreadPoolExecutor): str(i_filepath + counter_offset), ] # indeed there is a parameter file + elif self.use_id_mapping: + args = [ + self.diffcrash_home / f"DFC_Import_{self.crash_code}", + "-ID", + self.simulation_runs[i_filepath], + self.project_dir, + str(i_filepath + counter_offset), + ] else: - if self.use_id_mapping: - args = [ - self.diffcrash_home / f"DFC_Import_{self.crash_code}", - "-ID", - self.simulation_runs[i_filepath], - self.project_dir, - str(i_filepath + counter_offset), - ] - else: - args = [ - self.diffcrash_home / f"DFC_Import_{self.crash_code}", - self.simulation_runs[i_filepath], - self.project_dir, - str(i_filepath + counter_offset), - ] + args = [ + self.diffcrash_home / f"DFC_Import_{self.crash_code}", + self.simulation_runs[i_filepath], + self.project_dir, + str(i_filepath + counter_offset), + ] # append args to list import_arguments.append(args) @@ -689,7 +687,11 @@ def run_import(self, pool: futures.ThreadPoolExecutor): if n_imports_finished != n_new_imports_finished: # pylint: disable = consider-using-f-string - msg = f"Running Imports ... [{n_new_imports_finished}/{len(return_code_futures)}] - {percentage:3.2f}%\r" + msg = ( + f"Running Imports ... [{n_new_imports_finished}/{len(return_code_futures)}] - " + f"{percentage:3.2f}%\r" + ) + print(str_running(msg), end="", flush=True) self.logger.info(msg) @@ -1206,7 +1208,7 @@ def read_config_file(self, config_file: str) -> list[str]: conf_lines = conf.readlines() line = 0 - for i in range(0, len(conf_lines)): + for i in range(len(conf_lines)): if conf_lines[i].find("FUNCTION") >= 0: line = i + 1 break @@ -1216,7 +1218,7 @@ def read_config_file(self, config_file: str) -> list[str]: if line != 0: while 1: while 1: - for i in range(0, len(conf_lines[line])): + for i in range(len(conf_lines[line])): if conf_lines[line][i] == "<": element_start = i + 1 if conf_lines[line][i] == ">": @@ -1231,7 +1233,7 @@ def read_config_file(self, config_file: str) -> list[str]: j += 1 items = check.split(" ") pos = -1 - for n in range(0, len(items)): + for n in range(len(items)): if items[n].startswith("!"): msg = f"FOUND at {n}" print(msg) @@ -1240,7 +1242,7 @@ def read_config_file(self, config_file: str) -> list[str]: break pos = len(items) - for n in range(0, pos): + for n in range(pos): if items[n] == "PDMX" or items[n] == "pdmx": break if items[n] == "PDXMX" or items[n] == "pdxmx": @@ -1262,7 +1264,7 @@ def read_config_file(self, config_file: str) -> list[str]: for k in range(n, pos): postval = None - for m in range(0, n): + for m in range(n): if items[m] == "coordinates": items[m] = "geometry" if postval is None: diff --git a/src/lasso/diffcrash/run.py b/src/lasso/diffcrash/run.py index d697760..f10bd27 100644 --- a/src/lasso/diffcrash/run.py +++ b/src/lasso/diffcrash/run.py @@ -12,7 +12,6 @@ DiffcrashRun, parse_diffcrash_args, ) - from lasso.logging import str_error diff --git a/src/lasso/dimred/__init__.py b/src/lasso/dimred/__init__.py index 623eebe..11d3365 100644 --- a/src/lasso/dimred/__init__.py +++ b/src/lasso/dimred/__init__.py @@ -1,3 +1,4 @@ from .dimred_run import DimredRun, DimredStage + __all__ = ["DimredRun", "DimredStage"] diff --git a/src/lasso/dimred/dimred_run.py b/src/lasso/dimred/dimred_run.py index eb4237d..2c245ea 100644 --- a/src/lasso/dimred/dimred_run.py +++ b/src/lasso/dimred/dimred_run.py @@ -6,9 +6,9 @@ import shutil import sys import time +from collections.abc import Sequence from concurrent.futures.process import ProcessPoolExecutor from typing import Union -from collections.abc import Sequence import h5py import numpy as np @@ -18,7 +18,6 @@ from rich.table import Table from rich.text import Text -from lasso.utils.rich_progress_bars import PlaceHolderBar, WorkingDots from lasso.dimred.svd.clustering_betas import ( create_cluster_arg_dict, create_detector_arg_dict, @@ -27,6 +26,8 @@ from lasso.dimred.svd.plot_beta_clusters import plot_clusters_js from lasso.dimred.svd.pod_functions import calculate_v_and_betas from lasso.dimred.svd.subsampling_methods import create_reference_subsample, remap_random_subsample +from lasso.utils.rich_progress_bars import PlaceHolderBar, WorkingDots + # pylint: disable = too-many-lines @@ -505,7 +506,8 @@ def _parse_part_ids(self, part_ids: Union[Sequence[int], None]) -> Sequence[int] if not part_ids: return () - assert all(isinstance(pid, int) for pid in part_ids), "All part ids must be of type 'int'" + if not all(isinstance(pid, int) for pid in part_ids): + raise TypeError("All part ids must be of type 'int'") return part_ids @@ -583,14 +585,13 @@ def _parse_simulation_and_reference_runs( self.raise_error(err_msg) reference_run = os.path.normpath(reference_run_pattern) + # use first simulation run if no reference run was provided + # check if enough simulation runs remain + elif len(simulation_runs) > 1: + reference_run = simulation_runs[0] else: - # use first simulation run if no reference run was provided - # check if enough simulation runs remain - if len(simulation_runs) > 1: - reference_run = simulation_runs[0] - else: - err_msg = "Number of Simulation runs after using first as reference run is zero." - self.raise_error(err_msg) + err_msg = "Number of Simulation runs after using first as reference run is zero." + self.raise_error(err_msg) # add to table table.add_row("reference-run", reference_run) @@ -804,7 +805,10 @@ def subsample_to_reference_run(self): h5_sample.attrs[HDF5FileNames.SUBSAMPLE_PROCESS_TIME.value] = sub[ 1 ].result()[1] - submitted_samples.pop(i) + # FIXME: loop-iterator-mutation (B909) + # Mutation to loop iterable `submitted_samples` during iteration + # See: https://docs.astral.sh/ruff/rules/loop-iterator-mutation/ + submitted_samples.pop(i) # noqa B909 prog.advance(task1) # type: ignore t_cum_io += sub[1].result()[2] t_cum += sub[1].result()[1] diff --git a/src/lasso/dimred/graph_laplacian.py b/src/lasso/dimred/graph_laplacian.py index 25a01ce..3169ad5 100644 --- a/src/lasso/dimred/graph_laplacian.py +++ b/src/lasso/dimred/graph_laplacian.py @@ -76,7 +76,8 @@ def _laplacian_gauss_idw( L: array-like, shape (n_points, n_points) The laplacian matrix for manifold given by its sampling `points`. """ - assert 2 == points.ndim + if points.ndim != 2: + raise TypeError("Only 2D arrays are supported.") if min_neighbors is None: min_neighbors = points.shape[1] @@ -110,7 +111,8 @@ def _laplacian_gauss_idw( graph[i, j] = d = np.exp(d) graph[j, i] = d[:, np.newaxis] - assert 0 == (graph != graph.T).sum() + if not np.array_equal(graph, graph.T): + raise RuntimeError("graph matrix is not symetric.") return csgraph.laplacian(graph, normed=True) diff --git a/src/lasso/dimred/hashing.py b/src/lasso/dimred/hashing.py index 0b1a2ec..66df89b 100644 --- a/src/lasso/dimred/hashing.py +++ b/src/lasso/dimred/hashing.py @@ -1,8 +1,8 @@ import multiprocessing import os import time -from typing import Union from collections.abc import Sequence +from typing import Union import h5py import numpy as np @@ -83,7 +83,10 @@ def is_orientation_flip_required(eigenvectors1: np.ndarray, eigenvectors2: np.nd The eigenmodes require switching if the dot product of the knn-eigenfields yield a negative result. """ - assert eigenvectors1.shape == eigenvectors2.shape + if eigenvectors1.shape != eigenvectors2.shape: + raise AssertionError( + f"shape mismatch detected. {eigenvectors1.shape} not equal to {eigenvectors2.shape}" + ) # one eigenmode only if eigenvectors1.ndim == 1: @@ -133,7 +136,10 @@ def _compute_mode_similarities( mode_similarities = [] for i_hash, j_hash in matches: - assert hashes1.shape[2] == hashes2.shape[2] + if hashes1.shape[2] != hashes2.shape[2]: + raise AssertionError( + f"Unequal number of columns. {hashes1.shape[2]} is not equal to {hashes2.shape[2]}" + ) field1 = eigenvectors_sub1[:, i_hash] field2 = eigenvectors_sub2[:, j_hash] @@ -238,7 +244,8 @@ def run_hash_comparison( # pylint: disable = too-many-locals, too-many-statements - assert n_threads > 0 + if n_threads <= 0: + raise ValueError("Number of threads cannot be negative or zero.") # fixed settings hdf5_dataset_compression = "gzip" @@ -621,7 +628,11 @@ def compute_hashes( For comparison, only y is usually used. """ - assert eig_vecs.shape[0] == len(result_field), f"{eig_vecs.shape[0]} != {len(result_field)}" + if eig_vecs.shape[0] != len(result_field): + raise AssertionError( + f"Unequal number of rows detected. {eig_vecs.shape[0]} " + f"is not equal to {len(result_field)}" + ) # Note: needs to be vectorized to speed it up diff --git a/src/lasso/dimred/hashing_sphere.py b/src/lasso/dimred/hashing_sphere.py index e656d15..0fd7582 100644 --- a/src/lasso/dimred/hashing_sphere.py +++ b/src/lasso/dimred/hashing_sphere.py @@ -11,6 +11,7 @@ from scipy.stats import binned_statistic_2d from sklearn.preprocessing import normalize + warnings.simplefilter(action="ignore", category=FutureWarning) @@ -31,7 +32,8 @@ def _create_sphere_mesh(diameter: np.ndarray) -> tuple[np.ndarray, np.ndarray]: beta bin boundaries """ - assert diameter.dtype == np.float32 + if diameter.dtype != np.float32: + raise TypeError("diameter array must be of type `np.float32`.") # partition latitude n_alpha = 145 @@ -59,8 +61,7 @@ def _create_sphere_mesh(diameter: np.ndarray) -> tuple[np.ndarray, np.ndarray]: bin_beta = 1 - tmp # In case of trailing floats (-1.00000004 for example) - if bin_beta[-1] < -1: - bin_beta[-1] = -1 + bin_beta[-1] = max(bin_beta[-1], -1) bin_beta = np.arccos(bin_beta) @@ -139,10 +140,16 @@ def sphere_hashing( """ # bin_numbers holds the bin_number for its respective index and must have # same length as the number of points - assert len(bin_numbers[0] == len(field)) + if not len(bin_numbers[0] == len(field)): + raise AssertionError( + "bin_numbers holds the bin_number for its respective index and" + "must have same length as the number of points." + ) # check data types - assert bin_numbers.dtype == np.int - assert bin_counts.dtype == np.float32 + if not np.issubdtype(bin_numbers.dtype, np.integer): + raise TypeError(f"Expected int dtype got {bin_numbers.dtype}") + if not np.issubdtype(bin_counts.dtype, np.floating): + raise TypeError(f"Expected float dtype got {bin_counts.dtype}.") n_rows = bin_counts.shape[0] n_cols = bin_counts.shape[1] diff --git a/src/lasso/dimred/sphere/algorithms.py b/src/lasso/dimred/sphere/algorithms.py index d76ac2d..5b8fb92 100644 --- a/src/lasso/dimred/sphere/algorithms.py +++ b/src/lasso/dimred/sphere/algorithms.py @@ -1,13 +1,12 @@ import numpy as np -from sklearn.preprocessing import normalize - # scipy is C-code which causes invalid linter warning about ConvexHull not # being around. # pylint: disable = no-name-in-module from scipy.spatial import ConvexHull from scipy.stats import binned_statistic_2d from scipy.stats._binned_statistic import BinnedStatistic2dResult +from sklearn.preprocessing import normalize def to_spherical_coordinates(points: np.ndarray, centroid: np.ndarray, axis: str = "Z"): @@ -78,7 +77,11 @@ def sphere_hashing(histo: BinnedStatistic2dResult, field: np.ndarray): """ bin_n = histo.binnumber - assert len(bin_n[0] == len(field)) + if not len(bin_n[0] == len(field)): + raise AssertionError( + "bin_numbers holds the bin_number for its respective index and" + "must have same length as the number of points." + ) # get dims of the embedding space n_rows = histo.statistic.shape[0] @@ -150,8 +153,7 @@ def create_sphere(diameter: float): tmp = count * a_ele tmp /= r**2 * delt_alpha bin_beta = 1 - tmp - if bin_beta[-1] < -1: - bin_beta[-1] = -1 + bin_beta[-1] = max(bin_beta[-1], -1) bin_beta = np.arccos(bin_beta) return bin_alpha, bin_beta diff --git a/src/lasso/dimred/svd/clustering_betas.py b/src/lasso/dimred/svd/clustering_betas.py index 75fd5b3..22286ae 100644 --- a/src/lasso/dimred/svd/clustering_betas.py +++ b/src/lasso/dimred/svd/clustering_betas.py @@ -1,5 +1,5 @@ -from typing import Union from collections.abc import Sequence +from typing import Union import numpy as np from sklearn.cluster import DBSCAN, OPTICS, KMeans, SpectralClustering @@ -355,7 +355,8 @@ def __rescale_betas(betas): maxb: np.ndarray Array to rescale betas back to original values """ - assert len(betas.shape) == 2 + if len(betas.shape) != 2: + raise TypeError("only 2D arrays are supported.") ref_betas = np.abs(betas) maxb = np.array([np.max(ref_betas[:, i]) for i in range(betas.shape[1])]) # return np.array([(betas[:, i]/maxb[i]) for i in range(betas.shape[1])]).T diff --git a/src/lasso/dimred/svd/plot_beta_clusters.py b/src/lasso/dimred/svd/plot_beta_clusters.py index fc6adcf..1638378 100644 --- a/src/lasso/dimred/svd/plot_beta_clusters.py +++ b/src/lasso/dimred/svd/plot_beta_clusters.py @@ -2,8 +2,8 @@ import re import time import webbrowser -from typing import Union from collections.abc import Sequence +from typing import Union import numpy as np diff --git a/src/lasso/dimred/svd/pod_functions.py b/src/lasso/dimred/svd/pod_functions.py index 3c233c2..a6493db 100644 --- a/src/lasso/dimred/svd/pod_functions.py +++ b/src/lasso/dimred/svd/pod_functions.py @@ -1,9 +1,9 @@ from typing import Union import numpy as np +from rich.progress import Progress, TaskID from scipy.sparse import csc_matrix from scipy.sparse.linalg import svds -from rich.progress import Progress, TaskID from lasso.utils.rich_progress_bars import PlaceHolderBar diff --git a/src/lasso/dimred/svd/subsampling_methods.py b/src/lasso/dimred/svd/subsampling_methods.py index 539701c..73468ac 100644 --- a/src/lasso/dimred/svd/subsampling_methods.py +++ b/src/lasso/dimred/svd/subsampling_methods.py @@ -1,8 +1,8 @@ import os import random import time -from typing import Union from collections.abc import Sequence +from typing import Union import numpy as np from sklearn.neighbors import NearestNeighbors diff --git a/src/lasso/dyna/__init__.py b/src/lasso/dyna/__init__.py index 1bbcdec..519167d 100644 --- a/src/lasso/dyna/__init__.py +++ b/src/lasso/dyna/__init__.py @@ -1,7 +1,8 @@ -from .d3plot import D3plot -from .binout import Binout from .array_type import ArrayType +from .binout import Binout +from .d3plot import D3plot +from .d3plot_header import D3plotFiletype, D3plotHeader from .filter_type import FilterType -from .d3plot_header import D3plotHeader, D3plotFiletype + __all__ = ["Binout", "D3plot", "ArrayType", "FilterType", "D3plotHeader", "D3plotFiletype"] diff --git a/src/lasso/dyna/d3plot.py b/src/lasso/dyna/d3plot.py index 314b3f1..c0d68f6 100644 --- a/src/lasso/dyna/d3plot.py +++ b/src/lasso/dyna/d3plot.py @@ -1,5 +1,4 @@ import ctypes -from dataclasses import dataclass import logging import mmap import os @@ -10,20 +9,22 @@ import traceback import typing import webbrowser -from typing import Any, BinaryIO, Union from collections.abc import Iterable +from dataclasses import dataclass +from typing import Any, BinaryIO, Union import numpy as np +from lasso.dyna.array_type import ArrayType +from lasso.dyna.d3plot_header import D3plotFiletype, D3plotHeader +from lasso.dyna.femzip_mapper import FemzipMapper, filter_femzip_variables +from lasso.dyna.filter_type import FilterType from lasso.femzip.femzip_api import FemzipAPI, FemzipBufferInfo, FemzipVariableCategory from lasso.io.binary_buffer import BinaryBuffer from lasso.io.files import open_file_or_filepath from lasso.logging import get_logger from lasso.plotting import plot_shell_mesh -from lasso.dyna.array_type import ArrayType -from lasso.dyna.d3plot_header import D3plotFiletype, D3plotHeader -from lasso.dyna.femzip_mapper import FemzipMapper, filter_femzip_variables -from lasso.dyna.filter_type import FilterType + # pylint: disable = too-many-lines @@ -129,7 +130,9 @@ def __init__(self, d3plot: Any, block_size_bytes: int, single_file: bool): msg = "Invalid type for floats: {0}. np.float32 or np.float64 is required." raise RuntimeError(msg.format(d3plot.ftype)) - assert isinstance(d3plot, D3plot) + if not isinstance(d3plot, D3plot): + raise TypeError(f"d3plot must be a D3plot instance, got {type(d3plot)}") + self.d3plot = d3plot self._header = {} self.block_size_bytes = block_size_bytes @@ -176,7 +179,9 @@ def header(self): @header.setter def set_header(self, new_header: dict): - assert isinstance(new_header, dict) + if not isinstance(new_header, dict): + raise TypeError(f"new_header must be a dict, got {type(new_header)}") + self._header = new_header # pylint: disable = too-many-branches, too-many-statements, too-many-locals @@ -603,17 +608,16 @@ def build_header(self): or ArrayType.element_tshell_stress in self.d3plot.arrays ): new_header["ioshl1"] = 1000 + # if either stress or pstrain is written for solids + # the whole block of 7 basic variables is always written + # to the file + elif ( + ArrayType.element_solid_stress in self.d3plot.arrays + or ArrayType.element_solid_effective_plastic_strain in self.d3plot.arrays + ): + new_header["ioshl1"] = 999 else: - # if either stress or pstrain is written for solids - # the whole block of 7 basic variables is always written - # to the file - if ( - ArrayType.element_solid_stress in self.d3plot.arrays - or ArrayType.element_solid_effective_plastic_strain in self.d3plot.arrays - ): - new_header["ioshl1"] = 999 - else: - new_header["ioshl1"] = 0 + new_header["ioshl1"] = 0 if n_shells == 0 and n_thick_shells == 0 and n_solids == 0: new_header["ioshl1"] = ( @@ -635,11 +639,10 @@ def build_header(self): or ArrayType.element_tshell_effective_plastic_strain in self.d3plot.arrays ): new_header["ioshl2"] = 1000 + elif ArrayType.element_solid_effective_plastic_strain in self.d3plot.arrays: + new_header["ioshl2"] = 999 else: - if ArrayType.element_solid_effective_plastic_strain in self.d3plot.arrays: - new_header["ioshl2"] = 999 - else: - new_header["ioshl2"] = 0 + new_header["ioshl2"] = 0 if n_shells == 0 and n_thick_shells == 0 and n_solids == 0: new_header["ioshl2"] = ( @@ -662,15 +665,14 @@ def build_header(self): or ArrayType.element_shell_normal_force in self.d3plot.arrays ): new_header["ioshl3"] = 1000 + # See https://github.com/open-lasso-python/lasso-python/issues/39 + elif ( + ArrayType.element_shell_thickness in self.d3plot.arrays + or ArrayType.element_shell_internal_energy in self.d3plot.arrays + ): + new_header["ioshl3"] = 999 else: - # See https://github.com/open-lasso-python/lasso-python/issues/39 - if ( - ArrayType.element_shell_thickness in self.d3plot.arrays - or ArrayType.element_shell_internal_energy in self.d3plot.arrays - ): - new_header["ioshl3"] = 999 - else: - new_header["ioshl3"] = 0 + new_header["ioshl3"] = 0 if n_shells == 0: new_header["ioshl3"] = ( @@ -846,11 +848,7 @@ def build_header(self): if array.ndim != 3: msg = "Array '{0}' was expected to have {1} dimensions ({2})." raise ValueError( - msg.format( - ArrayType.rigid_wall_force, - 3, - ",".join(["n_timesteps", "n_rigid_bodies", "x_y_z"]), - ) + msg.format(ArrayType.rigid_wall_force, 3, "n_timesteps,n_rigid_bodies,x_y_z") ) new_header["numrbs"] = array.shape[1] else: @@ -922,9 +920,7 @@ def build_header(self): if array.ndim != 2: msg = "Array '{0}' was expected to have {1} dimensions ({2})." raise ValueError( - msg.format( - ArrayType.rigid_wall_force, 2, ",".join(["n_timesteps", "n_rigid_walls"]) - ) + msg.format(ArrayType.rigid_wall_force, 2, "n_timesteps,n_rigid_walls") ) n_rigid_walls = array.shape[1] if ArrayType.rigid_wall_position in self.d3plot.arrays: @@ -933,11 +929,7 @@ def build_header(self): if array.ndim != 3: msg = "Array '{0}' was expected to have {1} dimensions ({2})." raise ValueError( - msg.format( - ArrayType.rigid_wall_position, - 3, - ",".join(["n_timesteps", "n_rigid_walls", "x_y_z"]), - ) + msg.format(ArrayType.rigid_wall_position, 3, "n_timesteps,n_rigid_walls,x_y_z") ) n_rigid_walls = array.shape[1] @@ -1118,7 +1110,10 @@ def pack(self, value: Any, size=None, dtype_hint=None) -> bytes: If the type cannot be deserialized for being unknown. """ - assert dtype_hint in (None, np.integer, np.floating) + if dtype_hint not in (None, np.integer, np.floating): + raise TypeError( + f"dtype_hint must be None, np.integer, or np.floating, got {dtype_hint}" + ) # INT if isinstance(value, self._allowed_int_types): @@ -1215,12 +1210,9 @@ def count_array_state_var( if has_layers: if n_layers == 0: n_layers = array.shape[2] - else: - if n_layers != array.shape[2]: - msg = ( - "Array '{0}' has '{1}' integration layers but another array used '{2}'." - ) - raise ValueError(msg.format(array_type, array.shape[2], n_layers)) + elif n_layers != array.shape[2]: + msg = "Array '{0}' has '{1}' integration layers but another array used '{2}'." + raise ValueError(msg.format(array_type, array.shape[2], n_layers)) # last dimension is collapsed if array.ndim == 3: @@ -1229,12 +1221,11 @@ def count_array_state_var( n_vars = array.shape[3] * n_layers # no layers + # last dimension is collapsed + elif array.ndim == 2: + n_vars = 1 else: - # last dimension is collapsed - if array.ndim == 2: - n_vars = 1 - else: - n_vars = array.shape[2] + n_vars = array.shape[2] return n_vars, n_layers @@ -1368,7 +1359,10 @@ class RigidBodyMetadata: active_node_indexes: np.ndarray -class RigidBodyInfo: +# NOTE: class-as-data-structure (B903) +# Class could be dataclass or namedtuple +# See: https://docs.astral.sh/ruff/rules/class-as-data-structure/ +class RigidBodyInfo: # noqa B903 """RigidBodyMetadata contains vars for the individual rigid bodies""" rigid_body_metadata_list: Iterable[RigidBodyMetadata] @@ -1381,7 +1375,10 @@ def __init__( self.n_rigid_bodies = n_rigid_bodies -class RigidRoadInfo: +# NOTE: class-as-data-structure (B903) +# Class could be dataclass or namedtuple +# See: https://docs.astral.sh/ruff/rules/class-as-data-structure/ +class RigidRoadInfo: # noqa B903 """RigidRoadInfo contains metadata for the description of rigid roads""" n_nodes: int = 0 @@ -1399,7 +1396,10 @@ def __init__( self.motion = motion -class StateInfo: +# NOTE: class-as-data-structure (B903) +# Class could be dataclass or namedtuple +# See: https://docs.astral.sh/ruff/rules/class-as-data-structure/ +class StateInfo: # noqa B903 """StateInfo holds metadata for states which is currently solely the timestep. We all had bigger plans in life ... """ @@ -1621,7 +1621,9 @@ def arrays(self) -> dict: @arrays.setter def arrays(self, array_dict: dict): - assert isinstance(array_dict, dict) + if not isinstance(array_dict, dict): + raise TypeError(f"array_dict must be a dict, got {type(array_dict)}") + self._arrays = array_dict @property @@ -3582,13 +3584,12 @@ def _read_states_transfer_memory( buffer_array = buffer_array_dict[array_name] n_states_buffer_array = buffer_array.shape[0] array[i_state : i_state + n_states_buffer_array] = buffer_array - else: - # remove unnecessary state arrays (not geometry arrays!) - # we "could" deal with this in the allocate function - # by not allocating them but this would replicate code - # in the reading functions - if array_name in state_array_names: - arrays_to_delete.append(array_name) + # remove unnecessary state arrays (not geometry arrays!) + # we "could" deal with this in the allocate function + # by not allocating them but this would replicate code + # in the reading functions + elif array_name in state_array_names: + arrays_to_delete.append(array_name) for array_name in arrays_to_delete: del master_array_dict[array_name] @@ -4023,20 +4024,19 @@ def _read_states_rigid_walls( ): LOGGER.warning("Bug while reading global data for rigid walls. Skipping this data.") var_index += self.header.n_global_vars - previous_global_vars - else: - # rigid wall force - if n_rigid_walls * n_rigid_wall_vars != 0: - array_dict[ArrayType.rigid_wall_force] = state_data[ - :, var_index : var_index + n_rigid_walls - ] - var_index += n_rigid_walls + # rigid wall force + elif n_rigid_walls * n_rigid_wall_vars != 0: + array_dict[ArrayType.rigid_wall_force] = state_data[ + :, var_index : var_index + n_rigid_walls + ] + var_index += n_rigid_walls - # rigid wall position - if n_rigid_wall_vars > 1: - array_dict[ArrayType.rigid_wall_position] = state_data[ - :, var_index : var_index + 3 * n_rigid_walls - ].reshape(n_states, n_rigid_walls, 3) - var_index += 3 * n_rigid_walls + # rigid wall position + if n_rigid_wall_vars > 1: + array_dict[ArrayType.rigid_wall_position] = state_data[ + :, var_index : var_index + 3 * n_rigid_walls + ].reshape(n_states, n_rigid_walls, 3) + var_index += 3 * n_rigid_walls return var_index @@ -6096,10 +6096,16 @@ def plot( >>> d3plot.plot(0, field=pstrain[last_timestep], fringe_limits=(0, 0.3)) """ - assert i_timestep < self._state_info.n_timesteps - assert ArrayType.node_displacement in self.arrays - if fringe_limits: - assert len(fringe_limits) == 2 + if i_timestep >= self._state_info.n_timesteps: + raise ValueError( + f"i_timestep must be less than {self._state_info.n_timesteps}, got {i_timestep}" + ) + + if ArrayType.node_displacement not in self.arrays: + raise KeyError("ArrayType.node_displacement is missing from self.arrays") + + if fringe_limits is not None and len(fringe_limits) != 2: + raise ValueError("fringe_limits must be a sequence of length 2") # shell nodes shell_node_indexes = self.arrays[ArrayType.element_shell_node_indexes] @@ -6109,7 +6115,12 @@ def plot( # check for correct field size if isinstance(field, np.ndarray): - assert field.ndim == 1 + if getattr(field, "ndim", None) != 1: + raise ValueError( + "field must be a 1-dimensional array, " + f"got ndim={getattr(field, 'ndim', 'unknown')}" + ) + if is_element_field and len(shell_node_indexes) != len(field): # type: ignore msg = "Element indexes and field have different len: {} != {}" raise ValueError(msg.format(shell_node_indexes.shape, field.shape)) @@ -6959,17 +6970,16 @@ def _write_geom_rigid_body_description( array = self.arrays[typename] if dim_size < 0: dim_size = len(array) - else: - if len(array) != dim_size: - dimension_size_dict = { - typename2: len(self.arrays[typename2]) for typename2 in array_dims - } - msg = "Inconsistency in array dim '{0}' detected:\n{1}" - size_list = [ - f" - name: {typename}, dim: {array_dims[typename]}, size: {size}" - for typename, size in dimension_size_dict.items() - ] - raise ValueError(msg.format("n_rigid_bodies", "\n".join(size_list))) + elif len(array) != dim_size: + dimension_size_dict = { + typename2: len(self.arrays[typename2]) for typename2 in array_dims + } + msg = "Inconsistency in array dim '{0}' detected:\n{1}" + size_list = [ + f" - name: {typename}, dim: {array_dims[typename]}, size: {size}" + for typename, size in dimension_size_dict.items() + ] + raise ValueError(msg.format("n_rigid_bodies", "\n".join(size_list))) rigid_body_part_indexes = self.arrays[ArrayType.rigid_body_part_indexes] + FORTRAN_OFFSET # rigid_body_n_nodes = self.arrays[ArrayType.rigid_body_n_nodes] @@ -8999,7 +9009,10 @@ def _write_states_airbags( ArrayType.airbag_bag_volume: 1, } n_airbags = self.check_array_dims(array_dims, "n_airbags") - assert n_airbags == settings.header["npefg"] % 1000 + if n_airbags != settings.header["npefg"] % 1000: + raise ValueError( + f"Expected n_airbags to equal settings.header['npefg'] % 1000, but got {n_airbags}" + ) array_dims = { ArrayType.global_timesteps: 0, @@ -9411,18 +9424,17 @@ def check_array_dims( raise ValueError(msg, dimension_name, dimension_size, "\n".join(msg_arrays)) # dynamic dimensions - else: - if dimension_size_dict: - unique_sizes = np.unique(list(dimension_size_dict.values())) - if len(unique_sizes) > 1: - msg = "Inconsistency in array dim '%d' detected:\n%s" - size_list = [ - f" - name: {typename}, dim: {array_dimensions[typename]}, size: {size}" - for typename, size in dimension_size_dict.items() - ] - raise ValueError(msg, dimension_name, "\n".join(size_list)) - if len(unique_sizes) == 1: - dimension_size = unique_sizes[0] + elif dimension_size_dict: + unique_sizes = np.unique(list(dimension_size_dict.values())) + if len(unique_sizes) > 1: + msg = "Inconsistency in array dim '%d' detected:\n%s" + size_list = [ + f" - name: {typename}, dim: {array_dimensions[typename]}, size: {size}" + for typename, size in dimension_size_dict.items() + ] + raise ValueError(msg, dimension_name, "\n".join(size_list)) + if len(unique_sizes) == 1: + dimension_size = unique_sizes[0] if dimension_size < 0: return 0 @@ -9515,7 +9527,9 @@ def compare(self, d3plot2, array_eps: Union[float, None] = None): # pylint: disable = too-many-nested-blocks - assert isinstance(d3plot2, D3plot) + if not isinstance(d3plot2, D3plot): + raise TypeError(f"d3plot2 must be an instance of D3plot, got {type(d3plot2)}") + d3plot1 = self hdr_differences = d3plot1.header.compare(d3plot2.header) @@ -9554,26 +9568,25 @@ def compare(self, d3plot2, array_eps: Union[float, None] = None): if isinstance(array1, np.ndarray): if array1.shape != array2.shape: comparison = f"shape mismatch {array1.shape} != {array2.shape}" + elif np.issubdtype(array1.dtype, np.number) and np.issubdtype( + array2.dtype, np.number + ): + diff = np.abs(array1 - array2) + if diff.size: + if array_eps is not None: + diff2 = diff[diff > array_eps] + if diff2.size: + diff2_max = diff2.max() + if diff2_max: + comparison = f"Δmax = {diff2_max}" + else: + diff_max = diff.max() + if diff_max: + comparison = f"Δmax = {diff_max}" else: - if np.issubdtype(array1.dtype, np.number) and np.issubdtype( - array2.dtype, np.number - ): - diff = np.abs(array1 - array2) - if diff.size: - if array_eps is not None: - diff2 = diff[diff > array_eps] - if diff2.size: - diff2_max = diff2.max() - if diff2_max: - comparison = f"Δmax = {diff2_max}" - else: - diff_max = diff.max() - if diff_max: - comparison = f"Δmax = {diff_max}" - else: - n_mismatches = (array1 != array2).sum() - if n_mismatches: - comparison = f"Mismatches: {n_mismatches}" + n_mismatches = (array1 != array2).sum() + if n_mismatches: + comparison = f"Mismatches: {n_mismatches}" else: comparison = "Arrays don't match" diff --git a/src/lasso/dyna/d3plot_header.py b/src/lasso/dyna/d3plot_header.py index edf9378..d2000c6 100644 --- a/src/lasso/dyna/d3plot_header.py +++ b/src/lasso/dyna/d3plot_header.py @@ -7,6 +7,7 @@ from lasso.io.binary_buffer import BinaryBuffer from lasso.logging import get_logger + # We have a lot of docstrings here but even if not so, we want to contain the # code here. # pylint: disable=too-many-lines @@ -883,35 +884,34 @@ def load_file(self, file: Union[str, BinaryBuffer]) -> "D3plotHeader": self.has_solid_shell_thermal_strain_tensor = get_digit(self.raw_header["idtdt"], 3) == 1 if self.raw_header["idtdt"] > 100: self.has_element_strain = get_digit(self.raw_header["idtdt"], 4) == 1 - else: - # took a 1000 years to figure this out ... - # Warning: 4 gaussian points are not considered - if self.n_shell_vars > 0: - if ( - self.n_shell_vars - - self.n_shell_tshell_layers - * ( - 6 * self.has_shell_tshell_stress - + self.has_shell_tshell_pstrain - + self.n_shell_tshell_history_vars - ) - - 8 * self.has_shell_forces - - 4 * self.has_shell_extra_variables - ) > 1: - self.has_element_strain = True - # else: - # self.has_element_strain = False - elif self.n_thick_shell_vars > 0: - if ( - self.n_thick_shell_vars - - self.n_shell_tshell_layers - * ( - 6 * self.has_shell_tshell_stress - + self.has_shell_tshell_pstrain - + self.n_shell_tshell_history_vars - ) - ) > 1: - self.has_element_strain = True + # took a 1000 years to figure this out ... + # Warning: 4 gaussian points are not considered + elif self.n_shell_vars > 0: + if ( + self.n_shell_vars + - self.n_shell_tshell_layers + * ( + 6 * self.has_shell_tshell_stress + + self.has_shell_tshell_pstrain + + self.n_shell_tshell_history_vars + ) + - 8 * self.has_shell_forces + - 4 * self.has_shell_extra_variables + ) > 1: + self.has_element_strain = True + # else: + # self.has_element_strain = False + elif self.n_thick_shell_vars > 0: + if ( + self.n_thick_shell_vars + - self.n_shell_tshell_layers + * ( + 6 * self.has_shell_tshell_stress + + self.has_shell_tshell_pstrain + + self.n_shell_tshell_history_vars + ) + ) > 1: + self.has_element_strain = True # else: # self.has_element_strain = False # else: @@ -1085,7 +1085,7 @@ def read_words(self, bb: BinaryBuffer, words_to_read: dict, storage_dict: dict = storage_dict[name] = int(bb.read_number(data[0], data[1])) elif data[1] == self.ftype: storage_dict[name] = float(bb.read_number(data[0], data[1])) - elif data[1] == str: + elif data[1] is str: try: storage_dict[name] = bb.read_text(data[0], data[2]) except UnicodeDecodeError: @@ -1182,7 +1182,11 @@ def compare(self, other: "D3plotHeader") -> dict[str, tuple[Any, Any]]: differences: Dict[str, Tuple[Any, Any]] The different entries of both headers in a dict """ - assert isinstance(other, D3plotHeader) + if not isinstance(other, D3plotHeader): + raise TypeError( + "Only D3plotHeaders instances are supported. " + f"Detected {type(other)} is unsupported." + ) differences = {} names = {*self.raw_header.keys(), *other.raw_header.keys()} diff --git a/src/lasso/dyna/femzip_mapper.py b/src/lasso/dyna/femzip_mapper.py index 0e858d8..ac7ecee 100644 --- a/src/lasso/dyna/femzip_mapper.py +++ b/src/lasso/dyna/femzip_mapper.py @@ -4,6 +4,7 @@ from typing import Union import numpy as np + from lasso.dyna.array_type import ArrayType from lasso.femzip.femzip_api import FemzipAPI, FemzipFileMetadata, VariableInfo from lasso.femzip.fz_config import FemzipArrayType, FemzipVariableCategory, get_last_int_of_line diff --git a/src/lasso/dyna/lsda_py3.py b/src/lasso/dyna/lsda_py3.py index 3f2b811..4a719eb 100644 --- a/src/lasso/dyna/lsda_py3.py +++ b/src/lasso/dyna/lsda_py3.py @@ -1,6 +1,7 @@ import glob import struct + # We disable pylint here since this code is ancient code from LSTC and has the # respective quality. I tried rewriting it but could not understand it at all # in time. @@ -100,15 +101,15 @@ def writelength(self, length): s = struct.pack(self.lunpack, length) self.fp.write(s) - def writecd(self, dir): + def writecd(self, directory): """Write a whole CD command to the file at the current location""" - length = self.lengthsize + self.commandsize + len(dir) + length = self.lengthsize + self.commandsize + len(directory) s = struct.pack(self.lcunpack, length, Lsda.CD) self.fp.write(s) - if isinstance(dir, str): - self.fp.write(bytes(dir, "utf-8")) + if isinstance(directory, str): + self.fp.write(bytes(directory, "utf-8")) else: - self.fp.write(dir) + self.fp.write(directory) def writestentry(self, r): """Write a VARIABLE command (symbol table entry) to the file at @@ -223,8 +224,7 @@ def lread(self, start=0, end=2000000000): This routine does NOT follow links.""" if self.type == 0: # directory -- return listing return sorted(self.children.keys()) - if end > self.length: - end = self.length + end = min(end, self.length) if end < 0: end = self.length + end if start > self.length: @@ -239,11 +239,11 @@ def lread(self, start=0, end=2000000000): self.file.ateof = 0 # format = self.file.ordercode + _Diskfile.packtype[self.type]*(end-start) # return struct.unpack(format,self.file.fp.read(size*(end-start))) - format = f"{(self.file.ordercode)}{end - start}{_Diskfile.packtype[self.type]}" + fmt = f"{(self.file.ordercode)}{end - start}{_Diskfile.packtype[self.type]}" if self.type == Lsda.LINK: - return struct.unpack(format, self.file.fp.read(size * (end - start)))[0] + return struct.unpack(fmt, self.file.fp.read(size * (end - start)))[0] else: - return struct.unpack(format, self.file.fp.read(size * (end - start))) + return struct.unpack(fmt, self.file.fp.read(size * (end - start))) def read(self, start=0, end=2000000000): """Read data from the file. Same as lread, but follows links""" @@ -253,8 +253,7 @@ def read_raw(self, start=0, end=2000000000): """Read data from the file and return as bytestring""" if self.type == 0: # directory -- return listing return sorted(self.children.keys()) - if end > self.length: - end = self.length + end = min(end, self.length) if end < 0: end = self.length + end if start > self.length: @@ -534,20 +533,19 @@ def cd(self, path, create=2): # change CWD if part == "..": if self.cwd.parent: self.cwd = self.cwd.parent - else: - # if self.cwd.children.has_key(part)): - if part in self.cwd.children: - self.cwd = self.cwd.children[part] - if self.cwd.type != 0: # component is a variable, not a directory! - self.cwd = self.cwd.parent - break - elif create == 1 or (create == 2 and self.make_dirs == 1): - self.cwd = Symbol(part, self.cwd) # Create directory on the fly - else: # component in path is missing + # if self.cwd.children.has_key(part)): + elif part in self.cwd.children: + self.cwd = self.cwd.children[part] + if self.cwd.type != 0: # component is a variable, not a directory! + self.cwd = self.cwd.parent break + elif create == 1 or (create == 2 and self.make_dirs == 1): + self.cwd = Symbol(part, self.cwd) # Create directory on the fly + else: # component in path is missing + break return self.cwd - def write(self, name, type, data): + def write(self, name, data_type, data): """Write a new DATA record to the file. Creates and returns the Symbol for the data written""" if self.fw is None: @@ -572,7 +570,7 @@ def write(self, name, type, data): sym = self.cwd.children[name] else: sym = Symbol(name, self.cwd) - sym.type = type + sym.type = data_type sym.length = len(data) self.fw.writedata(sym, data) self.dirty_symbols.add(sym) diff --git a/src/lasso/femzip/__init__.py b/src/lasso/femzip/__init__.py index 7c030b6..1837356 100644 --- a/src/lasso/femzip/__init__.py +++ b/src/lasso/femzip/__init__.py @@ -1,3 +1,4 @@ from .femzip_api import FemzipAPI + __all__ = ["FemzipAPI"] diff --git a/src/lasso/femzip/femzip_api.py b/src/lasso/femzip/femzip_api.py index 1b3a2d9..4c5d328 100644 --- a/src/lasso/femzip/femzip_api.py +++ b/src/lasso/femzip/femzip_api.py @@ -1,6 +1,5 @@ import logging import os -from pathlib import Path import re import stat import sys @@ -18,12 +17,14 @@ c_uint64, sizeof, ) +from pathlib import Path from typing import Any, Union import numpy as np from .fz_config import FemzipArrayType, FemzipVariableCategory, get_last_int_of_line + # During next refactoring we should take a look at reducing the file size. # pylint: disable = too-many-lines @@ -441,7 +442,10 @@ def copy_struct(src: Structure, dest: Structure): """ # We access some internal members to do some magic. # pylint: disable = protected-access - assert src._fields_ == dest._fields_ + if src._fields_ != dest._fields_: + raise ValueError( + f"Source and destination fields do not match: {src._fields_} != {dest._fields_}" + ) for field_name, _ in src._fields_: setattr(dest, field_name, getattr(src, field_name)) @@ -1284,7 +1288,10 @@ def _copy_variable_info_array(self, file_metadata: FemzipFileMetadata) -> Femzip return file_metadata2 -class FemzipD3plotArrayMapping: +# NOTE: class-as-data-structure (B903) +# Class could be dataclass or namedtuple +# See: https://docs.astral.sh/ruff/rules/class-as-data-structure/ +class FemzipD3plotArrayMapping: # noqa B903 """Contains information about how to map femzip arrays to d3plot arrays""" d3plot_array_type: str diff --git a/src/lasso/femzip/fz_config.py b/src/lasso/femzip/fz_config.py index d565e11..1129eee 100644 --- a/src/lasso/femzip/fz_config.py +++ b/src/lasso/femzip/fz_config.py @@ -1,6 +1,5 @@ -from typing import Union - import enum +from typing import Union def get_last_int_of_line(line: str) -> tuple[str, Union[None, int]]: diff --git a/src/lasso/io/binary_buffer.py b/src/lasso/io/binary_buffer.py index 1a12d68..0a81494 100644 --- a/src/lasso/io/binary_buffer.py +++ b/src/lasso/io/binary_buffer.py @@ -46,7 +46,9 @@ def memoryview(self, new_mv): new_mv: memoryview memoryview used to store the bytes """ - assert isinstance(new_mv, memoryview) + if not isinstance(new_mv, memoryview): + raise TypeError(f"new_mv must be a memoryview, got {type(new_mv)}") + self.mv_ = new_mv self.sizes_ = [len(self.mv_)] @@ -68,8 +70,11 @@ def get_slice(self, start: int, end=Union[None, int], step: int = 1) -> "BinaryB the slice as a new buffer """ - assert start < len(self) - assert end is None or end < len(self) + if start >= len(self): + raise IndexError(f"start index {start} out of range (length {len(self)})") + + if end is not None and end >= len(self): + raise IndexError(f"end index {end} out of range (length {len(self)})") end = len(self) if end is None else end @@ -283,7 +288,10 @@ def append(self, binary_buffer: "BinaryBuffer"): buffer to append """ - assert isinstance(binary_buffer, BinaryBuffer) + if not isinstance(binary_buffer, BinaryBuffer): + raise TypeError( + f"binary_buffer must be an instance of BinaryBuffer, got {type(binary_buffer)}" + ) self.mv_ = memoryview(bytearray(self.mv_) + bytearray(binary_buffer.mv_)) self.sizes_.append(len(binary_buffer)) diff --git a/src/lasso/io/files.py b/src/lasso/io/files.py index 3773848..985ae15 100644 --- a/src/lasso/io/files.py +++ b/src/lasso/io/files.py @@ -2,8 +2,8 @@ import glob import os import typing -from typing import Union from collections.abc import Iterator +from typing import Union @contextlib.contextmanager diff --git a/src/lasso/logging.py b/src/lasso/logging.py index b37a18d..26b3520 100644 --- a/src/lasso/logging.py +++ b/src/lasso/logging.py @@ -3,6 +3,7 @@ from lasso.utils.console_coloring import ConsoleColoring + # settings MARKER_INFO = "[/]" MARKER_RUNNING = "[~]" diff --git a/src/lasso/math/sampling.py b/src/lasso/math/sampling.py index a007cd0..0e9853c 100644 --- a/src/lasso/math/sampling.py +++ b/src/lasso/math/sampling.py @@ -1,5 +1,6 @@ import random from typing import Union + import numpy as np from sklearn.neighbors import KDTree @@ -23,7 +24,10 @@ def unique_subsamples(start: int, end: int, n_samples: int, seed=None) -> np.nda indexes: np.ndarray unique sample indexes """ - assert start <= end + if start > end: + raise ValueError( + f"Invalid range: start ({start}) must be less than or equal to end ({end})" + ) n_samples = min(n_samples, end - start) random.seed(seed) diff --git a/src/lasso/plotting/__init__.py b/src/lasso/plotting/__init__.py index 8b79eca..c6a14ad 100644 --- a/src/lasso/plotting/__init__.py +++ b/src/lasso/plotting/__init__.py @@ -1,3 +1,4 @@ from .plot_shell_mesh import plot_shell_mesh + __all__ = ["plot_shell_mesh"] diff --git a/src/lasso/plotting/plot_shell_mesh.py b/src/lasso/plotting/plot_shell_mesh.py index 3f71fe5..ac67324 100644 --- a/src/lasso/plotting/plot_shell_mesh.py +++ b/src/lasso/plotting/plot_shell_mesh.py @@ -1,10 +1,11 @@ -import os import io -import uuid import json +import os +import uuid from base64 import b64encode -from zipfile import ZipFile, ZIP_DEFLATED from typing import Union +from zipfile import ZIP_DEFLATED, ZipFile + import numpy as np @@ -55,16 +56,48 @@ def plot_shell_mesh( # pylint: disable = too-many-locals, too-many-statements - assert node_coordinates.ndim == 2 - assert node_coordinates.shape[1] == 3 - assert shell_node_indexes.ndim == 2 - assert shell_node_indexes.shape[1] in [3, 4] + if getattr(node_coordinates, "ndim", None) != 2: + raise ValueError( + f"node_coordinates must be 2-dimensional, " + f"got ndim={getattr(node_coordinates, 'ndim', 'unknown')}" + ) + + if getattr(node_coordinates, "shape", (None, None))[1] != 3: + raise ValueError( + f"node_coordinates must have shape[1] == 3, " + f"got shape={getattr(node_coordinates, 'shape', 'unknown')}" + ) + + if getattr(shell_node_indexes, "ndim", None) != 2: + raise ValueError( + f"shell_node_indexes must be 2-dimensional, " + f"got ndim={getattr(shell_node_indexes, 'ndim', 'unknown')}" + ) + + shape_1 = getattr(shell_node_indexes, "shape", (None, None))[1] + if shape_1 not in (3, 4): + raise ValueError(f"shell_node_indexes must have shape[1] of 3 or 4, got shape[1]={shape_1}") + if isinstance(field, np.ndarray): - assert field.ndim == 1 + if getattr(field, "ndim", None) != 1: + raise ValueError( + f"field must be 1-dimensional, got ndim={getattr(field, 'ndim', 'unknown')}" + ) + if is_element_field: - assert field.shape[0] == shell_node_indexes.shape[0] - else: - assert field.shape[0] == node_coordinates.shape[0] + if ( + getattr(field, "shape", (None,))[0] + != getattr(shell_node_indexes, "shape", (None,))[0] + ): + raise ValueError( + f"field length {getattr(field, 'shape', (None,))[0]} does not match " + f"shell_node_indexes length {getattr(shell_node_indexes, 'shape', (None,))[0]}" + ) + elif getattr(field, "shape", (None,))[0] != getattr(node_coordinates, "shape", (None,))[0]: + raise ValueError( + f"field length {getattr(field, 'shape', (None,))[0]} does not match " + f"node_coordinates length {getattr(node_coordinates, 'shape', (None,))[0]}" + ) # cast types correctly # the types MUST be float32 diff --git a/src/lasso/utils/language.py b/src/lasso/utils/language.py index f40b8f1..588dc54 100644 --- a/src/lasso/utils/language.py +++ b/src/lasso/utils/language.py @@ -47,10 +47,9 @@ def set_var(name, value, context): if i_name == len(name) - 1: current_context[current_name] = value # otherwise iterate into next level + elif current_name in current_context: + current_context = current_context[current_name] else: - if current_name in current_context: - current_context = current_context[current_name] - else: - new_level = {} - current_context[current_name] = new_level - current_context = new_level + new_level = {} + current_context[current_name] = new_level + current_context = new_level diff --git a/test/unit_tests/dimred/svd/test_clustering_betas.py b/test/unit_tests/dimred/svd/test_clustering_betas.py index 32de5bc..a95cd8d 100644 --- a/test/unit_tests/dimred/svd/test_clustering_betas.py +++ b/test/unit_tests/dimred/svd/test_clustering_betas.py @@ -1,7 +1,9 @@ from unittest import TestCase + import numpy as np + from lasso.dimred.svd.clustering_betas import group_betas -from lasso.dimred.svd.keyword_types import DetectorType, ClusterType +from lasso.dimred.svd.keyword_types import ClusterType, DetectorType class TestClustering(TestCase): diff --git a/test/unit_tests/dimred/svd/test_plot_betas_clusters.py b/test/unit_tests/dimred/svd/test_plot_betas_clusters.py index 443784f..6bf5c05 100644 --- a/test/unit_tests/dimred/svd/test_plot_betas_clusters.py +++ b/test/unit_tests/dimred/svd/test_plot_betas_clusters.py @@ -2,6 +2,7 @@ from unittest import TestCase import numpy as np + from lasso.dimred.svd.plot_beta_clusters import plot_clusters_js diff --git a/test/unit_tests/dimred/svd/test_pod_functions.py b/test/unit_tests/dimred/svd/test_pod_functions.py index d0e7779..233070e 100644 --- a/test/unit_tests/dimred/svd/test_pod_functions.py +++ b/test/unit_tests/dimred/svd/test_pod_functions.py @@ -1,7 +1,9 @@ from unittest import TestCase -from lasso.dimred.svd.pod_functions import calculate_v_and_betas + import numpy as np +from lasso.dimred.svd.pod_functions import calculate_v_and_betas + class PodFunctionsTest(TestCase): def test_calculate_v_and_betas(self): diff --git a/test/unit_tests/dyna/test_d3plot.py b/test/unit_tests/dyna/test_d3plot.py index 713965b..16467bc 100644 --- a/test/unit_tests/dyna/test_d3plot.py +++ b/test/unit_tests/dyna/test_d3plot.py @@ -1,14 +1,14 @@ import os +import struct import tempfile -from lasso.io.binary_buffer import BinaryBuffer -from lasso.femzip.femzip_api import FemzipAPI from unittest import TestCase -import struct import numpy as np -from lasso.dyna.d3plot import D3plot, FilterType, _negative_to_positive_state_indexes from lasso.dyna.array_type import ArrayType +from lasso.dyna.d3plot import D3plot, FilterType, _negative_to_positive_state_indexes +from lasso.femzip.femzip_api import FemzipAPI +from lasso.io.binary_buffer import BinaryBuffer class D3plotTest(TestCase): diff --git a/test/unit_tests/dyna/test_d3plot_header.py b/test/unit_tests/dyna/test_d3plot_header.py index 1b93d44..753ba83 100644 --- a/test/unit_tests/dyna/test_d3plot_header.py +++ b/test/unit_tests/dyna/test_d3plot_header.py @@ -1,7 +1,8 @@ +import warnings from unittest import TestCase import numpy as np -import warnings + from lasso.dyna.d3plot_header import ( D3plotFiletype, D3plotHeader, @@ -24,7 +25,7 @@ def test_loading(self): D3plotHeader().load_file(filepath) # TODO more - warnings.warn("No assertions of behavior, test is incomplete") + warnings.warn(message="No assertions of behavior, test is incomplete", stacklevel=2) def test_get_digit(self) -> None: number = 1234567890 diff --git a/test/unit_tests/dyna/test_mapper.py b/test/unit_tests/dyna/test_mapper.py index 2cdd3eb..308c5cc 100644 --- a/test/unit_tests/dyna/test_mapper.py +++ b/test/unit_tests/dyna/test_mapper.py @@ -3,9 +3,10 @@ import numpy as np -from lasso.femzip.fz_config import FemzipArrayType, FemzipVariableCategory from lasso.dyna.array_type import ArrayType from lasso.dyna.femzip_mapper import FemzipMapper +from lasso.femzip.fz_config import FemzipArrayType, FemzipVariableCategory + part_global_femzip_translations: dict[tuple[FemzipArrayType, FemzipVariableCategory], set[str]] = { # GLOBAL @@ -256,7 +257,7 @@ def validate( self, fz: dict[tuple[str, FemzipVariableCategory], np.ndarray], d3plot_shape: tuple, - data_index_positions: Union[tuple, slice] = slice(None), + data_index_positions: Union[tuple, slice] = None, ): """Validate that the arrays have the same shape and that the raw data has been allocated to the correct positions in the @@ -271,6 +272,8 @@ def validate( data_index_positions: positions of the raw data in the d3plot array """ + if data_index_positions is None: + data_index_positions = slice(None) d3plot_name = [] m = FemzipMapper() diff --git a/test/unit_tests/io/test_binary_buffer.py b/test/unit_tests/io/test_binary_buffer.py index 3eb3996..5e01039 100644 --- a/test/unit_tests/io/test_binary_buffer.py +++ b/test/unit_tests/io/test_binary_buffer.py @@ -1,7 +1,8 @@ +import filecmp import os -import numpy as np from unittest import TestCase -import filecmp + +import numpy as np from lasso.io.binary_buffer import BinaryBuffer @@ -19,7 +20,7 @@ def test_init(self): def test_memoryview(self): self.assertEqual(self.bb.mv_, self.bb.memoryview) - with self.assertRaises(AssertionError): + with self.assertRaises(TypeError): self.bb.memoryview = None self.memoryview = memoryview(bytearray(b"")) diff --git a/test/unit_tests/io/test_files.py b/test/unit_tests/io/test_files.py index 7c8adac..c674856 100644 --- a/test/unit_tests/io/test_files.py +++ b/test/unit_tests/io/test_files.py @@ -1,4 +1,5 @@ import unittest + from lasso.io.files import collect_files