From 8571768462749ceae43e8b0e4a5052b632934504 Mon Sep 17 00:00:00 2001 From: Stephen Nneji Date: Mon, 18 May 2026 09:36:35 +0100 Subject: [PATCH 1/3] Check that fitted parameters have reasonable range --- ratapi/inputs.py | 63 +++++++++++++++++++++++--------------------- ratapi/run.py | 4 ++- tests/test_inputs.py | 13 +++++++++ 3 files changed, 49 insertions(+), 31 deletions(-) diff --git a/ratapi/inputs.py b/ratapi/inputs.py index bdfaddad..736a6cb1 100644 --- a/ratapi/inputs.py +++ b/ratapi/inputs.py @@ -3,6 +3,7 @@ import importlib import os import pathlib +import warnings from collections.abc import Callable import numpy as np @@ -10,7 +11,7 @@ import ratapi import ratapi.wrappers from ratapi.rat_core import Checks, Control, NameStore, ProblemDefinition -from ratapi.utils.enums import Calculations, Languages, LayerModels, TypeOptions +from ratapi.utils.enums import Calculations, Languages, LayerModels, Procedures, TypeOptions parameter_field = { "parameters": "params", @@ -137,13 +138,13 @@ def make_input(project: ratapi.Project, controls: ratapi.Controls) -> tuple[Prob The controls object used in the compiled RAT code. """ - problem = make_problem(project) + problem = make_problem(project, controls.procedure != Procedures.Calculate) cpp_controls = make_controls(controls) return problem, cpp_controls -def make_problem(project: ratapi.Project) -> ProblemDefinition: +def make_problem(project: ratapi.Project, validate_range: bool = False) -> ProblemDefinition: """Construct the problem input required for the compiled RAT code. Parameters @@ -351,26 +352,35 @@ def make_problem(project: ratapi.Project) -> ProblemDefinition: problem.domainContrastLayers = [ domain_contrast_model if domain_contrast_model else [] for domain_contrast_model in domain_contrast_models ] - problem.fitParams = [ - param.value - for class_list in ratapi.project.parameter_class_lists - for param in getattr(project, class_list) - if param.fit - ] - problem.fitLimits = [ - [param.min, param.max] - for class_list in ratapi.project.parameter_class_lists - for param in getattr(project, class_list) - if param.fit - ] - problem.priorNames = [ - param.name for class_list in ratapi.project.parameter_class_lists for param in getattr(project, class_list) - ] - problem.priorValues = [ - [prior_id[param.prior_type], param.mu, param.sigma] - for class_list in ratapi.project.parameter_class_lists - for param in getattr(project, class_list) - ] + + fit_params = [] + fit_limits = [] + prior_names = [] + prior_values = [] + problem.checks = Checks() + for class_list in ratapi.project.parameter_class_lists: + field = parameter_field[class_list] + check_list = [] + for param in getattr(project, class_list): + prior_names.append(param.name) + prior_values.append([prior_id[param.prior_type], param.mu, param.sigma]) + check_list.append(int(param.fit)) + if param.fit: + if validate_range and (param.max - param.min) < 1e-10: + warnings.warn( + f'{class_list.replace("_", " ").title()} "{param.name}" was removed from the ' + "fit because its range is too small (< 1e-10).", + stacklevel=2, + ) + check_list[-1] = 0 + else: + fit_params.append(param.value) + fit_limits.append([param.min, param.max]) + setattr(problem.checks, field, check_list) + problem.fitParams = fit_params + problem.fitLimits = fit_limits + problem.priorNames = prior_names + problem.priorValues = prior_values # Names problem.names = NameStore() @@ -378,13 +388,6 @@ def make_problem(project: ratapi.Project) -> ProblemDefinition: setattr(problem.names, parameter_field[class_list], [param.name for param in getattr(project, class_list)]) problem.names.contrasts = [contrast.name for contrast in project.contrasts] - # Checks - problem.checks = Checks() - for class_list in ratapi.project.parameter_class_lists: - setattr( - problem.checks, parameter_field[class_list], [int(element.fit) for element in getattr(project, class_list)] - ) - check_indices(problem) return problem diff --git a/ratapi/run.py b/ratapi/run.py index d973d068..9a99224b 100644 --- a/ratapi/run.py +++ b/ratapi/run.py @@ -130,7 +130,9 @@ def run(project, controls): # Update parameter values in project for class_list in ratapi.project.parameter_class_lists: for index, value in enumerate(getattr(problem_definition, parameter_field[class_list])): - getattr(project, class_list)[index].value = value + param = getattr(project, class_list)[index] + param.fit = bool(getattr(problem_definition.checks, parameter_field[class_list])[index]) + param.value = value controls.delete_IPC() diff --git a/tests/test_inputs.py b/tests/test_inputs.py index 242d7480..23dc2ad9 100644 --- a/tests/test_inputs.py +++ b/tests/test_inputs.py @@ -483,6 +483,19 @@ def test_make_problem(test_project, test_problem, request) -> None: check_problem_equal(problem, test_problem) +def test_make_problem_validate_range(request) -> None: + """The problem should not contain fitted parameters with small range.""" + test_project = request.getfixturevalue("standard_layers_project") + + test_project.scalefactors.set_fields(0, min=10, value=10, max=10, fit=True) + problem = make_problem(test_project) + assert problem.checks.scalefactors[0] == 1 + + with pytest.warns(UserWarning, match="was removed from the fit because its range is too small \(< 1e-10\)"): + problem = make_problem(test_project, True) + assert problem.checks.scalefactors[0] == 0 + + @pytest.mark.parametrize("test_problem", ["standard_layers_problem", "custom_xy_problem", "domains_problem"]) class TestCheckIndices: """Tests for check_indices over a set of three test problems.""" From 5f5bde0af422bfa2aed559a511d61ab8fcd454fd Mon Sep 17 00:00:00 2001 From: Stephen Nneji Date: Fri, 7 Aug 2026 16:30:30 +0100 Subject: [PATCH 2/3] use value * 1e-6 --- ratapi/inputs.py | 5 +++-- tests/test_inputs.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ratapi/inputs.py b/ratapi/inputs.py index 736a6cb1..bdd96d38 100644 --- a/ratapi/inputs.py +++ b/ratapi/inputs.py @@ -366,10 +366,11 @@ def make_problem(project: ratapi.Project, validate_range: bool = False) -> Probl prior_values.append([prior_id[param.prior_type], param.mu, param.sigma]) check_list.append(int(param.fit)) if param.fit: - if validate_range and (param.max - param.min) < 1e-10: + min_range = abs(param.value) * 1e-6 if param.value == 0 else 1e-6 + if validate_range and (param.max - param.min) < min_range: warnings.warn( f'{class_list.replace("_", " ").title()} "{param.name}" was removed from the ' - "fit because its range is too small (< 1e-10).", + f"fit because its range is too small (< {min_range:g}).", stacklevel=2, ) check_list[-1] = 0 diff --git a/tests/test_inputs.py b/tests/test_inputs.py index 23dc2ad9..00a4df7f 100644 --- a/tests/test_inputs.py +++ b/tests/test_inputs.py @@ -491,7 +491,7 @@ def test_make_problem_validate_range(request) -> None: problem = make_problem(test_project) assert problem.checks.scalefactors[0] == 1 - with pytest.warns(UserWarning, match="was removed from the fit because its range is too small \(< 1e-10\)"): + with pytest.warns(UserWarning, match="was removed from the fit because its range is too small \(< 1e-06\)"): problem = make_problem(test_project, True) assert problem.checks.scalefactors[0] == 0 From 3372d28844c455945b3153620dc23c93e17c44a3 Mon Sep 17 00:00:00 2001 From: Stephen Nneji Date: Fri, 7 Aug 2026 17:03:01 +0100 Subject: [PATCH 3/3] Update standard layer example --- .../DSPC_standard_layers.ipynb | 6 +++--- .../DSPC_standard_layers.py | 6 +++--- tests/test_data/R1DSPCBilayer.mat | Bin 5538 -> 5520 bytes 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ratapi/examples/normal_reflectivity/DSPC_standard_layers.ipynb b/ratapi/examples/normal_reflectivity/DSPC_standard_layers.ipynb index 3110cfe5..060df721 100644 --- a/ratapi/examples/normal_reflectivity/DSPC_standard_layers.ipynb +++ b/ratapi/examples/normal_reflectivity/DSPC_standard_layers.ipynb @@ -85,7 +85,7 @@ " Parameter(name=\"SAM Heads Hydration\", min=10.0, value=45.45, max=50.0, fit=True, prior_type=\"gaussian\", mu=30.0, sigma=3.0),\n", " #\n", " Parameter(name=\"CW Thickness\", min=10.0, value=17.12, max=28.0, fit=True, prior_type=\"uniform\"),\n", - " Parameter(name=\"CW SLD\", min=0.0, value=0.0, max=1e-09, fit=False, prior_type=\"uniform\"),\n", + " Parameter(name=\"CW SLD\", min=0.0, value=0.0, max=0.0, fit=False, prior_type=\"uniform\"),\n", " Parameter(name=\"CW Hydration\", min=99.9, value=100.0, max=100.0, fit=False, prior_type=\"uniform\"),\n", " #\n", " Parameter(name=\"Bilayer Heads Thickness\", min=7.0, value=10.7, max=17.0, fit=True, prior_type=\"gaussian\", mu=10.0, sigma=2.0),\n", @@ -175,8 +175,8 @@ "outputs": [], "source": [ "del problem.scalefactors[0]\n", - "problem.scalefactors.append(name=\"Scalefactor 1\", min=0.05, value=0.10, max=0.2, fit=False)\n", - "problem.scalefactors.append(name=\"Scalefactor 2\", min=0.05, value=0.15, max=0.2, fit=False)\n", + "problem.scalefactors.append(name=\"Scalefactor 1\", min=0.05, value=0.10, max=0.2, fit=True)\n", + "problem.scalefactors.append(name=\"Scalefactor 2\", min=0.05, value=0.15, max=0.2, fit=True)\n", "\n", "# Now deal with the backgrounds\n", "del problem.backgrounds[0]\n", diff --git a/ratapi/examples/normal_reflectivity/DSPC_standard_layers.py b/ratapi/examples/normal_reflectivity/DSPC_standard_layers.py index 6962f036..260de48d 100644 --- a/ratapi/examples/normal_reflectivity/DSPC_standard_layers.py +++ b/ratapi/examples/normal_reflectivity/DSPC_standard_layers.py @@ -22,7 +22,7 @@ def DSPC_standard_layers(): problem.parameters.append(name="SAM Tails Hydration", min=1.0, value=5.252, max=50.0, fit=True) problem.parameters.append(name="SAM Roughness", min=1.0, value=5.64, max=15.0, fit=True) problem.parameters.append(name="CW Thickness", min=10.0, value=17.12, max=28.0, fit=True) - problem.parameters.append(name="CW SLD", min=0.0, value=0.0, max=1e-09, fit=False) + problem.parameters.append(name="CW SLD", min=0.0, value=0.0, max=0.0, fit=False) problem.parameters.append( name="SAM Heads Thickness", @@ -131,8 +131,8 @@ def DSPC_standard_layers(): # Set the scalefactors - use one for each contrast del problem.scalefactors[0] - problem.scalefactors.append(name="Scalefactor 1", min=0.05, value=0.10, max=0.2, fit=False) - problem.scalefactors.append(name="Scalefactor 2", min=0.05, value=0.15, max=0.2, fit=False) + problem.scalefactors.append(name="Scalefactor 1", min=0.05, value=0.10, max=0.2, fit=True) + problem.scalefactors.append(name="Scalefactor 2", min=0.05, value=0.15, max=0.2, fit=True) # Now deal with the backgrounds del problem.backgrounds[0] diff --git a/tests/test_data/R1DSPCBilayer.mat b/tests/test_data/R1DSPCBilayer.mat index 596eb814a590f6ec92fefe094b2f91a5bdcc8e4b..87018fda6f9bace5b3004bc61a7c48dfabad085a 100644 GIT binary patch delta 5186 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 delta 5188 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