From 7b59dafb10e3656336934e1436af8beeb4393a93 Mon Sep 17 00:00:00 2001 From: Sebastian Bauer Date: Tue, 9 Dec 2025 11:10:09 +0000 Subject: [PATCH 01/16] chore: ignore log files in test folder --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index db4049f..be61a59 100644 --- a/.gitignore +++ b/.gitignore @@ -30,5 +30,8 @@ __pycache__ *.Identifier !requirements.txt .jupyter_cache + +# Ignore log files +tests/*.log tests/multiprocess_log.txt .vscode From b9949d2042a8292ff98dfe504f97d8b1c44b236b Mon Sep 17 00:00:00 2001 From: Sebastian Bauer Date: Thu, 11 Dec 2025 11:35:52 +0000 Subject: [PATCH 02/16] fix: make sure all values before unit conversion are numeric --- pyAMARES/kernel/PriorKnowledge.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyAMARES/kernel/PriorKnowledge.py b/pyAMARES/kernel/PriorKnowledge.py index dc4a4ba..3b81dc0 100644 --- a/pyAMARES/kernel/PriorKnowledge.py +++ b/pyAMARES/kernel/PriorKnowledge.py @@ -181,6 +181,9 @@ def unitconverter(df_ini, MHz=120.0): pandas.DataFrame: A DataFrame with converted unit values in specified rows. """ df = deepcopy(df_ini) + df = df.apply( + pd.to_numeric, errors="raise", downcast="float" + ) # By this point the values should only be numeric if "chemicalshift" in df.index: df.loc["chemicalshift", df.notna().loc["chemicalshift"]] *= MHz @@ -189,7 +192,7 @@ def unitconverter(df_ini, MHz=120.0): if "phase" in df.index: df.loc["phase", df.notna().loc["phase"]] = np.deg2rad( - df.loc["phase"][df.notna().loc["phase"]].astype(float) + df.loc["phase"][df.notna().loc["phase"]] ) return df From 6e9cc7017aa5164b4b7fed2171c828ffbb42117a Mon Sep 17 00:00:00 2001 From: Sebastian Bauer Date: Mon, 5 Jan 2026 10:38:51 +0000 Subject: [PATCH 03/16] chore: improve log ignore pattern --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index be61a59..43aa725 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,6 @@ __pycache__ .jupyter_cache # Ignore log files -tests/*.log +tests/**/*.log tests/multiprocess_log.txt .vscode From 355691fb72b28cfc35f950b8abea38d81f0a06cc Mon Sep 17 00:00:00 2001 From: Sebastian Bauer Date: Mon, 5 Jan 2026 10:39:25 +0000 Subject: [PATCH 04/16] chore: add current env flag for nbval environment detection --- pytest.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest.ini b/pytest.ini index f7fb197..e481281 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,3 @@ [pytest] -addopts = --nbval-lax +addopts = --nbval-lax --nbval-current-env testpaths = tests From 3da033130f919217b4f1751d659b1008cb28bbd1 Mon Sep 17 00:00:00 2001 From: Sebastian Bauer Date: Mon, 5 Jan 2026 10:43:03 +0000 Subject: [PATCH 05/16] fix: nbval current env in workflow --- .github/workflows/test-notebooks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-notebooks.yml b/.github/workflows/test-notebooks.yml index e842d3e..59a0101 100644 --- a/.github/workflows/test-notebooks.yml +++ b/.github/workflows/test-notebooks.yml @@ -42,7 +42,7 @@ jobs: done - name: Run notebook tests - run: pytest --nbval-lax --current-env tests/ + run: pytest --nbval-lax --nbval-current-env tests/ - name: Upload executed notebooks on failure if: failure() From f2a0078b241e686019142afa89b41aedaa4b23fb Mon Sep 17 00:00:00 2001 From: Sebastian Bauer Date: Mon, 5 Jan 2026 10:55:53 +0000 Subject: [PATCH 06/16] fix: all map functions to be backward compatible --- pyAMARES/kernel/PriorKnowledge.py | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/pyAMARES/kernel/PriorKnowledge.py b/pyAMARES/kernel/PriorKnowledge.py index 3b81dc0..6bad6fb 100644 --- a/pyAMARES/kernel/PriorKnowledge.py +++ b/pyAMARES/kernel/PriorKnowledge.py @@ -329,13 +329,19 @@ def generateparameter( else: raise NotImplementedError("file format must be Excel (xlsx) or CSV!") - # Compatible with pandas both older and newer than 2.1.0 - pk = ( - pk.map(safe_convert_to_numeric) - if hasattr(pk, "map") - else pk.applymap(safe_convert_to_numeric) - ) # To be compatible with CSV + def backward_compatible_map(df, func): + # Check if the newer 'map' method exists (pandas >= 2.1.0) + if hasattr(df, "map"): + return df.map(func) + # Fallback to the older 'applymap' (pandas < 2.1.0) + elif hasattr(df, "applymap"): + return df.applymap(func) # type: ignore + else: + raise AttributeError( + "Pandas DataFrame has neither 'map' nor 'applymap' method." + ) + pk = backward_compatible_map(pk, safe_convert_to_numeric) peaklist = pk.columns.to_list() # generate a peak list directly from the [assert_peak_format(x) for x in peaklist] dfini = extractini(pk, MHz=MHz) # Parse initial values @@ -348,17 +354,8 @@ def generateparameter( df_lb2 = unitconverter(df_lb, MHz=MHz) df_ub2 = unitconverter(df_ub, MHz=MHz) # Make sure the bounds are numeric - # Compatible with pandas both older and newer than 2.1.0 - df_lb2 = ( - df_lb2.map(safe_convert_to_numeric) - if hasattr(df_lb2, "map") - else df_lb2.applymap(safe_convert_to_numeric) - ) - df_ub2 = ( - df_ub2.map(safe_convert_to_numeric) - if hasattr(df_ub2, "map") - else df_ub2.applymap(safe_convert_to_numeric) - ) + df_lb2 = backward_compatible_map(df_lb2, safe_convert_to_numeric) + df_ub2 = backward_compatible_map(df_ub2, safe_convert_to_numeric) if g_global is False: logger.debug( "Parameter g will be fit with the initial value set in the file %s" % fname From 84b0b082596547ca9f5cc871e546c686ef788663 Mon Sep 17 00:00:00 2001 From: Jia Xu Date: Wed, 21 Jan 2026 22:45:23 -0600 Subject: [PATCH 07/16] Fix errors introduced by abrupt pandas 2.0 API changes affecting Python 3.11+ --- pyAMARES/__init__.py | 2 +- pyAMARES/kernel/PriorKnowledge.py | 5 ++++ pyAMARES/util/hsvd.py | 49 ++++++++++++++++++++----------- 3 files changed, 38 insertions(+), 18 deletions(-) diff --git a/pyAMARES/__init__.py b/pyAMARES/__init__.py index 453b135..dc91ce3 100644 --- a/pyAMARES/__init__.py +++ b/pyAMARES/__init__.py @@ -1,5 +1,5 @@ __author__ = "Jia Xu, MR Research Facility, University of Iowa" -__version__ = "0.3.33" +__version__ = "0.3.34dev" # print("Current pyAMARES version is %s" % __version__) # print("Author: %s" % __author__) diff --git a/pyAMARES/kernel/PriorKnowledge.py b/pyAMARES/kernel/PriorKnowledge.py index 6bad6fb..768310d 100644 --- a/pyAMARES/kernel/PriorKnowledge.py +++ b/pyAMARES/kernel/PriorKnowledge.py @@ -5,6 +5,11 @@ import matplotlib.pyplot as plt import numpy as np import pandas as pd + +# Disable automatic string dtype inference for pandas 3.0 and above +if hasattr(pd.options, "future") and hasattr(pd.options.future, "infer_string"): + pd.options.future.infer_string = False + from lmfit import Parameters from ..libs.logger import get_logger diff --git a/pyAMARES/util/hsvd.py b/pyAMARES/util/hsvd.py index c2cc4b6..4199308 100644 --- a/pyAMARES/util/hsvd.py +++ b/pyAMARES/util/hsvd.py @@ -194,23 +194,36 @@ def uniquify_dataframe(df): pandas.DataFrame: A DataFrame where for each unique ``name``, only the entry with the maximum absolute ``ak`` value retains its name, and others have their ``name`` set to NaN. """ - - def process_group(group): - if len(group) > 1: - # Find the index of the row with the max absolute 'ak' value - max_ak_idx = group["ak"].abs().idxmax() - # Set 'name' to NaN for all other rows - group.loc[group.index != max_ak_idx, "name"] = np.nan - return group - - df_non_nan = ( - df[df["name"].notna()].groupby("name", group_keys=False).apply(process_group) - ) - - df_nan = df[df["name"].isna()] - result_df = pd.concat([df_non_nan, df_nan]).sort_index() - - return result_df + pd_version = tuple(int(x) for x in pd.__version__.split(".")[:2]) + + if pd_version >= (3, 0): + # pandas 3.0+: grouping column excluded from apply, use index-based approach + df = df.copy() + non_nan_mask = df["name"].notna() + idx_to_keep = ( + df.loc[non_nan_mask] + .groupby("name")["ak"] + .apply(lambda x: x.abs().idxmax()) + .values + ) + df.loc[non_nan_mask & ~df.index.isin(idx_to_keep), "name"] = np.nan + return df + else: + # pandas 2.x: original approach with copy fix + def process_group(group): + if len(group) > 1: + group = group.copy() + max_ak_idx = group["ak"].abs().idxmax() + group.loc[group.index != max_ak_idx, "name"] = np.nan + return group + + df_non_nan = ( + df[df["name"].notna()] + .groupby("name", group_keys=False) + .apply(process_group) + ) + df_nan = df[df["name"].isna()] + return pd.concat([df_non_nan, df_nan]).sort_index() def HSVDinitializer( @@ -279,7 +292,9 @@ def HSVDinitializer( if fitting_parameters is None: # Initialize parameters when there is no prior knowledge temp_to_unfold = assign_hsvd_peaks(p_pd, None) + print("temp_to_unfold before uniquify:", temp_to_unfold) temp_to_unfold = uniquify_dataframe(temp_to_unfold) + print("temp_to_unfold after uniquify:", temp_to_unfold) allpara_hsvd = hsvd_initialize_parameters( temp_to_unfold.dropna(subset=["name"]), None, From f67bf3857b0938d7b1bca28684704a81dba5535c Mon Sep 17 00:00:00 2001 From: Sebastian Bauer Date: Mon, 8 Dec 2025 17:26:51 +0000 Subject: [PATCH 08/16] chore: remove old get logger and replace with loguru and remove old print statements --- pyAMARES/__init__.py | 1 - pyAMARES/fileio/readfidall.py | 8 ++------ pyAMARES/fileio/readmat.py | 10 +--------- pyAMARES/fileio/readnifti.py | 5 +---- pyAMARES/kernel/PriorKnowledge.py | 18 +----------------- pyAMARES/kernel/fid.py | 13 +------------ pyAMARES/kernel/lmfit.py | 9 +-------- pyAMARES/libs/MPFIR.py | 4 ---- pyAMARES/util/crlb.py | 21 +-------------------- pyAMARES/util/hsvd.py | 12 ++---------- pyAMARES/util/multiprocessing.py | 17 +++-------------- pyAMARES/util/report.py | 9 +-------- pyAMARES/util/visualization.py | 1 - 13 files changed, 14 insertions(+), 114 deletions(-) diff --git a/pyAMARES/__init__.py b/pyAMARES/__init__.py index dc91ce3..20c9acd 100644 --- a/pyAMARES/__init__.py +++ b/pyAMARES/__init__.py @@ -1,7 +1,6 @@ __author__ = "Jia Xu, MR Research Facility, University of Iowa" __version__ = "0.3.34dev" -# print("Current pyAMARES version is %s" % __version__) # print("Author: %s" % __author__) from .fileio import * # noqa: F403 diff --git a/pyAMARES/fileio/readfidall.py b/pyAMARES/fileio/readfidall.py index ae0d7c1..e51d7ef 100644 --- a/pyAMARES/fileio/readfidall.py +++ b/pyAMARES/fileio/readfidall.py @@ -2,12 +2,9 @@ import mat73 import numpy as np +from loguru import logger from scipy import io -from ..libs.logger import get_logger - -logger = get_logger(__name__) - def is_mat_file_v7_3(filename): with open(filename, "rb") as f: @@ -135,7 +132,6 @@ def read_fidall(filename): "Note pyAMARES.fitAMARES only fits 1D MRS data, however, your data shape is {data.shape}. Is it MRSI or raw MRS data that needs to be coil-combined?" ) - # print("data.shape=", data.shape) - logger.debug("data.shape=%s", data.shape) + logger.debug("data.shape=%s", data.shape) return header, data diff --git a/pyAMARES/fileio/readmat.py b/pyAMARES/fileio/readmat.py index 8d314be..0845014 100644 --- a/pyAMARES/fileio/readmat.py +++ b/pyAMARES/fileio/readmat.py @@ -1,12 +1,10 @@ import mat73 import numpy as np +from loguru import logger from scipy import io -from ..libs.logger import get_logger from .readfidall import is_mat_file_v7_3 -logger = get_logger(__name__) - def readmrs(filename): """ @@ -43,28 +41,23 @@ def readmrs(filename): - For MATLAB files, both traditional (.mat) and V7.3 (.mat) files are supported, but the variable must be named ``fid`` or ``data``. """ if filename.endswith("csv"): - # print("Try to load 2-column CSV") logger.debug("Try to load 2-column CSV") data = np.loadtxt(filename, delimiter=",") data = data[:, 0] + 1j * data[:, 1] elif filename.endswith("txt"): - # print("Try to load 2-column ASCII data") logger.debug("Try to load 2-column ASCII data") data = np.loadtxt(filename, delimiter=" ") data = data[:, 0] + 1j * data[:, 1] elif filename.endswith("npy"): - # print("Try to load python NPY file") logger.debug("Try to load python NPY file") data = np.load(filename) elif filename.endswith("mat"): if is_mat_file_v7_3(filename): - # print("Try to load Matlab V7.3 mat file with the var saved as fid or data") logger.debug( "Try to load Matlab V7.3 mat file with the var saved as fid or data" ) matdic = mat73.loadmat(filename) else: - # print("Try to load Matlab mat file with the var saved as fid or data") logger.debug( "Try to load Matlab mat file with the var saved as fid or data" ) @@ -87,6 +80,5 @@ def readmrs(filename): "Note pyAMARES.fitAMARES only fits 1D MRS data, however, your data shape is {data.shape}. Is it MRSI or raw MRS data that needs to be coil-combined?" ) - # print("data.shape=", data.shape) logger.debug("data.shape=%s", data.shape) return data diff --git a/pyAMARES/fileio/readnifti.py b/pyAMARES/fileio/readnifti.py index 48b0ecb..7135db9 100644 --- a/pyAMARES/fileio/readnifti.py +++ b/pyAMARES/fileio/readnifti.py @@ -1,10 +1,7 @@ import argparse import numpy as np - -from ..libs.logger import get_logger - -logger = get_logger(__name__) +from loguru import logger def read_nifti(filename): diff --git a/pyAMARES/kernel/PriorKnowledge.py b/pyAMARES/kernel/PriorKnowledge.py index 768310d..374dcb7 100644 --- a/pyAMARES/kernel/PriorKnowledge.py +++ b/pyAMARES/kernel/PriorKnowledge.py @@ -11,12 +11,10 @@ pd.options.future.infer_string = False from lmfit import Parameters +from loguru import logger -from ..libs.logger import get_logger from .fid import fft_params -logger = get_logger(__name__) - def safe_convert_to_numeric(x): try: @@ -292,8 +290,6 @@ def find_header_row(filename, comment_char="#"): processedline = line.replace('"', "").replace("'", "").strip() if not processedline.startswith(comment_char): return i - # else: - # print("Comment:", processedline) return None # Return None if all lines are comments or file is empty @@ -353,7 +349,6 @@ def backward_compatible_map(df, func): dfini2 = unitconverter( dfini, MHz=MHz ) # Convert ppm to Hz, convert FWHM to dk, convert degree to radians. - # print(f"{dfini2=}") df_lb, df_ub = parse_bounds(pk) # Parse bounds df_expr = extract_expr(pk, MHz=MHz) # Parse expression df_lb2 = unitconverter(df_lb, MHz=MHz) @@ -365,7 +360,6 @@ def backward_compatible_map(df, func): logger.debug( "Parameter g will be fit with the initial value set in the file %s" % fname ) - # print(f"Parameter g will be fit with the initial value set in the file {fname}") allpara = Parameters() for peak in dfini2.columns: for i, para in enumerate(paramter_prefix): @@ -514,10 +508,6 @@ def initialize_FID( ppm = np.linspace(-sw / 2, sw / 2, fidpt) / np.abs(MHz) Hz = np.linspace(-sw / 2, sw / 2, fidpt) - # print(f"{sw=}") - # print(f"{np.max(ppm)=} {np.min(ppm)=}") - # print(f"{np.max(Hz)=} {np.min(Hz)=}") - # print(f"{-sw/2=}") opts = argparse.Namespace() opts.deadtime = deadtime @@ -609,15 +599,9 @@ def initialize_FID( "new value should be opts.initialParams[%s].value + opts.ppm_offset * opts.MHz=%s" % (p, opts.initialParams[p].value + opts.ppm_offset * opts.MHz) ) - - # print(f"before {opts.initialParams[p].value=}") - # print( - # f"new value should be {opts.initialParams[p].value + opts.ppm_offset * opts.MHz=}" - # ) opts.initialParams[p].value = ( opts.initialParams[p].value + hz_offset ) - # print(f"after {opts.initialParams[p].value=}") logger.debug( "after opts.initialParams[%s].value=%s" % (p, opts.initialParams[p].value) diff --git a/pyAMARES/kernel/fid.py b/pyAMARES/kernel/fid.py index 538258e..ab70f51 100644 --- a/pyAMARES/kernel/fid.py +++ b/pyAMARES/kernel/fid.py @@ -2,10 +2,7 @@ import matplotlib.pyplot as plt import nmrglue as ng import numpy as np - -from ..libs.logger import get_logger - -logger = get_logger(__name__) +from loguru import logger def interleavefid(fid): @@ -147,10 +144,6 @@ def Jac6(params, x, fid=None): dk = np.array(poptall[2::5]) g = np.array(poptall[4::5]) - # if len(g[g > 1]) > 0: - # print("Warning, g>1", g) - # if len(g[g < 0]): - # print("warning! g<0", g) g[g > 1] = 1.0 g[g < 0] = 0.0 @@ -197,10 +190,6 @@ def Jac6c(params, x, fid=None): dk = np.array(poptall[2::5]) # noqa F841 g = np.array(poptall[4::5]) - # if len(g[g > 1]) > 0: - # print("Warning, g>1", g) - # if len(g[g < 0]): - # print("warning! g<0", g) g[g > 1] = 1.0 g[g < 0] = 0.0 diff --git a/pyAMARES/kernel/lmfit.py b/pyAMARES/kernel/lmfit.py index c2b6601..0243e8a 100644 --- a/pyAMARES/kernel/lmfit.py +++ b/pyAMARES/kernel/lmfit.py @@ -4,13 +4,11 @@ import numpy as np import pandas as pd from lmfit import Minimizer, Parameters +from loguru import logger -from ..libs.logger import get_logger from .fid import Compare_to_OXSA, fft_params from .objective_func import default_objective -logger = get_logger(__name__) - def check_removed_expr(df): """ @@ -71,7 +69,6 @@ def filter_param_by_ppm(allpara, fit_ppm, MHz, delta=100): DataFrame: DataFrame filtered based on the specified criteria. """ fit_Hz = np.array(fit_ppm) * MHz - # print(f"{fit_Hz=}") logger.info("fit_Hz=%s" % fit_Hz) tofilter_pd = parameters_to_dataframe(allpara) chemshift_pd = tofilter_pd[tofilter_pd["name"].str.startswith("freq")] @@ -204,7 +201,6 @@ def result_pd_to_params(result_table, MHz=120.0): params = Parameters() for row in result_table.iterrows(): - # print(row[0]) for name in df_name: value = row[1][name] new_name = name_dic[name] + "_" + row[0] @@ -353,7 +349,6 @@ def fitAMARES_kernel( from ..util import get_ppm_limit fit_range = get_ppm_limit(fid_parameters.ppm, fit_range) - # print(f"Fitting range {fid_parameters.ppm[fit_range[0]]} ppm to {fid_parameters.ppm[fit_range[1]]} ppm!") logger.debug( "Fitting range %s ppm to %s ppm!" % (fid_parameters.ppm[fit_range[0]], fid_parameters.ppm[fit_range[1]]) @@ -373,7 +368,6 @@ def fitAMARES_kernel( else: out_obj = min_obj.minimize(method=method) timeafter = datetime.now() - # print(f"Fitting with {method=} took {(timeafter - timebefore).total_seconds()} seconds") logger.debug( "Fitting with method=%s took %s seconds" % (method, (timeafter - timebefore).total_seconds()) @@ -543,7 +537,6 @@ def plotAMARES(fid_parameters, fitted_params=None, plotParameters=None, filename ).T if plotParameters is None: plotParameters = fid_parameters.plotParameters - # print(f"{plotParameters.xlim=}") combined_plot( amares_arr, ppm=fid_parameters.ppm, diff --git a/pyAMARES/libs/MPFIR.py b/pyAMARES/libs/MPFIR.py index fd24871..b0d3869 100644 --- a/pyAMARES/libs/MPFIR.py +++ b/pyAMARES/libs/MPFIR.py @@ -87,7 +87,6 @@ def minphlpnew(h0): def pbfirnew(wl, wh, signal, ri, M0): N = np.max(signal.shape) wc = (wh - wl) / 2 - # print(f"{wc=} {wh=} {wl=}") noise = np.std(np.real(signal[-20:])) maxs = np.max(np.abs(np.fft.fft(signal))) / np.sqrt(N) @@ -102,7 +101,6 @@ def pbfirnew(wl, wh, signal, ri, M0): supold = sup # Initialize here while ok == 1: - # print(f'try M={M} wc={wc} ri={ri} sup={sup}') fir_h = fircls1(M, wc, ri, sup) fir_h = minphlpnew(fir_h) M2 = len(fir_h) @@ -113,7 +111,6 @@ def pbfirnew(wl, wh, signal, ri, M0): phas = np.pi + np.arctan(np.imag(phastemp) / np.real(phastemp)) else: phas = np.arctan(np.imag(phastemp) / np.real(phastemp)) - # print(f"{phas=}") fir_h = fir_h * np.exp(-1j * phas) # f = filter(fir_h, 1, signal[::-1]) f = lfilter(fir_h, [1], signal[::-1]) # needs to check @@ -208,7 +205,6 @@ def MPFIR( xmax = (max(ppm_range) - carrier) * frequency / 1e6 # in kHz wl = xmin * 2 * step wh = xmax * 2 * step - # print(f"{wl=} {wh=}") fir_h = pbfirnew(wl, wh, signal, rippass, M) signal = lfilter(np.flip(fir_h), 1, signal) signal = np.concatenate([signal[len(fir_h) - 1 :], np.zeros(len(fir_h) - 1)]) diff --git a/pyAMARES/util/crlb.py b/pyAMARES/util/crlb.py index 552f3b4..390b8c5 100644 --- a/pyAMARES/util/crlb.py +++ b/pyAMARES/util/crlb.py @@ -4,14 +4,12 @@ import numpy as np import scipy import sympy +from loguru import logger from sympy.parsing import sympy_parser from ..kernel import Jac6, multieq6, uninterleave -from ..libs.logger import get_logger from .report import report_crlb -logger = get_logger(__name__) - def calculateCRB(D, variance, P=None, verbose=False, condthreshold=1e11, cond=False): """ @@ -39,9 +37,7 @@ def calculateCRB(D, variance, P=None, verbose=False, condthreshold=1e11, cond=Fa D = uninterleave(D) Dmat = np.dot(D.conj().T, D) if verbose: - # print("D.shape", D.shape, "Dmat.shape", Dmat.shape) logger.debug("D.shape=%s Dmat.shape=%s" % (D.shape, Dmat.shape)) - # print("P.shape=%s" % str(P.shape)) logger.debug("P.shape=%s" % str(P.shape)) # Compute the Fisher information matrix @@ -51,12 +47,9 @@ def calculateCRB(D, variance, P=None, verbose=False, condthreshold=1e11, cond=Fa else: Fisher = np.real(P.T @ Dmat @ P) / variance if verbose: - # print("Fisher.shape=%s P.shape=%s" % (Fisher.shape, P.shape)) logger.debug("Fisher.shape=%s P.shape=%s" % (Fisher.shape, P.shape)) condition_number = np.linalg.cond(Fisher) if condition_number > condthreshold: - # print("Warning: The matrix may be ill-conditioned. Condition number is high:" - # , condition_number) logger.warning( f"The matrix may be ill-conditioned. Condition number is high: " f"{condition_number:3.3e}" @@ -70,7 +63,6 @@ def calculateCRB(D, variance, P=None, verbose=False, condthreshold=1e11, cond=Fa # Ensure non-negative covariance values if np.min(CRBcov) < 0: if verbose: - # print("np.min(CRBcov)=%s, make the negative values to 0!" % # np.min(CRBcov)) logger.warning( "np.min(CRBcov)=%s, make the negative values to 0!" % np.min(CRBcov) @@ -85,7 +77,6 @@ def calculateCRB(D, variance, P=None, verbose=False, condthreshold=1e11, cond=Fa else: return False if np.max(np.diag(CRBcov)) < 1e-5: - # print("Ill conditioned matrix! CRLB not reliable!") logger.warning("Ill conditioned matrix! CRLB not reliable!") if verbose: msg = ["\n Debug Information:"] @@ -96,10 +87,6 @@ def calculateCRB(D, variance, P=None, verbose=False, condthreshold=1e11, cond=Fa msg.append(f"Max mDTD: {np.max(Dmat):.2e}") msg_string = "\n ".join(msg) logger.debug(msg_string) - # print("CRBcov.shape", CRBcov.shape) - # print("max CRBcov", np.max(np.diag(CRBcov))) - # print("max Fisher %2.2e" % np.max(Fisher)) - # print("max mDTD %2.2e" % np.max(Dmat)) return np.sqrt(np.diag(CRBcov)) @@ -158,7 +145,6 @@ def evaluateCRB(outparams, opts, P=None, Jacfunc=Jac6, verbose=False): ) if verbose: - # print("opts.D.shape=%s" % str(opts.D.shape)) logger.debug("opts.D.shape=%s" % str(opts.D.shape)) plt.plot(opts.residual.real) plt.title("residual") @@ -205,8 +191,6 @@ def create_pmatrix(pkpd, verbose=False, ifplot=False): """ # Extract parameter indices and expressions for Equation 3 in the # Reference. S Cavassila et al NMR Biomed. 2001 Jun;14(4):278-83 - # print(f"{[extract_strings(x) for x in pkpd.dropna(axis=0)['expr']]=}") - # print(f"{pkpd.columns=} {pkpd.index=} {pkpd['name']=}") pm_index = get_matches( pkpd, [extract_strings(x) for x in pkpd.dropna(axis=0)["expr"]] ) @@ -227,7 +211,6 @@ def create_pmatrix(pkpd, verbose=False, ifplot=False): ] # pass freepd ID to all ID. ID will be NaNs for fixed variables pm_index2 = pkpd2.iloc[pm_index]["newid"].to_list() if np.all(np.isnan(pm_index2)): # If all NaN - # print(f"{pm_index2=}") logger.warning( "pm_index are all NaNs, return None so that P matrix is a identity matrix!" ) @@ -238,14 +221,12 @@ def create_pmatrix(pkpd, verbose=False, ifplot=False): # Fill the diagonal for free parameters for ind in freepd.index: if verbose: - # print("ind=%s newid=%s" % (ind, freepd.loc[ind]["newid"])) logger.debug("ind=%s newid=%s" % (ind, freepd.loc[ind]["newid"])) Pmatrix[freepd.loc[ind]["newid"], ind] = 1.0 # Fill in partial derivatives for parameter relationships for x, y, partial_d in zip(pl_index, pm_index, plm): if verbose: - # print("x=%s y=%s partial_d=%s" % (x, y, partial_d)) logger.debug("x=%s y=%s partial_d=%s" % (x, y, partial_d)) Pmatrix[y, x] = partial_d diff --git a/pyAMARES/util/hsvd.py b/pyAMARES/util/hsvd.py index 4199308..9f29002 100644 --- a/pyAMARES/util/hsvd.py +++ b/pyAMARES/util/hsvd.py @@ -19,12 +19,11 @@ # 2025-03-20 from ..libs import hlsvd +from loguru import logger + from ..kernel.fid import Compare_to_OXSA, equation6, interleavefid, uninterleave from ..kernel.lmfit import parameters_to_dataframe from ..libs.hlsvd import create_hlsvd_fids -from ..libs.logger import get_logger - -logger = get_logger(__name__) def HSVDp0(hsvdfid, timeaxis, ppm, MHz=120, ifplot=True): @@ -161,11 +160,6 @@ def hsvd_initialize_parameters(temp_to_unfold, allpara_hsvd=None, g_global=0.0): var_name ].vary: # v0.23c, HSVDinitializer only changes varying parameters if var_name.startswith("ak") and var < 0: - # print( - # "Warning ak for %s %s is negative!, Make it positive - # and flip the phase!" - # % (peak_name, var) - # ) logger.warning( "ak for %s %s is negative!, Make it positive and flip the " "phase!" % (peak_name, var) @@ -275,13 +269,11 @@ def HSVDinitializer( ) plist.append(p2) if verbose: - # print("fitted p0", p2) logger.debug("fitted p0 %s" % p2) p_pd = pd.DataFrame(np.array(plist)) p_pd.columns = ["ak", "freq", "dk", "phi", "g"] if verbose: - # print("Filtering peaks with linewidth broader than %i Hz" % lw_threshold) logger.debug("Filtering peaks with linewidth broader than %i Hz" % lw_threshold) p_pd = p_pd[p_pd["dk"] < lw_threshold] # filter out too broadened peaks p_pd["g"] = ( diff --git a/pyAMARES/util/multiprocessing.py b/pyAMARES/util/multiprocessing.py index 75b856d..ded0996 100644 --- a/pyAMARES/util/multiprocessing.py +++ b/pyAMARES/util/multiprocessing.py @@ -4,10 +4,9 @@ from copy import deepcopy from datetime import datetime -from ..kernel.lmfit import fitAMARES -from ..libs.logger import get_logger +from loguru import logger -logger = get_logger(__name__) +from ..kernel.lmfit import fitAMARES @contextlib.contextmanager @@ -89,7 +88,6 @@ def fit_dataset( del out return result_table except Exception as e: - # print(f"Error in fit_dataset: {e}") logger.critical("Error in fit_dataset: %s", e) return None @@ -141,12 +139,10 @@ def run_parallel_fitting_with_progress( try: del FIDobj_shared.styled_df except AttributeError: - # print("There is no styled_df!") logger.warning("There is no styled_df!") try: del FIDobj_shared.simple_df except AttributeError: - # print("There is no styled_df!") logger.warning("There is no simple_df!") timebefore = datetime.now() results = [] @@ -170,14 +166,7 @@ def run_parallel_fitting_with_progress( results.append(future.result()) timeafter = datetime.now() - # print( - # "Fitting %i spectra with %i processors took %i seconds" - # % (len(fid_arrs), num_workers, (timeafter - timebefore).total_seconds()) - # ) logger.info( - "Fitting %i spectra with %i processors took %i seconds", - len(fid_arrs), - num_workers, - (timeafter - timebefore).total_seconds(), + f"Fitting {len(fid_arrs)} spectra with {num_workers} processors took {(timeafter - timebefore).total_seconds()} seconds" ) return results diff --git a/pyAMARES/util/report.py b/pyAMARES/util/report.py index 670303b..755e311 100644 --- a/pyAMARES/util/report.py +++ b/pyAMARES/util/report.py @@ -1,5 +1,6 @@ import numpy as np import pandas as pd +from loguru import logger from ..kernel import ( Jac6, @@ -8,9 +9,6 @@ parameters_to_dataframe_result, remove_zero_padding, ) -from ..libs.logger import get_logger - -logger = get_logger(__name__) try: import jinja2 # noqa F401 @@ -65,7 +63,6 @@ def report_crlb(outparams, crlb, Jacfunc=None): pandas.DataFrame: DataFrame with CRLB information for relevant parameters. """ pdpoptall = parameters_to_dataframe_result(outparams) - # print(f"{Jacfunc=}, {pdpoptall=} {outparams=}") if Jacfunc is None or Jacfunc is Jac6: # You'll need to assert there is a peaklist in the fid_parameters poptall = pdpoptall["value"] @@ -74,8 +71,6 @@ def report_crlb(outparams, crlb, Jacfunc=None): # elif Jacfunc is flexible_Jac: # poptall = pdpoptall[pdpoptall['vary']]['value'] # Parameters with vary=True else: - # print(f"{Jacfunc=} is not supported!") - # print("Jacfunc=%s is not supported!" % Jacfunc) logger.warning(f"Jacfunc={Jacfunc} is not supported!") resultpd = pdpoptall.loc[poptall.index] @@ -328,7 +323,6 @@ def report_amares(outparams, fid_parameters, verbose=False): ) else: simple_df = None - # print("There is no result_sum generated, simple_df is set to None") logger.warning( "There is no result_sum generated, simple_df is set to None" ) @@ -340,7 +334,6 @@ def report_amares(outparams, fid_parameters, verbose=False): simple_df = extract_key_parameters(fid_parameters.result_sum) else: simple_df = None - # print("There is no result_sum generated, simple_df is set to None") logger.warning( "There is no result_sum generated, simple_df is set to None" ) diff --git a/pyAMARES/util/visualization.py b/pyAMARES/util/visualization.py index 8fc7973..001a88b 100644 --- a/pyAMARES/util/visualization.py +++ b/pyAMARES/util/visualization.py @@ -171,7 +171,6 @@ def combined_plot( filename (str or None, optional): If provided, the figure will be saved to this file. Defaults to None. """ - # print(f"{xlim=}") fig, (ax1, ax2) = plt.subplots( 2, 1, figsize=(10, 8), sharex=True, layout="constrained" ) From 609414f3cb0e55c779ba5f9ed5ac071d4d8e4b44 Mon Sep 17 00:00:00 2001 From: Sebastian Bauer Date: Mon, 8 Dec 2025 17:56:40 +0000 Subject: [PATCH 09/16] chore: change old format strings to newer generation fstrings --- pyAMARES/__init__.py | 2 +- pyAMARES/fileio/readfidall.py | 4 ++-- pyAMARES/fileio/readmat.py | 2 +- pyAMARES/kernel/PriorKnowledge.py | 31 +++++++++++++------------------ pyAMARES/kernel/fid.py | 16 ++++++---------- pyAMARES/kernel/lmfit.py | 16 ++++++---------- pyAMARES/libs/MPFIR.py | 3 +-- pyAMARES/util/crlb.py | 22 +++++++++------------- pyAMARES/util/hsvd.py | 7 +++---- pyAMARES/util/multiprocessing.py | 2 +- pyAMARES/util/report.py | 9 ++++----- 11 files changed, 47 insertions(+), 67 deletions(-) diff --git a/pyAMARES/__init__.py b/pyAMARES/__init__.py index 20c9acd..f42944a 100644 --- a/pyAMARES/__init__.py +++ b/pyAMARES/__init__.py @@ -1,7 +1,7 @@ __author__ = "Jia Xu, MR Research Facility, University of Iowa" __version__ = "0.3.34dev" -# print("Author: %s" % __author__) +# print(f"Author: {__author__)}" from .fileio import * # noqa: F403 from .kernel import * # noqa: F403 diff --git a/pyAMARES/fileio/readfidall.py b/pyAMARES/fileio/readfidall.py index e51d7ef..0195682 100644 --- a/pyAMARES/fileio/readfidall.py +++ b/pyAMARES/fileio/readfidall.py @@ -129,9 +129,9 @@ def read_fidall(filename): if len(data.shape) != 1: logger.warning( - "Note pyAMARES.fitAMARES only fits 1D MRS data, however, your data shape is {data.shape}. Is it MRSI or raw MRS data that needs to be coil-combined?" + f"Note pyAMARES.fitAMARES only fits 1D MRS data, however, your data shape is {data.shape}. Is it MRSI or raw MRS data that needs to be coil-combined?" ) - logger.debug("data.shape=%s", data.shape) + logger.debug(f"data.shape={data.shape}") return header, data diff --git a/pyAMARES/fileio/readmat.py b/pyAMARES/fileio/readmat.py index 0845014..e396d2e 100644 --- a/pyAMARES/fileio/readmat.py +++ b/pyAMARES/fileio/readmat.py @@ -80,5 +80,5 @@ def readmrs(filename): "Note pyAMARES.fitAMARES only fits 1D MRS data, however, your data shape is {data.shape}. Is it MRSI or raw MRS data that needs to be coil-combined?" ) - logger.debug("data.shape=%s", data.shape) + logger.debug(f"data.shape={data.shape}") return data diff --git a/pyAMARES/kernel/PriorKnowledge.py b/pyAMARES/kernel/PriorKnowledge.py index 374dcb7..ac6923e 100644 --- a/pyAMARES/kernel/PriorKnowledge.py +++ b/pyAMARES/kernel/PriorKnowledge.py @@ -268,13 +268,12 @@ def assert_peak_format(input_str): if re.search(r"\.\d+$", input_str): logger.error(msg) raise ValueError( - "The peak name %s cannot end with a floating-point number!" % input_str + f"The peak name {input_str} cannot end with a floating-point number!" ) if re.search(r"\d+[\D]", input_str) or re.search(r"^\d+", input_str): logger.error(msg) raise ValueError( - "The peak name %s cannot contain numbers at the beginning or in the middle!" - % input_str + f"The peak name {input_str} cannot contain numbers at the beginning or in the middle!" ) @@ -284,7 +283,7 @@ def find_header_row(filename, comment_char="#"): logger.info("Checking comment lines in the prior knowledge file") for i, line in enumerate(file): if "#" in line: - logger.info("Comment: in line %d: %s", i, line) + logger.info(f"Comment: in line {i}: {line}") with open(filename, "r") as file: for i, line in enumerate(file): processedline = line.replace('"', "").replace("'", "").strip() @@ -358,7 +357,7 @@ def backward_compatible_map(df, func): df_ub2 = backward_compatible_map(df_ub2, safe_convert_to_numeric) if g_global is False: logger.debug( - "Parameter g will be fit with the initial value set in the file %s" % fname + f"Parameter g will be fit with the initial value set in the file {fname}" ) allpara = Parameters() for peak in dfini2.columns: @@ -492,15 +491,13 @@ def initialize_FID( dwelltime = 1.0 / sw if truncate_initial_points > 0: logger.info( - "Truncating %i points from the beginning of the FID signal" - % truncate_initial_points + f"Truncating {truncate_initial_points} points from the beginning of the FID signal" ) deadtime_old = deadtime * 1.0 deadtime = deadtime + truncate_initial_points * dwelltime fid = fid[truncate_initial_points:] logger.info( - "The deadtime is changing from %f seconds to %f seconds" - % (deadtime_old, deadtime) + f"The deadtime is changing from {deadtime} seconds to {deadtime_old} seconds" ) fidpt = len(fid) # TD = fidpt * 2 @@ -518,7 +515,7 @@ def initialize_FID( # This must be done before the shifting FID for carrier. fid = np.conj(fid) if carrier != 0: - logger.info("Shift FID so that center frequency is at %s ppm!" % carrier) + logger.info(f"Shift FID so that center frequency is at {carrier} ppm!") fid = fid * np.exp(1j * 2 * np.pi * carrier * MHz * opts.timeaxis) # ppm = ppm + carrier # Hz = Hz + carrier / np.abs(MHz) @@ -579,7 +576,7 @@ def initialize_FID( timeaxis=opts.timeaxis, params=opts.initialParams, fid=True ) if ppm_offset != 0: - logger.info("Shifting the ppm by ppm_offset=%2.2f ppm" % ppm_offset) + logger.info(f"Shifting the ppm by ppm_offset={ppm_offset:.2f} ppm") for p in opts.initialParams: if p.startswith("freq"): hz_offset = opts.ppm_offset * opts.MHz @@ -592,19 +589,17 @@ def initialize_FID( ): # Check if there's an upper bound set opts.initialParams[p].max += hz_offset logger.debug( - "before opts.initialParams[%s].value=%s" - % (p, opts.initialParams[p].value) + f"before opts.initialParams[{p}].value={opts.initialParams[p].value}" ) logger.debug( - "new value should be opts.initialParams[%s].value + opts.ppm_offset * opts.MHz=%s" - % (p, opts.initialParams[p].value + opts.ppm_offset * opts.MHz) + f"new value should be opts.initialParams[{p}].value + opts.ppm_offset * opts.MHz=" + f"{opts.initialParams[p].value + opts.ppm_offset * opts.MHz}" ) opts.initialParams[p].value = ( opts.initialParams[p].value + hz_offset ) logger.debug( - "after opts.initialParams[%s].value=%s" - % (p, opts.initialParams[p].value) + f"after opts.initialParams[{p}].value={opts.initialParams[p].value}" ) opts.allpara = opts.initialParams # obsolete API, will be removed @@ -624,7 +619,7 @@ def initialize_FID( plt.xlabel("ppm") plt.show() if priorknowledgefile is not None: - logger.info("Printing the Prior Knowledge File %s" % priorknowledgefile) + logger.info(f"Printing the Prior Knowledge File {priorknowledgefile}") try: from IPython.display import display diff --git a/pyAMARES/kernel/fid.py b/pyAMARES/kernel/fid.py index ab70f51..383d040 100644 --- a/pyAMARES/kernel/fid.py +++ b/pyAMARES/kernel/fid.py @@ -313,9 +313,9 @@ def Compare_to_OXSA(inputfid, resultfid): dataNormSq = np.linalg.norm(inputfid - np.mean(inputfid)) ** 2 resNormSq = np.sum(np.abs((resultfid - inputfid)) ** 2) relativeNorm = resNormSq / dataNormSq - logger.info("Norm of residual = %3.3f" % resNormSq) - logger.info("Norm of the data = %3.3f" % dataNormSq) - logger.info("resNormSq / dataNormSq = %3.3f" % relativeNorm) + logger.info(f"Norm of residual = {resNormSq:.3f}") + logger.info(f"Norm of the data = {dataNormSq:.3f}") + logger.info(f"resNormSq / dataNormSq = {relativeNorm:.3f}") return resNormSq, relativeNorm @@ -411,9 +411,7 @@ def simulate_fid( timeaxis = np.arange(0, dwelltime * fid_len, dwelltime) + deadtime # timeaxis fidsim = uninterleave(multieq6(x=timeaxis, params=params)) if extra_line_broadening > 0: - logger.info( - "Applying extra line broadening of %2.2f Hz" % extra_line_broadening - ) + logger.info(f"Applying extra line broadening of {extra_line_broadening:.2f} Hz") fidsim = ng.proc_base.em(fidsim, extra_line_broadening / sw) if snr_target is not None: fidsim = add_noise_FID(fidsim, snr_target, indsignal, pts_noise) @@ -423,10 +421,8 @@ def simulate_fid( label = "Pure FID" plt.title("Simulated FID") else: - label = "SNR=%2.2f" % fidSNR( - fid=fidsim, indsignal=indsignal, pts_noise=pts_noise - ) - plt.title("Simulated FID with an SNR of %2.2f" % snr_target) + label = f"SNR={fidSNR(fid=fidsim, indsignal=indsignal, pts_noise=pts_noise):.2f}" + plt.title(f"Simulated FID with an SNR of {snr_target:.2f}") plt.plot(Hz, np.real(ng.proc_base.fft(fidsim)), label=label) plt.legend() plt.xlabel("Hz") diff --git a/pyAMARES/kernel/lmfit.py b/pyAMARES/kernel/lmfit.py index 0243e8a..ccfac44 100644 --- a/pyAMARES/kernel/lmfit.py +++ b/pyAMARES/kernel/lmfit.py @@ -69,7 +69,7 @@ def filter_param_by_ppm(allpara, fit_ppm, MHz, delta=100): DataFrame: DataFrame filtered based on the specified criteria. """ fit_Hz = np.array(fit_ppm) * MHz - logger.info("fit_Hz=%s" % fit_Hz) + logger.info(f"fit_Hz={fit_Hz}") tofilter_pd = parameters_to_dataframe(allpara) chemshift_pd = tofilter_pd[tofilter_pd["name"].str.startswith("freq")] @@ -350,8 +350,7 @@ def fitAMARES_kernel( fit_range = get_ppm_limit(fid_parameters.ppm, fit_range) logger.debug( - "Fitting range %s ppm to %s ppm!" - % (fid_parameters.ppm[fit_range[0]], fid_parameters.ppm[fit_range[1]]) + f"Fitting range {fid_parameters.ppm[fit_range[0]]} ppm to {fid_parameters.ppm[fit_range[1]]} ppm!" ) min_obj = Minimizer( objective_func, @@ -369,8 +368,7 @@ def fitAMARES_kernel( out_obj = min_obj.minimize(method=method) timeafter = datetime.now() logger.debug( - "Fitting with method=%s took %s seconds" - % (method, (timeafter - timebefore).total_seconds()) + f"Fitting with method={method} took {(timeafter - timebefore).total_seconds()} seconds" ) return out_obj @@ -425,8 +423,7 @@ def fitAMARES( from copy import deepcopy logger.debug( - "A copy of the input fid_parameters will be returned because inplace=%s" - % inplace + f"A copy of the input fid_parameters will be returned because inplace={inplace}" ) fid_parameters = deepcopy(fid_parameters) fitting_parameters = deepcopy(fitting_parameters) @@ -436,7 +433,7 @@ def fitAMARES( tol = np.sqrt(amp0) * 1e-6 fit_kws = {"max_nfev": 1000, "xtol": tol, "ftol": tol} # fit_kws = {'max_nfev':1000, 'xtol':tol, 'ftol':tol, 'gtol':tol} - logger.debug("Autogenerated tol is %3.3e" % tol) + logger.debug(f"Autogenerated tolerance is {tol:.3e}") if not initialize_with_lm: # The old API, without an initializer out_obj = fitAMARES_kernel( @@ -449,8 +446,7 @@ def fitAMARES( ) # fitting kernel else: logger.debug( - "Run internal leastsq initializer to optimize fitting parameters for the next %s fitting" - % method + f"Run internal leastsq initializer to optimize fitting parameters for the next {method} fitting" ) params_LM = fitAMARES_kernel( fid_parameters, diff --git a/pyAMARES/libs/MPFIR.py b/pyAMARES/libs/MPFIR.py index b0d3869..a61f549 100644 --- a/pyAMARES/libs/MPFIR.py +++ b/pyAMARES/libs/MPFIR.py @@ -218,8 +218,7 @@ def MPFIR( ppm_range[1], color="gray", alpha=0.1, - label="selected region\n%i to %i ppm" - % (np.min(ppm_range), np.max(ppm_range)), + label=f"selected region\n{np.min(ppm_range)} to {np.max(ppm_range)} ppm", ) plt.legend() plt.xlabel("ppm") diff --git a/pyAMARES/util/crlb.py b/pyAMARES/util/crlb.py index 390b8c5..6870cd6 100644 --- a/pyAMARES/util/crlb.py +++ b/pyAMARES/util/crlb.py @@ -37,8 +37,8 @@ def calculateCRB(D, variance, P=None, verbose=False, condthreshold=1e11, cond=Fa D = uninterleave(D) Dmat = np.dot(D.conj().T, D) if verbose: - logger.debug("D.shape=%s Dmat.shape=%s" % (D.shape, Dmat.shape)) - logger.debug("P.shape=%s" % str(P.shape)) + logger.debug(f"D.shape={D.shape} Dmat.shape={Dmat.shape}") + logger.debug(f"P.shape={str(P.shape)}") # Compute the Fisher information matrix if P is None: # No prior knowledge @@ -47,7 +47,7 @@ def calculateCRB(D, variance, P=None, verbose=False, condthreshold=1e11, cond=Fa else: Fisher = np.real(P.T @ Dmat @ P) / variance if verbose: - logger.debug("Fisher.shape=%s P.shape=%s" % (Fisher.shape, P.shape)) + logger.debug(f"Fisher.shape={Fisher.shape} P.shape={P.shape}") condition_number = np.linalg.cond(Fisher) if condition_number > condthreshold: logger.warning( @@ -65,10 +65,8 @@ def calculateCRB(D, variance, P=None, verbose=False, condthreshold=1e11, cond=Fa if verbose: # np.min(CRBcov)) logger.warning( - "np.min(CRBcov)=%s, make the negative values to 0!" % np.min(CRBcov) + f"np.min(CRBcov)={np.min(CRBcov)}, make the negative values to 0!" ) - # warnings.warn("np.min(CRBcov)=%s, make the negative values to 0!" % - # np.min(CRBcov), UserWarning) CRBcov[CRBcov < 0] = 0.0 if cond: @@ -135,17 +133,15 @@ def evaluateCRB(outparams, opts, P=None, Jacfunc=Jac6, verbose=False): try: opts.variance = float(opts.noise_var) logger.debug( - "The CRLB estimation will be divided by the input variance %s" - % opts.variance + f"The CRLB estimation will be divided by the input variance {opts.variance}" ) except ValueError: logger.error( - "Error: noise_var %s is not a recognized string or a valid number." - % opts.variance + f"Error: noise_var {opts.variance} is not a recognized string or a valid number." ) if verbose: - logger.debug("opts.D.shape=%s" % str(opts.D.shape)) + logger.debug(f"opts.D.shape={str(opts.D.shape)}") plt.plot(opts.residual.real) plt.title("residual") plt.show() @@ -221,13 +217,13 @@ def create_pmatrix(pkpd, verbose=False, ifplot=False): # Fill the diagonal for free parameters for ind in freepd.index: if verbose: - logger.debug("ind=%s newid=%s" % (ind, freepd.loc[ind]["newid"])) + logger.debug(f"ind={ind} newid={freepd.loc[ind]['newid']}") Pmatrix[freepd.loc[ind]["newid"], ind] = 1.0 # Fill in partial derivatives for parameter relationships for x, y, partial_d in zip(pl_index, pm_index, plm): if verbose: - logger.debug("x=%s y=%s partial_d=%s" % (x, y, partial_d)) + logger.debug(f"x={x} y={y} partial_d={partial_d}") Pmatrix[y, x] = partial_d if ifplot: diff --git a/pyAMARES/util/hsvd.py b/pyAMARES/util/hsvd.py index 9f29002..f3577de 100644 --- a/pyAMARES/util/hsvd.py +++ b/pyAMARES/util/hsvd.py @@ -161,8 +161,7 @@ def hsvd_initialize_parameters(temp_to_unfold, allpara_hsvd=None, g_global=0.0): ].vary: # v0.23c, HSVDinitializer only changes varying parameters if var_name.startswith("ak") and var < 0: logger.warning( - "ak for %s %s is negative!, Make it positive and flip the " - "phase!" % (peak_name, var) + f"ak for {peak_name} {var} is negative! Making it positive and flipping the phase!" ) allpara_hsvd[var_name].set(value=np.abs(var)) # Flip the phase @@ -269,12 +268,12 @@ def HSVDinitializer( ) plist.append(p2) if verbose: - logger.debug("fitted p0 %s" % p2) + logger.debug(f"fitted p0 {p2}") p_pd = pd.DataFrame(np.array(plist)) p_pd.columns = ["ak", "freq", "dk", "phi", "g"] if verbose: - logger.debug("Filtering peaks with linewidth broader than %i Hz" % lw_threshold) + logger.debug(f"Filtering peaks with linewidth broader than {lw_threshold} Hz") p_pd = p_pd[p_pd["dk"] < lw_threshold] # filter out too broadened peaks p_pd["g"] = ( fid_parameters.g_global diff --git a/pyAMARES/util/multiprocessing.py b/pyAMARES/util/multiprocessing.py index ded0996..0bcc44f 100644 --- a/pyAMARES/util/multiprocessing.py +++ b/pyAMARES/util/multiprocessing.py @@ -88,7 +88,7 @@ def fit_dataset( del out return result_table except Exception as e: - logger.critical("Error in fit_dataset: %s", e) + logger.critical(f"Error in fit_dataset: {e}") return None diff --git a/pyAMARES/util/report.py b/pyAMARES/util/report.py index 755e311..8fbe44a 100644 --- a/pyAMARES/util/report.py +++ b/pyAMARES/util/report.py @@ -211,9 +211,8 @@ def report_amares(outparams, fid_parameters, verbose=False): negative_amplitude = result["amplitude"] < 0 if negative_amplitude.sum() > 0: logger.warning( - "The amplitude of index %s is negative!" - " Make it positive and flip the phase!", - result.loc[negative_amplitude].index.values, + f"The amplitude of index {result.loc[negative_amplitude].index.values} is negative! " + f"Make it positive and flip the phase!" ) result.loc[negative_amplitude, "amplitude"] = result.loc[ negative_amplitude, "amplitude" @@ -232,7 +231,7 @@ def report_amares(outparams, fid_parameters, verbose=False): val_columns = ["amplitude", "chem shift", "lw", "phase", "g_value"] for col, crlb_col, val_col in zip(sd_columns, crlb_columns, val_columns): if result[col].isnull().all(): - logger.info("%s is all None, use crlb instead!" % col) + logger.info(f"{col} is all None, use crlb instead!") result[col] = result[crlb_col] / 100 * result[val_col] result["chem shift"] = result["chem shift"] / MHz @@ -256,7 +255,7 @@ def report_amares(outparams, fid_parameters, verbose=False): result.loc[result["g_CRLB(%)"] == 0.0, "g_CRLB(%)"] = np.nan zero_ind = remove_zero_padding(fid_parameters.fid) if zero_ind > 0: - logger.info("It seems that zeros are padded after %i" % zero_ind) + logger.info(f"It seems that zeros are padded after {zero_ind}") logger.info("Remove padded zeros from residual estimation!") fid_parameters.fid_padding_removed = fid_parameters.fid[:zero_ind] std_noise = np.std( From febb46f2cf3d0a60ed85447b44f1a4258a03d28c Mon Sep 17 00:00:00 2001 From: Sebastian Bauer Date: Mon, 8 Dec 2025 18:07:19 +0000 Subject: [PATCH 10/16] refactor: remove stdout capture and use additional logger to set level. Would require to set specific level in other code to get less verbosity --- pyAMARES/util/multiprocessing.py | 61 ++++++++++++-------------------- 1 file changed, 22 insertions(+), 39 deletions(-) diff --git a/pyAMARES/util/multiprocessing.py b/pyAMARES/util/multiprocessing.py index 0bcc44f..35c9496 100644 --- a/pyAMARES/util/multiprocessing.py +++ b/pyAMARES/util/multiprocessing.py @@ -1,5 +1,3 @@ -import contextlib -import sys from concurrent.futures import ProcessPoolExecutor from copy import deepcopy from datetime import datetime @@ -9,24 +7,6 @@ from ..kernel.lmfit import fitAMARES -@contextlib.contextmanager -def redirect_stdout_to_file(filename): - """ - A context manager that redirects stdout and stderr to a specified file. - - This function temporarily redirects the standard output (stdout) and - standard error (stderr) streams to a file, capturing all outputs generated - within the context block. - """ - with open(filename, "w") as f: - old_stdout, old_stderr = sys.stdout, sys.stderr - sys.stdout, sys.stderr = f, f - try: - yield - finally: - sys.stdout, sys.stderr = old_stdout, old_stderr - - def fit_dataset( fid_current, FIDobj_shared, @@ -99,7 +79,7 @@ def run_parallel_fitting_with_progress( method="leastsq", initialize_with_lm=False, num_workers=8, - logfilename="multiprocess_log.txt", + logfilename="multiprocess.log", objective_func=None, notebook=True, ): @@ -121,7 +101,7 @@ def run_parallel_fitting_with_progress( initialize_with_lm (bool, optional, default False, new in 0.3.9): If True, a Levenberg-Marquardt initializer (``least_sq``) is executed internally. See ``pyAMARES.lmfit.fitAMARES`` for details. num_workers (int, optional): The number of worker processes to use in parallel processing. Defaults to 8. - logfilename (str, optional): The name of the file where the progress log is saved. Defaults to 'multiprocess_log.txt'. + logfilename (str, optional): The name of the file where the progress log is saved. Defaults to 'multiprocess.log'. objective_func (callable, optional): Custom objective function for ``pyAMARES.lmfit.fitAMARES``. If None, the default objective function will be used. Defaults to None. notebook (bool, optional): If True, uses tqdm.notebook for progress display in Jupyter notebooks. @@ -147,23 +127,26 @@ def run_parallel_fitting_with_progress( timebefore = datetime.now() results = [] - with redirect_stdout_to_file(logfilename): - with ProcessPoolExecutor(max_workers=num_workers) as executor: - futures = [ - executor.submit( - fit_dataset, - fid_current=fid_arrs[i, :], - FIDobj_shared=FIDobj_shared, - initial_params=initial_params, - method=method, - initialize_with_lm=initialize_with_lm, - objective_func=objective_func, - ) - for i in range(fid_arrs.shape[0]) - ] - - for future in tqdm(futures, total=len(futures), desc="Processing Datasets"): - results.append(future.result()) + loggerID = logger.add(logfilename, level="DEBUG", rotation="10 MB") + + with ProcessPoolExecutor(max_workers=num_workers) as executor: + futures = [ + executor.submit( + fit_dataset, + fid_current=fid_arrs[i, :], + FIDobj_shared=FIDobj_shared, + initial_params=initial_params, + method=method, + initialize_with_lm=initialize_with_lm, + objective_func=objective_func, + ) + for i in range(fid_arrs.shape[0]) + ] + + for future in tqdm(futures, total=len(futures), desc="Processing Datasets"): + results.append(future.result()) + + logger.remove(loggerID) timeafter = datetime.now() logger.info( From 3669fc20b953a4b6e32fb7863062acc802cbe464 Mon Sep 17 00:00:00 2001 From: Sebastian Bauer Date: Tue, 9 Dec 2025 11:10:27 +0000 Subject: [PATCH 11/16] chore: rotate multiprocessing log file every 10 mins --- pyAMARES/util/multiprocessing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyAMARES/util/multiprocessing.py b/pyAMARES/util/multiprocessing.py index 35c9496..1cc0891 100644 --- a/pyAMARES/util/multiprocessing.py +++ b/pyAMARES/util/multiprocessing.py @@ -127,7 +127,7 @@ def run_parallel_fitting_with_progress( timebefore = datetime.now() results = [] - loggerID = logger.add(logfilename, level="DEBUG", rotation="10 MB") + loggerID = logger.add(logfilename, level="DEBUG", rotation="10 min") with ProcessPoolExecutor(max_workers=num_workers) as executor: futures = [ From 7efbb51fa48d1d42dbebf38baabe3ca495ae662a Mon Sep 17 00:00:00 2001 From: Sebastian Bauer Date: Tue, 9 Dec 2025 12:10:53 +0000 Subject: [PATCH 12/16] feat: log initial parameters used for batch fitting at to logfile --- pyAMARES/util/multiprocessing.py | 43 +++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/pyAMARES/util/multiprocessing.py b/pyAMARES/util/multiprocessing.py index 1cc0891..b50418d 100644 --- a/pyAMARES/util/multiprocessing.py +++ b/pyAMARES/util/multiprocessing.py @@ -2,6 +2,7 @@ from copy import deepcopy from datetime import datetime +import pandas as pd from loguru import logger from ..kernel.lmfit import fitAMARES @@ -80,6 +81,7 @@ def run_parallel_fitting_with_progress( initialize_with_lm=False, num_workers=8, logfilename="multiprocess.log", + loglevel=21, objective_func=None, notebook=True, ): @@ -102,6 +104,7 @@ def run_parallel_fitting_with_progress( If True, a Levenberg-Marquardt initializer (``least_sq``) is executed internally. See ``pyAMARES.lmfit.fitAMARES`` for details. num_workers (int, optional): The number of worker processes to use in parallel processing. Defaults to 8. logfilename (str, optional): The name of the file where the progress log is saved. Defaults to 'multiprocess.log'. + loglevel (int, optional): The logging level for the logger. Defaults to 21 - just above info. objective_func (callable, optional): Custom objective function for ``pyAMARES.lmfit.fitAMARES``. If None, the default objective function will be used. Defaults to None. notebook (bool, optional): If True, uses tqdm.notebook for progress display in Jupyter notebooks. @@ -127,8 +130,34 @@ def run_parallel_fitting_with_progress( timebefore = datetime.now() results = [] - loggerID = logger.add(logfilename, level="DEBUG", rotation="10 min") + loggerID = logger.add(logfilename, level=loglevel, rotation="10 min") + logger.level("BATCH_INFO", no=21) + + data = [ + { + "name": name, + "value": float(par.value), + "min": par.min, + "max": par.max, + "vary": par.vary, + "expr": par.expr, + "brute_step": par.brute_step, + "stderr": par.stderr, + } + for name, par in initial_params.items() + ] + + df = pd.DataFrame(data) + df.set_index("name", inplace=True) + df.sort_values(by="name", inplace=True) + logger.log( + "BATCH_INFO", f"Initial Paramerters used for batch fitting:\n{df.to_string()}" + ) + logger.log( + "BATCH_INFO", + f"Starting fitting of {len(fid_arrs)} datasets with parallel processing. Number of workers: {num_workers}", + ) with ProcessPoolExecutor(max_workers=num_workers) as executor: futures = [ executor.submit( @@ -146,10 +175,16 @@ def run_parallel_fitting_with_progress( for future in tqdm(futures, total=len(futures), desc="Processing Datasets"): results.append(future.result()) - logger.remove(loggerID) + logger.log( + "BATCH_INFO", + "Fitting completed. If no errors were logged, all fits were successful.", + ) timeafter = datetime.now() - logger.info( - f"Fitting {len(fid_arrs)} spectra with {num_workers} processors took {(timeafter - timebefore).total_seconds()} seconds" + logger.log( + "BATCH_INFO", + f"Fitting {len(fid_arrs)} spectra with {num_workers} processors took {(timeafter - timebefore).total_seconds()} seconds", ) + + logger.remove(loggerID) return results From 4e2465ef338acc9dc5cfa2a42a60be4af1297511 Mon Sep 17 00:00:00 2001 From: Sebastian Bauer Date: Tue, 9 Dec 2025 12:11:17 +0000 Subject: [PATCH 13/16] chore: add loguru to requirements and setup --- requirements.txt | 1 + setup.py | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index 266cac8..c27223c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,4 +9,5 @@ lmfit mat73 tqdm notebook +loguru # hlsvdpro==2.0.0 #arm64 does not have it \ No newline at end of file diff --git a/setup.py b/setup.py index e4c9754..850b597 100644 --- a/setup.py +++ b/setup.py @@ -87,6 +87,7 @@ def run(self): "sympy", "nmrglue", "xlrd", + "loguru", "jinja2", "tqdm", "mat73", From 0338a8eb2459b236f564222413338254fd34ac70 Mon Sep 17 00:00:00 2001 From: Sebastian Bauer Date: Tue, 9 Dec 2025 16:10:22 +0000 Subject: [PATCH 14/16] chore: move default log level above warning to get rid off ill conditioned warning, only log errors --- pyAMARES/util/multiprocessing.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pyAMARES/util/multiprocessing.py b/pyAMARES/util/multiprocessing.py index b50418d..34a661c 100644 --- a/pyAMARES/util/multiprocessing.py +++ b/pyAMARES/util/multiprocessing.py @@ -80,8 +80,8 @@ def run_parallel_fitting_with_progress( method="leastsq", initialize_with_lm=False, num_workers=8, - logfilename="multiprocess.log", - loglevel=21, + logfilename="logs/parellelfitting.log", + loglevel=31, objective_func=None, notebook=True, ): @@ -103,8 +103,8 @@ def run_parallel_fitting_with_progress( initialize_with_lm (bool, optional, default False, new in 0.3.9): If True, a Levenberg-Marquardt initializer (``least_sq``) is executed internally. See ``pyAMARES.lmfit.fitAMARES`` for details. num_workers (int, optional): The number of worker processes to use in parallel processing. Defaults to 8. - logfilename (str, optional): The name of the file where the progress log is saved. Defaults to 'multiprocess.log'. - loglevel (int, optional): The logging level for the logger. Defaults to 21 - just above info. + logfilename (str, optional): The name of the file where the progress log is saved. Defaults to 'logs/parellelfitting.log'. + loglevel (int, optional): The logging level for the logger. Defaults to 31 - just above warning. objective_func (callable, optional): Custom objective function for ``pyAMARES.lmfit.fitAMARES``. If None, the default objective function will be used. Defaults to None. notebook (bool, optional): If True, uses tqdm.notebook for progress display in Jupyter notebooks. @@ -131,7 +131,11 @@ def run_parallel_fitting_with_progress( results = [] loggerID = logger.add(logfilename, level=loglevel, rotation="10 min") - logger.level("BATCH_INFO", no=21) + try: + logger.level("BATCH_INFO", no=loglevel) + except ValueError: + # This means the logger level "BATCH_INFO" was already added + pass data = [ { From 902e9607fe2d57e2a37b5fedf63eb5a38230e8f3 Mon Sep 17 00:00:00 2001 From: Sebastian Bauer Date: Thu, 11 Dec 2025 16:19:18 +0000 Subject: [PATCH 15/16] chore: change logging levels from info to debug --- pyAMARES/kernel/PriorKnowledge.py | 16 ++++++++-------- pyAMARES/kernel/fid.py | 6 +++--- pyAMARES/kernel/lmfit.py | 8 ++++---- pyAMARES/util/report.py | 8 ++++---- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/pyAMARES/kernel/PriorKnowledge.py b/pyAMARES/kernel/PriorKnowledge.py index ac6923e..0a3fcca 100644 --- a/pyAMARES/kernel/PriorKnowledge.py +++ b/pyAMARES/kernel/PriorKnowledge.py @@ -280,10 +280,10 @@ def assert_peak_format(input_str): def find_header_row(filename, comment_char="#"): """Determine the index of the first non-commented line.""" with open(filename, "r") as file: - logger.info("Checking comment lines in the prior knowledge file") + logger.debug("Checking comment lines in the prior knowledge file") for i, line in enumerate(file): if "#" in line: - logger.info(f"Comment: in line {i}: {line}") + logger.debug(f"Comment: in line {i}: {line}") with open(filename, "r") as file: for i, line in enumerate(file): processedline = line.replace('"', "").replace("'", "").strip() @@ -490,13 +490,13 @@ def initialize_FID( deadtime = float(deadtime) dwelltime = 1.0 / sw if truncate_initial_points > 0: - logger.info( + logger.debug( f"Truncating {truncate_initial_points} points from the beginning of the FID signal" ) deadtime_old = deadtime * 1.0 deadtime = deadtime + truncate_initial_points * dwelltime fid = fid[truncate_initial_points:] - logger.info( + logger.debug( f"The deadtime is changing from {deadtime} seconds to {deadtime_old} seconds" ) fidpt = len(fid) @@ -515,7 +515,7 @@ def initialize_FID( # This must be done before the shifting FID for carrier. fid = np.conj(fid) if carrier != 0: - logger.info(f"Shift FID so that center frequency is at {carrier} ppm!") + logger.debug(f"Shift FID so that center frequency is at {carrier} ppm!") fid = fid * np.exp(1j * 2 * np.pi * carrier * MHz * opts.timeaxis) # ppm = ppm + carrier # Hz = Hz + carrier / np.abs(MHz) @@ -576,7 +576,7 @@ def initialize_FID( timeaxis=opts.timeaxis, params=opts.initialParams, fid=True ) if ppm_offset != 0: - logger.info(f"Shifting the ppm by ppm_offset={ppm_offset:.2f} ppm") + logger.debug(f"Shifting the ppm by ppm_offset={ppm_offset:.2f} ppm") for p in opts.initialParams: if p.startswith("freq"): hz_offset = opts.ppm_offset * opts.MHz @@ -619,12 +619,12 @@ def initialize_FID( plt.xlabel("ppm") plt.show() if priorknowledgefile is not None: - logger.info(f"Printing the Prior Knowledge File {priorknowledgefile}") + logger.debug(f"Printing the Prior Knowledge File {priorknowledgefile}") try: from IPython.display import display display(opts.PK_table) # display table except ImportError: - logger.info(opts.PK_table) + logger.debug(opts.PK_table) return opts diff --git a/pyAMARES/kernel/fid.py b/pyAMARES/kernel/fid.py index 383d040..f9dae68 100644 --- a/pyAMARES/kernel/fid.py +++ b/pyAMARES/kernel/fid.py @@ -313,9 +313,9 @@ def Compare_to_OXSA(inputfid, resultfid): dataNormSq = np.linalg.norm(inputfid - np.mean(inputfid)) ** 2 resNormSq = np.sum(np.abs((resultfid - inputfid)) ** 2) relativeNorm = resNormSq / dataNormSq - logger.info(f"Norm of residual = {resNormSq:.3f}") - logger.info(f"Norm of the data = {dataNormSq:.3f}") - logger.info(f"resNormSq / dataNormSq = {relativeNorm:.3f}") + logger.debug(f"Norm of residual = {resNormSq:.3f}") + logger.debug(f"Norm of the data = {dataNormSq:.3f}") + logger.debug(f"resNormSq / dataNormSq = {relativeNorm:.3f}") return resNormSq, relativeNorm diff --git a/pyAMARES/kernel/lmfit.py b/pyAMARES/kernel/lmfit.py index ccfac44..13c6b83 100644 --- a/pyAMARES/kernel/lmfit.py +++ b/pyAMARES/kernel/lmfit.py @@ -29,7 +29,7 @@ def check_removed_expr(df): Raises: UserWarning: If an 'expr' is found to be dependent on a removed parameter. """ - logger.info( + logger.debug( "Check if the expr for all parameters is restricted to a parameter that has already been filtered out." ) result_df = df.copy() @@ -69,7 +69,7 @@ def filter_param_by_ppm(allpara, fit_ppm, MHz, delta=100): DataFrame: DataFrame filtered based on the specified criteria. """ fit_Hz = np.array(fit_ppm) * MHz - logger.info(f"fit_Hz={fit_Hz}") + logger.debug(f"fit_Hz={fit_Hz}") tofilter_pd = parameters_to_dataframe(allpara) chemshift_pd = tofilter_pd[tofilter_pd["name"].str.startswith("freq")] @@ -266,7 +266,7 @@ def save_parameter_to_csv(params, filename="params.csv"): This function converts the ``params`` object to a DataFrame before saving it as a CSV file. """ df = parameters_to_dataframe(params) - logger.info(f"Saving parameter file to {filename}") + logger.debug(f"Saving parameter file to {filename}") df.to_csv(filename) @@ -566,4 +566,4 @@ def print_lmfit_fitting_results(result): msg_string = "\n ".join(msg) - logger.info(msg_string) + logger.debug(msg_string) diff --git a/pyAMARES/util/report.py b/pyAMARES/util/report.py index 8fbe44a..0e34179 100644 --- a/pyAMARES/util/report.py +++ b/pyAMARES/util/report.py @@ -255,8 +255,8 @@ def report_amares(outparams, fid_parameters, verbose=False): result.loc[result["g_CRLB(%)"] == 0.0, "g_CRLB(%)"] = np.nan zero_ind = remove_zero_padding(fid_parameters.fid) if zero_ind > 0: - logger.info(f"It seems that zeros are padded after {zero_ind}") - logger.info("Remove padded zeros from residual estimation!") + logger.debug(f"It seems that zeros are padded after {zero_ind}") + logger.debug("Remove padded zeros from residual estimation!") fid_parameters.fid_padding_removed = fid_parameters.fid[:zero_ind] std_noise = np.std( fid_parameters.fid_padding_removed[ @@ -293,7 +293,7 @@ def report_amares(outparams, fid_parameters, verbose=False): ) # reorder to the peaklist from the pk, not the local peaklist # fid_parameters.peaklist = peaklist else: - logger.info("No peaklist, probably it is from an HSVD initialized object") + logger.debug("No peaklist, probably it is from an HSVD initialized object") fid_parameters.result_multiplets = result # Keep the multiplets # Sum multiplets if needed if contains_non_numeric_strings(result): # assigned peaks in the index @@ -339,7 +339,7 @@ def report_amares(outparams, fid_parameters, verbose=False): if hasattr(fid_parameters, "result_sum"): fid_parameters.metabolites = fid_parameters.result_sum.index.to_list() else: - logger.info("There is no result_sum generated, probably there is only 1 peak") + logger.debug("There is no result_sum generated, probably there is only 1 peak") fid_parameters.styled_df = styled_df fid_parameters.simple_df = simple_df return styled_df From d2cc11441ee2be49e31b6b4d751a20af3221f0ed Mon Sep 17 00:00:00 2001 From: Sebastian Bauer Date: Tue, 6 Jan 2026 08:12:47 +0000 Subject: [PATCH 16/16] chore: remove old logging file --- pyAMARES/libs/logger.py | 128 ---------------------------------------- 1 file changed, 128 deletions(-) delete mode 100644 pyAMARES/libs/logger.py diff --git a/pyAMARES/libs/logger.py b/pyAMARES/libs/logger.py deleted file mode 100644 index 9e35eb5..0000000 --- a/pyAMARES/libs/logger.py +++ /dev/null @@ -1,128 +0,0 @@ -import logging -import sys - -LOG_MODES = { - "debug": logging.DEBUG, - "info": logging.INFO, - "warning": logging.WARNING, - "error": logging.ERROR, - "critical": logging.CRITICAL, -} - -# GLOBAL VARIABLE: LOG_STYLE -# The output mode for the log messages. Options: -# - "plain": Uses `logging.Formatter` and logs to stdout. -# Always displays plain text messages in all environments. -# - "stderr": Uses `logging.Formatter` and logs to stderr. -# This is the normal behaviour of the python logging module, but -# produces red output in Jupyter notebooks. -# -# Accessed by get_logger: -LOG_STYLE = "plain" - -# GLOBAL VARIABLE: DEFAULT_LOG_LEVEL -# The default log level for all loggers. Options: -# - "debug", "info", "warning", "error", "critical" -# -# Accessed by get_logger and set_log_level: -DEFAULT_LOG_LEVEL = "info" - - -def get_logger( - name: str, format_string: str = "[AMARES | {levelname}] {message}" -) -> logging.Logger: - """ - Get or create a logger with the specified name and format. - - If the logger has no existing handlers, it initializes one based on the provided log style. - The log level defaults to `DEFAULT_LOG_LEVEL`. To change it globally, use `set_log_level()`. - - Parameters - ---------- - name : str - The name of the logger. - format_string : str, optional - The format string for log messages (default: "[AMARES | {levelname}] {message}"). - - Returns - ------- - logging.Logger - A configured logger instance. - - Raises - ------ - ValueError - If an invalid `LOG_STYLE` is provided. - - Examples - -------- - >>> logger = get_logger("example_logger") - >>> logger.debug("This is a debug message.") - [DEBUG] example_logger: This is a debug message. - """ - global LOG_STYLE - - logger = logging.getLogger(name) - logger.setLevel(LOG_MODES.get(DEFAULT_LOG_LEVEL, logging.ERROR)) - - if not logger.hasHandlers(): - if LOG_STYLE == "plain": - formatter = logging.Formatter(format_string, style="{") - handler = logging.StreamHandler(sys.stdout) - elif LOG_STYLE == "stderr": - formatter = logging.Formatter(format_string, style="{") - handler = logging.StreamHandler() - else: - raise ValueError( - f"Invalid mode: '{LOG_STYLE}'. Choose from 'plain' or 'stderr'." - ) - - handler.setFormatter(formatter) - logger.addHandler(handler) - - return logger - - -def set_log_level(level: str = DEFAULT_LOG_LEVEL, verbose: bool = True): - """ - Set the global logging level for all loggers. - - This function sets the logging level across the entire application using - the basicConfig method of the logging module. It also ensures that all - existing loggers adhere to this global level. - - Parameters - ---------- - level : str - The logging level to set globally. Acceptable values are 'critical', - 'error', 'warning', 'info', 'debug', and 'notset'. Default is the - `DEFAULT_LOG_LEVEL`. - - verbose : bool - Print an overview of the log levels and highlight the selected level. - - Examples - -------- - >>> set_log_level('info') - >>> logger = logging.getLogger('example_logger') - >>> logger.debug('This debug message will not show.') - >>> logger.info('This info message will show.') - [INFO] example_logger: This info message will show. - """ - if level.lower() not in LOG_MODES.keys(): - raise AssertionError( - f"'{level}' is not a valid logging level. " - + f"Choose from {list(LOG_MODES.keys())}." - ) - - numeric_level = LOG_MODES.get(level.lower(), logging.ERROR) - - if verbose: - for lname in LOG_MODES.keys(): - arrow = "-->" if lname == level.lower() else " " - print(f"{arrow} {lname.upper():<10}") - - for logger_name in logging.root.manager.loggerDict: - if logger_name.startswith("pyAMARES"): - logger = logging.getLogger(logger_name) - logger.setLevel(numeric_level)