diff --git a/.github/workflows/pypi.yaml b/.github/workflows/pypi.yaml index 4720d152820..e39fddc9b36 100644 --- a/.github/workflows/pypi.yaml +++ b/.github/workflows/pypi.yaml @@ -12,7 +12,7 @@ jobs: name: Build Package runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - name: Setup Python @@ -46,7 +46,7 @@ jobs: url: https://pypi.org/p/topostats steps: - name: Download all the dists - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: name: python-package-distributions path: dist/ @@ -72,7 +72,7 @@ jobs: # steps: # - name: Download all the dists - # uses: actions/download-artifact@v4 + # uses: actions/download-artifact@v5 # with: # name: python-package-distributions # path: dist/ diff --git a/.github/workflows/sphinx_docs_to_gh_pages.yaml b/.github/workflows/sphinx_docs_to_gh_pages.yaml index 59db9471c43..947b8d618b8 100644 --- a/.github/workflows/sphinx_docs_to_gh_pages.yaml +++ b/.github/workflows/sphinx_docs_to_gh_pages.yaml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest name: Sphinx docs to gh-pages steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - name: Setup Python diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 5676b79a59c..e458b249b6d 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -30,7 +30,7 @@ jobs: os: ["ubuntu-latest", "macos-latest", "windows-latest"] python-version: ["3.10", "3.11"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python uses: actions/setup-python@v5 with: diff --git a/topostats/__init__.py b/topostats/__init__.py index fd282f998e9..4412bae342c 100644 --- a/topostats/__init__.py +++ b/topostats/__init__.py @@ -23,6 +23,10 @@ __version__ = version("topostats") __release__ = ".".join(__version__.split(".")[:-2]) +TOPOSTATS_DETAILS = version("topostats").split("+g") +TOPOSTATS_VERSION = TOPOSTATS_DETAILS[0] +TOPOSTATS_COMMIT = TOPOSTATS_DETAILS[1].split(".d")[0] + colormaps.register(cmap=Colormap("nanoscope").get_cmap()) colormaps.register(cmap=Colormap("gwyddion").get_cmap()) @@ -269,3 +273,9 @@ def topostats_to_dict(self) -> dict[str, str | ImageGrainCrops | npt.NDArray]: Dictionary of ''TopoStats'' object. """ return {re.sub(r"^_", "", key): value for key, value in self.__dict__.items()} + + +def log_topostats_version() -> None: + """Log the TopoStats version, commit and date to system logger.""" + LOGGER.info(f"TopoStats version : {TOPOSTATS_VERSION}") + LOGGER.info(f"Commit : {TOPOSTATS_COMMIT}") diff --git a/topostats/entry_point.py b/topostats/entry_point.py index 35a34a8da40..c574ddd5208 100644 --- a/topostats/entry_point.py +++ b/topostats/entry_point.py @@ -8,7 +8,7 @@ import sys from pathlib import Path -from topostats import __version__, run_modules +from topostats import __version__, log_topostats_version, run_modules from topostats.io import write_config_with_comments from topostats.plotting import run_toposum @@ -1268,6 +1268,9 @@ def entry_point(manually_provided_args=None, testing=False) -> None: None Does not return anything. """ + # Log topostats version and the commit id + log_topostats_version() + # Parse command line options, load config (or default) and update with command line options parser = create_parser() args = parser.parse_args() if manually_provided_args is None else parser.parse_args(manually_provided_args) diff --git a/topostats/grainstats.py b/topostats/grainstats.py index 53b325abe59..fe917d3256e 100644 --- a/topostats/grainstats.py +++ b/topostats/grainstats.py @@ -25,7 +25,7 @@ # pylint: disable=line-too-long # pylint: disable=fixme # FIXME : The calculate_stats() and calculate_aspect_ratio() raise this error when linting, could consider putting -# variables into dictionar, see example of breaking code out to staticmethod extremes() and returning a +# variables into dictionary, see example of breaking code out to staticmethod extremes() and returning a # dictionary of x_min/x_max/y_min/y_max # pylint: disable=too-many-locals # FIXME : calculate_aspect_ratio raises this error when linting it has 65 statements, recommended not to exceed 50. @@ -238,6 +238,7 @@ def calculate_stats(self) -> tuple[pd.DataFrame, dict]: f"[{self.image_name}] : Skipping subgrain due to being too small " f"(size: {subgrain_tight_shape}) to calculate stats for." ) + continue # Calculate all the stats points = self.calculate_points(subgrain_only_mask) diff --git a/topostats/io.py b/topostats/io.py index e9ed309090d..0e8fb551bf8 100644 --- a/topostats/io.py +++ b/topostats/io.py @@ -21,7 +21,7 @@ from numpyencoder import NumpyEncoder from ruamel.yaml import YAML, YAMLError -from topostats import __release__, grains +from topostats import TOPOSTATS_COMMIT, TOPOSTATS_VERSION, __release__, grains from topostats.logs.logs import LOGGER_NAME LOGGER = logging.getLogger(LOGGER_NAME) @@ -237,6 +237,11 @@ def write_yaml( header = f"# {header_message} : {get_date_time()}\n" + CONFIG_DOCUMENTATION_REFERENCE else: header = f"# Configuration from TopoStats run completed : {get_date_time()}\n" + CONFIG_DOCUMENTATION_REFERENCE + + # Add comment to config with topostats version + commit + header += f"# TopoStats version: {TOPOSTATS_VERSION}\n" + header += f"# Commit: {TOPOSTATS_COMMIT}\n" + output_config.write_text(header, encoding="utf-8") yaml = YAML(typ="safe") diff --git a/topostats/processing.py b/topostats/processing.py index 80ee9db1231..02313e6be57 100644 --- a/topostats/processing.py +++ b/topostats/processing.py @@ -9,7 +9,7 @@ import pandas as pd from art import tprint -from topostats import __version__ +from topostats import TOPOSTATS_COMMIT, TOPOSTATS_VERSION from topostats.array_manipulation import re_crop_grain_image_and_mask_to_set_size_nm from topostats.filters import Filters from topostats.grains import GrainCrop, GrainCropsDirection, Grains, ImageGrainCrops @@ -338,62 +338,73 @@ def run_grainstats( grainstats_config.pop("run") class_names = {index + 1: class_name for index, class_name in enumerate(grainstats_config.pop("class_names"))} # Grain Statistics : + LOGGER.info(f"[{filename}] : *** Grain Statistics ***") + grainstats_dict = {} + height_profiles_dict = {} try: - LOGGER.info(f"[{filename}] : *** Grain Statistics ***") grain_plot_dict = { key: value for key, value in plotting_config["plot_dict"].items() if key in ["grain_image", "grain_mask", "grain_mask_image"] } - grainstats_dict = {} - height_profiles_dict = {} + except Exception as e: + LOGGER.error( + f"[{filename}] : An error occurred whilst creating a grain plots dictionary: {e}\nReturning empty dataframe." + ) + return create_empty_dataframe(column_set="grainstats"), height_profiles_dict, {} - # There are two layers to process those above the given threshold and those below - grain_crops_direction: GrainCropsDirection + # There are two layers to process those above the given threshold and those below + grain_crops_direction: GrainCropsDirection + try: for direction, grain_crops_direction in image_grain_crops.__dict__.items(): if grain_crops_direction is None: LOGGER.warning( f"No grains exist for the {direction} direction. Skipping grainstats for {direction}." ) continue - grainstats_calculator = GrainStats( - grain_crops=grain_crops_direction.crops, - direction=direction, - base_output_dir=grain_out_path, - image_name=filename, - plot_opts=grain_plot_dict, - **grainstats_config, - ) - grainstats_dict[direction], height_profiles_dict[direction] = grainstats_calculator.calculate_stats() - grainstats_dict[direction]["threshold"] = direction - # Create results dataframe from above and below results - # Appease pylint and ensure that grainstats_df is always created - grainstats_df = create_empty_dataframe(column_set="grainstats") - if "above" in grainstats_dict and "below" in grainstats_dict: - grainstats_df = pd.concat([grainstats_dict["below"], grainstats_dict["above"]]) - elif "above" in grainstats_dict: - grainstats_df = grainstats_dict["above"] - elif "below" in grainstats_dict: - grainstats_df = grainstats_dict["below"] - else: - raise ValueError( - "grainstats dictionary has neither 'above' nor 'below' keys. This should be impossible." - ) - grainstats_df["basename"] = basename.parent - grainstats_df["class_name"] = grainstats_df["class_number"].map(class_names) - LOGGER.info(f"[{filename}] : Calculated grainstats for {len(grainstats_df)} grains.") - LOGGER.info(f"[{filename}] : Grainstats stage completed successfully.") - return grainstats_df, height_profiles_dict, grainstats_calculator.grain_crops - except Exception: - LOGGER.info( - f"[{filename}] : Errors occurred whilst calculating grain statistics. Returning empty dataframe." + try: + grainstats_calculator = GrainStats( + grain_crops=grain_crops_direction.crops, + direction=direction, + base_output_dir=grain_out_path, + image_name=filename, + plot_opts=grain_plot_dict, + **grainstats_config, + ) + grainstats_dict[direction], height_profiles_dict[direction] = ( + grainstats_calculator.calculate_stats() + ) + grainstats_dict[direction]["threshold"] = direction + except Exception as e: + LOGGER.error( + f"[{filename}] : An error occurred whilst calculating grain statistics: {e}\nReturning empty dataframe." + ) + return create_empty_dataframe(column_set="grainstats"), height_profiles_dict, {} + except Exception as e: + LOGGER.error( + f"[{filename}] : An error occurred whilst trying to iterate over directions: {e}\nReturning empty dataframe." ) return create_empty_dataframe(column_set="grainstats"), height_profiles_dict, {} - else: - LOGGER.info( - f"[{filename}] : Calculation of grainstats disabled, returning empty dataframe and empty height_profiles." - ) - return create_empty_dataframe(column_set="grainstats"), {}, {} + # Create results dataframe from above and below results + # Appease pylint and ensure that grainstats_df is always created + grainstats_df = create_empty_dataframe(column_set="grainstats") + if "above" in grainstats_dict and "below" in grainstats_dict: + grainstats_df = pd.concat([grainstats_dict["below"], grainstats_dict["above"]]) + elif "above" in grainstats_dict: + grainstats_df = grainstats_dict["above"] + elif "below" in grainstats_dict: + grainstats_df = grainstats_dict["below"] + else: + raise ValueError("grainstats dictionary has neither 'above' nor 'below' keys. This should be impossible.") + grainstats_df["basename"] = basename.parent + grainstats_df["class_name"] = grainstats_df["class_number"].map(class_names) + LOGGER.info(f"[{filename}] : Calculated grainstats for {len(grainstats_df)} grains.") + LOGGER.info(f"[{filename}] : Grainstats stage completed successfully.") + return grainstats_df, height_profiles_dict, grainstats_calculator.grain_crops + LOGGER.info( + f"[{filename}] : Calculation of grainstats disabled, returning empty dataframe and empty height_profiles." + ) + return create_empty_dataframe(column_set="grainstats"), {}, {} def run_disordered_tracing( @@ -1626,7 +1637,8 @@ def completion_message(config: dict, img_files: list, summary_config: dict, imag tprint("TopoStats", font="twisted") LOGGER.info( f"\n\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ COMPLETE ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n" - f" TopoStats Version : {__version__}\n" + f" TopoStats Version : {TOPOSTATS_VERSION}\n" + f" TopoStats Commit : {TOPOSTATS_COMMIT}\n" f" Base Directory : {config['base_dir']}\n" f" File Extension : {config['file_ext']}\n" f" Files Found : {len(img_files)}\n"