diff --git a/pyproject.toml b/pyproject.toml index ae32867..353807e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,8 @@ requires = [ "numpy; platform_machine != 'ppc64le'", 'pyproject-metadata>=0.5.0', 'tomli>=1.0.0', - 'scipy' + 'scipy', + 'silx' ] [project.urls] @@ -67,6 +68,7 @@ cormapy = 'freesas.app.cormap:main' supycomb = 'freesas.app.supycomb:main' free_bift = 'freesas.app.bift:main' extract_ascii = 'freesas.app.extract_ascii:main' +free_dnn = 'freesas.app.dnn:main' [project.gui-scripts] freesas = 'freesas.app.plot_sas:main' diff --git a/src/freesas/app/dnn.py b/src/freesas/app/dnn.py new file mode 100644 index 0000000..c99d942 --- /dev/null +++ b/src/freesas/app/dnn.py @@ -0,0 +1,74 @@ +#!/usr/bin/python3 +# coding: utf-8 +# +# Project: freesas +# https://github.com/kif/freesas +# +# Copyright (C) 2020 European Synchrotron Radiation Facility, Grenoble, France +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +__author__ = ["Jérôme Kieffer", "Mayank Yadav"] +__license__ = "MIT" +__copyright__ = "2024, ESRF" +__date__ = "11/09/2024" + +import sys +import logging +from freesas.sas_argparser import SASParser +from freesas.fitting import run_dnn + +logging.basicConfig(level=logging.WARNING) +logger = logging.getLogger("free_dnn") + +if sys.version_info < (3, 6): + logger.error("This code uses F-strings and requires Python 3.6+") + + +def build_parser() -> SASParser: + """Build parser for input and return list of files. + :return: parser + """ + description = ( + "Assess the radius of gyration (Rg) and the diameter of the particle (Dmax) using a Dense Neural-Network" + " for a set of scattering curves" + ) + epilog = """free_dnn is an alternative implementation of + `gnnom` (https://doi.org/10.1016/j.str.2022.03.011). + As this tool used a different training set, some results are likely to differ. + """ + parser = SASParser(prog="free_gpa", description=description, epilog=epilog) + file_help_text = "dat files of the scattering curves" + parser.add_file_argument(help_text=file_help_text) + parser.add_output_filename_argument() + parser.add_output_data_format("native", "csv", "ssf", default="native") + parser.add_q_unit_argument() + + return parser + + + +def main() -> None: + """Entry point for free_gpa app""" + parser = build_parser() + run_dnn(parser=parser, logger=logger) + + +if __name__ == "__main__": + main() diff --git a/src/freesas/app/meson.build b/src/freesas/app/meson.build index aac2c24..80d19d6 100644 --- a/src/freesas/app/meson.build +++ b/src/freesas/app/meson.build @@ -7,7 +7,8 @@ py.install_sources([ 'cormap.py', 'extract_ascii.py', 'plot_sas.py', - 'supycomb.py' + 'supycomb.py', + 'dnn.py' ], pure: false, # Will be installed next to binaries subdir: 'freesas/app' # Folder relative to site-packages to install to diff --git a/src/freesas/fitting.py b/src/freesas/fitting.py index 8a7986e..ebebc2f 100644 --- a/src/freesas/fitting.py +++ b/src/freesas/fitting.py @@ -5,7 +5,7 @@ __contact__ = "martha.brennich@googlemail.com" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" -__date__ = "29/11/2023" +__date__ = "11/09/2024" __status__ = "development" __docformat__ = "restructuredtext" @@ -26,7 +26,7 @@ load_scattering_data, convert_inverse_angstrom_to_nanometer, ) -from .sas_argparser import GuinierParser +from .sas_argparser import GuinierParser, SASParser def set_logging_level(verbose_flag: int) -> None: @@ -110,6 +110,28 @@ def get_guinier_header( else: return "" +def get_dnn_header( + linesep: str, output_format: Optional[str] = None +) -> str: + """Return appropriate header line for selected output format + :param output_format: output format from string parser + :param linesep: correct linesep for chosen destination + :return: a one-line string""" + # pylint: disable=R1705 + if output_format == "csv": + return ( + ",".join( + ( + "File", + "Rg", + "Dmax", + ) + ) + + linesep + ) + else: + return "" + def rg_result_to_output_line( rg_result: RG_RESULT, @@ -161,6 +183,44 @@ def rg_result_to_output_line( else: return f"{afile} {rg_result}{linesep}" +def dnn_result_to_output_line( + dnn_result: tuple, + afile: Path, + linesep: str, + output_format: Optional[str] = None, +) -> str: + """Return result line formatted according to selected output format + :param dnn_result: Result of an dnn inference, 2 tuple + :param afile: The name of the file that was processed + :param output_format: The chosen output format + :param linesep: correct linesep for chosen destination + :return: a one-line string including linesep""" + # pylint: disable=R1705 + if output_format == "csv": + return ( + ",".join( + [ + f"{afile}", + f"{dnn_result[0]:6.4f}", + f"{dnn_result[1]:6.4f}", + ] + ) + + linesep + ) + elif output_format == "ssv": + return ( + " ".join( + [ + f"{dnn_result[0]:6.4f}", + f"{dnn_result[1]:6.4f}", + f"{afile}", + ] + ) + + linesep + ) + else: + return f"{afile} {dnn_result[0]} {dnn_result[1]}{linesep}" + def run_guinier_fit( fit_function: Callable[[ndarray], RG_RESULT], @@ -220,3 +280,61 @@ def run_guinier_fit( ) output_destination.write(res) output_destination.flush() +def run_dnn( + parser: SASParser, + logger: logging.Logger, +) -> None: + """ + reads in the data, infer the DNN and creates the result + :param parser: a function that returns the output of argparse.parse() + :param logger: a Logger + """ + from .dnn import Rg_Dmax # heavy import + + args = parser.parse_args() + set_logging_level(args.verbose) + files = collect_files(args.file) + logger.debug("%s input files", len(files)) + + with get_output_destination(args.output) as output_destination: + linesep = get_linesep(output_destination) + + output_destination.write( + get_dnn_header( + linesep, + args.format, + ) + ) + + for afile in files: + logger.info("Processing %s", afile) + try: + data = load_scattering_data(afile) + except OSError: + logger.error("Unable to read file %s", afile) + except ValueError: + logger.error("Unable to parse file %s", afile) + else: + if args.unit == "Å": + data = convert_inverse_angstrom_to_nanometer(data) + q, I = data.T[:2] + try: + dnn_result = Rg_Dmax(q, I) + except ( + InsufficientDataError, + NoGuinierRegionError, + ValueError, + IndexError, + ) as err: + sys.stderr.write( + f"{afile}, {err.__class__.__name__}: {err} {os.linesep}" + ) + else: + res = dnn_result_to_output_line( + dnn_result, + afile, + linesep, + args.format, + ) + output_destination.write(res) + output_destination.flush() diff --git a/version.py b/version.py index 737a154..2ee70bd 100755 --- a/version.py +++ b/version.py @@ -52,7 +52,7 @@ __authors__ = ["Jérôme Kieffer"] __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" -__date__ = "04/12/2023" +__date__ = "11/09/2024" __status__ = "production" __docformat__ = 'restructuredtext' __all__ = ["date", "version_info", "strictversion", "hexversion", "debianversion", @@ -68,11 +68,11 @@ "alpha": "a", "beta": "b", "candidate": "rc"} -MAJOR = 0 +MAJOR = 2024 MINOR = 9 -MICRO = 9 +MICRO = 0 RELEV = "dev" # <16 -SERIAL = 1 # <16 +SERIAL = 0 # <16 date = __date__