diff --git a/cpp/RAT b/cpp/RAT index a5d4d80b..e1e879c6 160000 --- a/cpp/RAT +++ b/cpp/RAT @@ -1 +1 @@ -Subproject commit a5d4d80ba63185846db678c3ebd9a85bdd82921d +Subproject commit e1e879c6eda6f1dee7a82b97dbbabd85d091017e diff --git a/pyproject.toml b/pyproject.toml index ab6724ae..f2f9201c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ build-backend = 'setuptools.build_meta' [project] name = "ratapi" -version = "0.0.0.dev14" +version = "0.0.0.dev15" description = "Python extension for the Reflectivity Analysis Toolbox (RAT)" readme = "README.md" requires-python = ">=3.10" @@ -28,7 +28,7 @@ Repository = "https://github.com/RascalSoftware/python-RAT" [project.optional-dependencies] dev = [ - "pytest>=7.4.0", + "pytest>=7.4.0,!=9.1.0", "pytest-cov>=4.1.0", "ruff>=0.4.10" ] diff --git a/ratapi/utils/convert.py b/ratapi/utils/convert.py index b957789c..7b3a44f7 100644 --- a/ratapi/utils/convert.py +++ b/ratapi/utils/convert.py @@ -5,7 +5,7 @@ from os import PathLike from pathlib import Path -from numpy import array, empty +from numpy import array, empty, ndarray from scipy.io.matlab import MatlabOpaque, loadmat from ratapi import Project, wrappers @@ -227,6 +227,12 @@ def fix_invalid_constraints(name: str, constrs: tuple[float, float], value: floa if isinstance(mat_project["resolNames"], str): mat_project["resolNames"] = [mat_project["resolNames"]] + if isinstance(mat_project["contrastNames"], (ndarray, list)) and len( + dict.fromkeys(mat_project["contrastNames"]) + ) != len(mat_project["contrastNames"]): + # contrast names are not unique so create unique ones + mat_project["contrastNames"] = [f"Contrast {i + 1}" for i in range(len(mat_project["contrastNames"]))] + contrasts = ClassList( [ Contrast( diff --git a/ratapi/utils/plotting.py b/ratapi/utils/plotting.py index 6f9debb7..509c7a4f 100644 --- a/ratapi/utils/plotting.py +++ b/ratapi/utils/plotting.py @@ -763,7 +763,7 @@ def plot_one_hist( results: ratapi.outputs.BayesResults, param: int | str, smooth: bool = True, - sigma: float | None = None, + window_size: int = 8, estimated_density: Literal["normal", "lognor", "kernel", None] = None, axes: Axes | None = None, block: bool = False, @@ -783,9 +783,9 @@ def plot_one_hist( smooth : bool, default True Whether to apply Gaussian smoothing to the histogram. Defaults to True. - sigma: float or None, default None - If given, is used as the sigma-parameter for the Gaussian smoothing. - If None, the default (1/3rd of parameter chain standard deviation) is used. + window_size : int, default 8 + The width of the smoothing window centered around the element being averaged. + The window moves down the length of the data, computing an average over the elements within each window. estimated_density : 'normal', 'lognor', 'kernel' or None, default None If None (default), ignore. Else, add an estimated density of the given form on top of the histogram by the following estimations: @@ -826,9 +826,7 @@ def plot_one_hist( sd_y = np.std(parameter_chain) if smooth: - if sigma is None: - sigma = sd_y / 2 - counts = gaussian_filter1d(counts, sigma) + counts = moving_average(counts, window_size=window_size) axes.hist( bins[:-1], bins, @@ -1134,7 +1132,6 @@ def validate_dens_type(dens_type: str | None, param: str): results, i, smooth=smooth, - sigma=sigma, estimated_density=estimated_density.get(i), axes=ax, **hist_settings, @@ -1233,3 +1230,33 @@ def plot_bayes(project: ratapi.Project, results: ratapi.outputs.BayesResults): plot_corner(results) else: raise ValueError("Bayes plots are only available for the results of Bayesian analysis (NS or DREAM)") + + +def moving_average(data: np.ndarray, window_size: int = 8) -> list[float]: + """Calculate the moving average of an array with a given window size. + + This is a python equivalent to MATLABs smoothdata(A, 'movmean') + + Parameters + ---------- + data : np.ndarray + The input array to smooth + window_size : int + The window slides down the length of the vector, + computing an average over the elements within each window. + + """ + if not 0 <= window_size <= len(data): + raise ValueError( + "The moving average window size is out of range. Please change to a positive integer which " + "does not exceed the number of histogram bins." + ) + moving_averages = [] + + for i in range(len(data)): + start_window_ind = floor(float(i - window_size / 2)) if i - window_size / 2 > 0 else 0 + end_window_ind = floor(float(i + window_size / 2)) if i + window_size / 2 < len(data) else len(data) + window_average = np.sum(data[start_window_ind:end_window_ind]) / (end_window_ind + 0 - start_window_ind) + moving_averages.append(window_average) + + return moving_averages diff --git a/tests/test_convert.py b/tests/test_convert.py index fc6f4400..0cbd621b 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -69,6 +69,13 @@ def test_r1_to_project(file, project, path_type, request): assert getattr(output_project, class_list) == getattr(expected_project, class_list) +def test_r1_with_non_unique_contrast_names(): + """Test that R1 to Project class conversion returns the expected Project.""" + output_project = r1_to_project(pathlib.Path(TEST_DIR_PATH, "nonUniqueContrast.mat")) + assert output_project.contrasts[0].name == "Contrast 1" + assert output_project.contrasts[1].name == "Contrast 2" + + @pytest.mark.parametrize( "project", [ diff --git a/tests/test_data/nonUniqueContrast.mat b/tests/test_data/nonUniqueContrast.mat new file mode 100644 index 00000000..41375134 Binary files /dev/null and b/tests/test_data/nonUniqueContrast.mat differ diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 31049a28..518888fe 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -495,3 +495,61 @@ def test_extract_plot_data(data) -> None: with pytest.raises(ValueError, match=r"Parameter `shift_value` must be between 0 and 100"): RATplot._extract_plot_data(data, False, True, 100.5) + + +def test_moving_average() -> None: + """Test the moving_average function.""" + data_to_average = np.arange(0, 20) + mov_avg = RATplot.moving_average(data_to_average) + assert mov_avg == [ + 1.5, + 2.0, + 2.5, + 3.0, + 3.5, + 4.5, + 5.5, + 6.5, + 7.5, + 8.5, + 9.5, + 10.5, + 11.5, + 12.5, + 13.5, + 14.5, + 15.5, + 16.0, + 16.5, + 17.0, + ] + + mov_avg = RATplot.moving_average(data_to_average, window_size=2) + assert mov_avg == [ + 0.0, + 0.5, + 1.5, + 2.5, + 3.5, + 4.5, + 5.5, + 6.5, + 7.5, + 8.5, + 9.5, + 10.5, + 11.5, + 12.5, + 13.5, + 14.5, + 15.5, + 16.5, + 17.5, + 18.5, + ] + + with pytest.raises(ValueError): + RATplot.moving_average(data_to_average, window_size=-1) + + with pytest.raises(ValueError): + RATplot.moving_average(data_to_average, window_size=len(data_to_average) + 1)