From 58d1141d098cbc7e84bcb6f21b049ac62f355887 Mon Sep 17 00:00:00 2001 From: "Alex H. Room" <69592136+alexhroom@users.noreply.github.com> Date: Wed, 12 Feb 2025 18:06:41 +0000 Subject: [PATCH 01/20] Adds docstrings to the Results struct (#132) * docstrings for outputs and fixed plot bayes docstring * review fixes --- RATapi/outputs.py | 244 ++++++++++++++++++++++++++++++++++++++- RATapi/utils/plotting.py | 1 + 2 files changed, 244 insertions(+), 1 deletion(-) diff --git a/RATapi/outputs.py b/RATapi/outputs.py index 0f5aeb31..353459eb 100644 --- a/RATapi/outputs.py +++ b/RATapi/outputs.py @@ -62,12 +62,40 @@ def __str__(self): @dataclass class CalculationResults(RATResult): + """The goodness of fit from the Abeles calculation. + + Attributes + ---------- + chiValues : np.ndarray + The chi-squared value for each contrast. + sumChi : float + The sum of the chiValues array. + + """ + chiValues: np.ndarray sumChi: float @dataclass class ContrastParams(RATResult): + """The experimental parameters for each contrast. + + Attributes + ---------- + scalefactors : np.ndarray + The scalefactor values for each contrast. + bulkIn : np.ndarray + The bulk in values for each contrast. + bulkOut : np.ndarray + The bulk out values for each contrast. + subRoughs : np.ndarray + The substrate roughness values for each contrast. + resample : np.ndarray + An array containing whether each contrast was resampled. + + """ + scalefactors: np.ndarray bulkIn: np.ndarray bulkOut: np.ndarray @@ -77,6 +105,42 @@ class ContrastParams(RATResult): @dataclass class Results: + """The results of a RAT calculation. + + Attributes + ---------- + reflectivity : list + The reflectivity curves for each contrast, + with the same range as the data + (``data_range`` in the contrast's ``Data`` object) + simulation : list + The reflectivity curves for each contrast, + which can be a wider range to allow extrapolation + (``simulation_range`` in the contrast's ``Data`` object). + shiftedData : list + The data with scalefactors and background corrections applied. + backgrounds : list + The background for each contrast defined over the simulation range. + resolutions : list + The resolution for each contrast defined over the simulation range. + layerSlds : list + The array of layer parameter values for each contrast. + sldProfiles : list + The SLD profiles for each contrast. + resampledLayers : list + If resampling is used, the SLD for each contrast after resampling has been performed. + calculationResults : CalculationResults + The chi-squared fit results from the final calculation and fit. + contrastParams : ContrastParams + The experimental parameters for the contrasts. + fitParams : np.ndarray + The best fit value of the parameter with name ``fitNames[i]``. + fitNames : list[str] + The names of the fit parameters, where ``fitNames[i]`` is the name + of the parameter with value given in ``fitParams[i]``. + + """ + reflectivity: list simulation: list shiftedData: list @@ -99,6 +163,27 @@ def __str__(self): @dataclass class PredictionIntervals(RATResult): + """The Bayesian prediction intervals for 95% and 65% confidence. + + For ``reflectivity`` and ``sld``, each list item is an array + with five rows. The rows represent: + + - 0: the 5th percentile; + - 1: the 35th percentile; + - 2: the mean value of the interval; + - 3: the 65th percentile; + - 4: the 95th percentile. + + Attributes + ---------- + reflectivity : list + The prediction interval data for reflectivity of each contrast. + SLD : list + The prediction interval data for SLD of each contrast. + sampleChi : np.ndarray + The value of sumChi at each point of the Markov chain. + """ + reflectivity: list sld: list sampleChi: np.ndarray @@ -106,6 +191,19 @@ class PredictionIntervals(RATResult): @dataclass class ConfidenceIntervals(RATResult): + """ + The 65% and 95% confidence intervals for the best fit results. + + Attributes + ---------- + percentile95 : np.ndarray + The 95% confidence intervals for each fit parameter. + percentile65 : np.ndarray + The 65% confidence intervals for each fit parameter. + mean : np.ndarray + The mean values for each fit parameter. + """ + percentile95: np.ndarray percentile65: np.ndarray mean: np.ndarray @@ -113,6 +211,54 @@ class ConfidenceIntervals(RATResult): @dataclass class DreamParams(RATResult): + """The parameters used by the inner DREAM algorithm. + + Attributes + ---------- + nParams : float + The number of parameters used by the algorithm. + nChains : float + The number of MCMC chains used by the algorithm. + nGenerations : float + The number of DE generations calculated per iteration. + parallel : bool + Whether the algorithm should run chains in parallel. + CPU : float + The number of processor cores used for parallel chains. + jumpProbability : float + A probability range for the size of jumps when performing subspace sampling. + pUnitGamma : float + The probability that the scaling-down factor of jumps will be ignored + and a larger jump will be taken for one iteration. + nCR : float + The number of crossovers performed each iteration. + delta : float + The number of chain mutation pairs proposed each iteration. + steps : float + The number of MCMC steps to perform between conversion checks. + zeta : float + The ergodicity of the algorithm. + outlier : str + What test should be used to detect outliers. + adaptPCR : bool + Whether the crossover probability for differential evolution should be + adapted by the algorithm as it runs. + thinning : float + The thinning rate of each Markov chain (to reduce memory intensity) + epsilon : float + The cutoff threshold for Approximate Bayesian Computation (if used) + ABC : bool + Whether Approximate Bayesian Computation is used. + IO : bool + Whether the algorithm should perform IO writes of the model in parallel. + storeOutput : bool + Whether output model simulations are performed. + R : np.ndarray + An array where row ``i`` is the list of chains + with which chain ``i`` can mutate. + + """ + nParams: float nChains: float nGenerations: float @@ -136,6 +282,45 @@ class DreamParams(RATResult): @dataclass class DreamOutput(RATResult): + """The diagnostic output information from DREAM. + + Attributes + ---------- + allChains : np.ndarray + An ``nGenerations`` x ``nParams + 2`` x ``nChains`` size array, + where ``chain_k = DreamOutput.allChains[:, :, k]`` + is the data of chain ``k`` in the final iteration; + for generation i of the final iteration, ``chain_k[i, j]`` represents: + + - the sampled value of parameter ``j`` for ``j in 0:nParams``; + - the associated log-prior for those sampled values for ``j = nParams + 1``; + - the associated log-likelihood for those sampled values for ``j = nParams + 2``. + + outlierChains : np.ndarray + A two-column array where ``DreamOutput.AR[i, 1]`` is the index of a chain + and ``DreamOutput.AR[i, 0]`` is the length of that chain when it was removed + for being an outlier. + runtime : float + The runtime of the DREAM algorithm in seconds. + iteration : float + The number of iterations performed. + modelOutput : float + Unused. Will always be 0. + AR : np.ndarray + A two-column array where ``DreamOutput.AR[i, 0]`` is an iteration number + and ``DreamOutput.AR[i, 1]`` is the average acceptance rate of chain step + proposals for that iteration. + R_stat : np.ndarray + An array where ``DreamOutput.R_stat[i, 0]`` is an iteration number and + ``DreamOutput.R_stat[i, j]`` is the convergence statistic for parameter ``j`` + at that iteration (where chains are indexed 1 to ``nParams`` inclusive). + CR : np.ndarray + A four-column array where ``DreamOutput.CR[i, 0]`` is an iteration number, + ``and DreamOutput.CR[i, j]`` is the selection probability of the ``j``'th crossover + value for that iteration. + + """ + allChains: np.ndarray outlierChains: np.ndarray runtime: float @@ -148,6 +333,26 @@ class DreamOutput(RATResult): @dataclass class NestedSamplerOutput(RATResult): + """The output information from the Nested Sampler (ns). + + Attributes + ---------- + logZ : float + The natural logarithm of the evidence Z for the parameter values. + logZErr : float + The estimated uncertainty in the final value of logZ. + nestSamples : np.ndarray + ``NestedSamplerOutput.nestSamples[i, j]`` represents the values + sampled at iteration ``i``, where this value is: + + - the value sampled for parameter ``j``, for ``j`` in ``0:nParams``, + - the minimum log-likelihood for ``j = nParams + 1``. + + postSamples : np.ndarray + The posterior values at the points sampled in ``NestedSamplerOutput.nestSamples``. + + """ + logZ: float logZErr: float nestSamples: np.ndarray @@ -156,6 +361,26 @@ class NestedSamplerOutput(RATResult): @dataclass class BayesResults(Results): + """The results of a Bayesian RAT calculation. + + Attributes + ---------- + predictionIntervals : PredictionIntervals + The prediction intervals. + confidenceIntervals : ConfidenceIntervals + The 65% and 95% confidence intervals for the best fit results. + dreamParams : DreamParams + The parameters used by DREAM, if relevant. + dreamOutput : DreamOutput + The output from DREAM if DREAM was used. + nestedSamplerOutput : NestedSamplerOutput + The output from nested sampling if ns was used. + chain : np.ndarray + The MCMC chains for each parameter. + The ``i``'th column of the array contains the chain for parameter ``fitNames[i]``. + + """ + predictionIntervals: PredictionIntervals confidenceIntervals: ConfidenceIntervals dreamParams: DreamParams @@ -169,7 +394,24 @@ def make_results( output_results: RATapi.rat_core.OutputResult, bayes_results: Optional[RATapi.rat_core.BayesResults] = None, ) -> Union[Results, BayesResults]: - """Initialise a python Results or BayesResults object using the outputs from a RAT calculation.""" + """Initialise a python Results or BayesResults object using the outputs from a RAT calculation. + + Parameters + ---------- + procedure : Procedures + The procedure used by the calculation. + output_results : RATapi.rat_core.OutputResult + The C++ output results from the calculation. + bayes_results : Optional[RATapi.rat_core.BayesResults] + The optional extra C++ Bayesian output results from a Bayesian calculation. + + Returns + ------- + Results or BayesResults + A result object containing the results of the calculation, of type + Results for non-Bayesian procedures and BayesResults for Bayesian procedures. + + """ calculation_results = CalculationResults( chiValues=output_results.calculationResults.chiValues, sumChi=output_results.calculationResults.sumChi, diff --git a/RATapi/utils/plotting.py b/RATapi/utils/plotting.py index 97d5fa47..7b8c03e8 100644 --- a/RATapi/utils/plotting.py +++ b/RATapi/utils/plotting.py @@ -865,6 +865,7 @@ def plot_bayes(project: RATapi.Project, results: RATapi.outputs.BayesResults): all parameters. Parameters + ---------- project : Project An instance of the Project class results : Union[Results, BayesResults] From 148d0d96939c981384ce767da86f1b6820876965 Mon Sep 17 00:00:00 2001 From: "Alex H. Room" <69592136+alexhroom@users.noreply.github.com> Date: Tue, 18 Feb 2025 09:32:14 +0000 Subject: [PATCH 02/20] Improves `Project.save` (#127) * added envrc to gitignore * paths are now saved as relative to the project directory * save now just has one filename parameter * removed walkup as it is unstable * simplified * portable tests * review fixes --- .gitignore | 3 ++ RATapi/project.py | 69 +++++++++++++++++++++++++++++++++---------- tests/test_project.py | 40 +++++++++++++++++++++++-- 3 files changed, 94 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index 093e835f..c1b6a85d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,9 @@ __pycache__/ .idea .vscode +# direnv +.envrc + # Unit test / coverage reports htmlcov/ .coverage diff --git a/RATapi/project.py b/RATapi/project.py index 9b38f88e..da36b5ed 100644 --- a/RATapi/project.py +++ b/RATapi/project.py @@ -4,6 +4,7 @@ import copy import functools import json +import warnings from enum import Enum from pathlib import Path from textwrap import indent @@ -835,17 +836,17 @@ def classlist_script(name, classlist): + "\n)" ) - def save(self, path: Union[str, Path], filename: str = "project"): + def save(self, filepath: Union[str, Path] = "./project.json"): """Save a project to a JSON file. Parameters ---------- - path : str or Path - The path in which the project will be written. - filename : str - The name of the generated project file. + filepath : str or Path + The path to where the project file will be written. """ + filepath = Path(filepath).with_suffix(".json") + json_dict = {} for field in self.model_fields: attr = getattr(self, field) @@ -869,7 +870,7 @@ def make_custom_file_dict(item): "name": item.name, "filename": item.filename, "language": item.language, - "path": str(item.path), + "path": try_relative_to(item.path, filepath), } json_dict["custom_files"] = [make_custom_file_dict(file) for file in attr] @@ -879,8 +880,7 @@ def make_custom_file_dict(item): else: json_dict[field] = attr - file = Path(path, f"{filename.removesuffix('.json')}.json") - file.write_text(json.dumps(json_dict)) + filepath.write_text(json.dumps(json_dict)) @classmethod def load(cls, path: Union[str, Path]) -> "Project": @@ -892,15 +892,21 @@ def load(cls, path: Union[str, Path]) -> "Project": The path to the project file. """ - input = Path(path).read_text() - model_dict = json.loads(input) - for i in range(0, len(model_dict["data"])): - if model_dict["data"][i]["name"] == "Simulation": - model_dict["data"][i]["data"] = np.empty([0, 3]) - del model_dict["data"][i]["data_range"] + path = Path(path) + input_data = path.read_text() + model_dict = json.loads(input_data) + for dataset in model_dict["data"]: + if dataset["name"] == "Simulation": + dataset["data"] = np.empty([0, 3]) + del dataset["data_range"] else: - data = model_dict["data"][i]["data"] - model_dict["data"][i]["data"] = np.array(data) + data = dataset["data"] + dataset["data"] = np.array(data) + + # file paths are saved as relative to the project directory + for file in model_dict["custom_files"]: + if not Path(file["path"]).is_absolute(): + file["path"] = Path(path, file["path"]) return cls.model_validate(model_dict) @@ -943,3 +949,34 @@ def wrapped_func(*args, **kwargs): return return_value return wrapped_func + + +def try_relative_to(path: Path, relative_to: Path) -> str: + """Attempt to create a relative path and warn the user if it isn't possible. + + Parameters + ---------- + path : Path + The path to try to find a relative path for. + relative_to: Path + The path to which we find a relative path for ``path``. + + Returns + ------- + str + The relative path if successful, else the absolute path. + + """ + path = Path(path) + relative_to = Path(relative_to) + if path.is_relative_to(relative_to): + return str(path.relative_to(relative_to)) + else: + warnings.warn( + "Could not save custom file path as relative to the project directory, " + "which means that it may not work on other devices." + "If you would like to share your project, make sure your custom files " + "are in a subfolder of the project save location.", + stacklevel=2, + ) + return str(path.resolve()) diff --git a/tests/test_project.py b/tests/test_project.py index ee2136ba..eb2b2d8f 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -2,6 +2,7 @@ import copy import tempfile +import warnings from pathlib import Path from typing import Callable @@ -1556,8 +1557,43 @@ def test_save_load(project, request): original_project = request.getfixturevalue(project) with tempfile.TemporaryDirectory() as tmp: - original_project.save(tmp) - converted_project = RATapi.Project.load(Path(tmp, "project.json")) + # ignore relative path warnings + path = Path(tmp, "project.json") + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + original_project.save(path) + converted_project = RATapi.Project.load(path) + + # resolve custom files in case the original project had unresolvable relative paths + for file in original_project.custom_files: + file.path = file.path.resolve() for field in RATapi.Project.model_fields: assert getattr(converted_project, field) == getattr(original_project, field) + + +def test_relative_paths(): + """Test that ``try_relative_to`` correctly creates relative paths to subfolders.""" + + with tempfile.TemporaryDirectory() as tmp: + data_path = Path(tmp, "data/myfile.dat") + + assert Path(RATapi.project.try_relative_to(data_path, tmp)) == Path("data/myfile.dat") + + +def test_relative_paths_warning(): + """Test that we get a warning for trying to walk up paths.""" + + data_path = "/tmp/project/data/mydata.dat" + relative_path = "/tmp/project/project_path/myproj.dat" + + with pytest.warns( + match="Could not save custom file path as relative to the project directory, " + "which means that it may not work on other devices." + "If you would like to share your project, make sure your custom files " + "are in a subfolder of the project save location.", + ): + assert ( + Path(RATapi.project.try_relative_to(data_path, relative_path)) + == Path("/tmp/project/data/mydata.dat").resolve() + ) From e148415d5cd78a7c1cc247406b977e345d3fe1fe Mon Sep 17 00:00:00 2001 From: Paul Sharp <44529197+DrPaulSharp@users.noreply.github.com> Date: Fri, 21 Feb 2025 11:47:31 +0000 Subject: [PATCH 03/20] Removes priors struct, adding relevant fields to the project (#135) * Removes priors struct, adding relevant fields to the project * Addresses review comments --- .../background_function.py | 2 +- RATapi/inputs.py | 85 +++--- RATapi/run.py | 3 +- cpp/RAT | 2 +- cpp/rat.cpp | 268 +++++++----------- tests/test_inputs.py | 190 ++++++------- 6 files changed, 219 insertions(+), 331 deletions(-) diff --git a/RATapi/examples/normal_reflectivity/background_function.py b/RATapi/examples/normal_reflectivity/background_function.py index 6728d6f8..68993814 100644 --- a/RATapi/examples/normal_reflectivity/background_function.py +++ b/RATapi/examples/normal_reflectivity/background_function.py @@ -1,7 +1,7 @@ import numpy as np -def backgroundFunction(xdata, params): +def background_function(xdata, params): # Split up the params array Ao = params[0] k = params[1] diff --git a/RATapi/inputs.py b/RATapi/inputs.py index e747e3ab..812b9ac4 100644 --- a/RATapi/inputs.py +++ b/RATapi/inputs.py @@ -10,9 +10,19 @@ import RATapi import RATapi.controls import RATapi.wrappers -from RATapi.rat_core import Checks, Control, Limits, NameStore, Priors, ProblemDefinition +from RATapi.rat_core import Checks, Control, Limits, NameStore, ProblemDefinition from RATapi.utils.enums import Calculations, Languages, LayerModels, TypeOptions +parameter_field = { + "parameters": "params", + "bulk_in": "bulkIns", + "bulk_out": "bulkOuts", + "scalefactors": "scalefactors", + "domain_ratios": "domainRatios", + "background_parameters": "backgroundParams", + "resolution_parameters": "resolutionParams", +} + def get_python_handle(file_name: str, function_name: str, path: Union[str, pathlib.Path] = "") -> Callable: """Get the function handle from a function defined in a python module located anywhere within the filesystem. @@ -94,7 +104,7 @@ def __len__(self): return len(self.files) -def make_input(project: RATapi.Project, controls: RATapi.Controls) -> tuple[ProblemDefinition, Limits, Priors, Control]: +def make_input(project: RATapi.Project, controls: RATapi.Controls) -> tuple[ProblemDefinition, Limits, Control]: """Constructs the inputs required for the compiled RAT code using the data defined in the input project and controls. @@ -111,65 +121,32 @@ def make_input(project: RATapi.Project, controls: RATapi.Controls) -> tuple[Prob The problem input used in the compiled RAT code. limits : RAT.rat_core.Limits A list of min/max values for each parameter defined in the project. - priors : RAT.rat_core.Priors - The priors defined for each parameter in the project. cpp_controls : RAT.rat_core.Control The controls object used in the compiled RAT code. """ - parameter_field = { - "parameters": "params", - "bulk_in": "bulkIns", - "bulk_out": "bulkOuts", - "scalefactors": "scalefactors", - "domain_ratios": "domainRatios", - "background_parameters": "backgroundParams", - "resolution_parameters": "resolutionParams", - } - - prior_id = {"uniform": 1, "gaussian": 2, "jeffreys": 3} - - checks = Checks() limits = Limits() - priors = Priors() for class_list in RATapi.project.parameter_class_lists: - setattr(checks, parameter_field[class_list], [int(element.fit) for element in getattr(project, class_list)]) setattr( limits, parameter_field[class_list], [[element.min, element.max] for element in getattr(project, class_list)], ) - setattr( - priors, - parameter_field[class_list], - [[element.name, element.prior_type, element.mu, element.sigma] for element in getattr(project, class_list)], - ) - # Use dummy values for qzshifts - checks.qzshifts = [] + # Use dummy value for qzshifts limits.qzshifts = [] - priors.qzshifts = [] - - priors.priorNames = [ - param.name for class_list in RATapi.project.parameter_class_lists for param in getattr(project, class_list) - ] - priors.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) - ] if project.model == LayerModels.CustomXY: controls.calcSldDuringFit = True - problem = make_problem(project, checks) + problem = make_problem(project) cpp_controls = make_controls(controls) - return problem, limits, priors, cpp_controls + return problem, limits, cpp_controls -def make_problem(project: RATapi.Project, checks: Checks) -> ProblemDefinition: +def make_problem(project: RATapi.Project) -> ProblemDefinition: """Constructs the problem input required for the compiled RAT code. Parameters @@ -184,6 +161,7 @@ def make_problem(project: RATapi.Project, checks: Checks) -> ProblemDefinition: """ hydrate_id = {"bulk in": 1, "bulk out": 2} + prior_id = {"uniform": 1, "gaussian": 2, "jeffreys": 3} # Set contrast parameters according to model type if project.model == LayerModels.StandardLayers: @@ -384,21 +362,32 @@ def make_problem(project: RATapi.Project, checks: Checks) -> ProblemDefinition: for param in getattr(project, class_list) if not 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) + ] # Names problem.names = NameStore() - problem.names.params = [param.name for param in project.parameters] - problem.names.backgroundParams = [param.name for param in project.background_parameters] - problem.names.scalefactors = [param.name for param in project.scalefactors] - problem.names.qzshifts = [] # Placeholder for qzshifts - problem.names.bulkIns = [param.name for param in project.bulk_in] - problem.names.bulkOuts = [param.name for param in project.bulk_out] - problem.names.resolutionParams = [param.name for param in project.resolution_parameters] - problem.names.domainRatios = [param.name for param in project.domain_ratios] + for class_list in RATapi.project.parameter_class_lists: + 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 + 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)] + ) + + # Use dummy values for qz shifts + problem.names.qzshifts = [] + problem.checks.qzshifts = [] + check_indices(problem) return problem diff --git a/RATapi/run.py b/RATapi/run.py index d60b76e0..6b158255 100644 --- a/RATapi/run.py +++ b/RATapi/run.py @@ -104,7 +104,7 @@ def run(project, controls): horizontal_line = "\u2500" * 107 + "\n" display_on = controls.display != Display.Off - problem_definition, limits, priors, cpp_controls = make_input(project, controls) + problem_definition, limits, cpp_controls = make_input(project, controls) if display_on: print("Starting RAT " + horizontal_line) @@ -115,7 +115,6 @@ def run(project, controls): problem_definition, limits, cpp_controls, - priors, ) end = time.time() diff --git a/cpp/RAT b/cpp/RAT index e14ea44c..b46dd2d1 160000 --- a/cpp/RAT +++ b/cpp/RAT @@ -1 +1 @@ -Subproject commit e14ea44c57b376e9f73936320b0d460e8bec3779 +Subproject commit b46dd2d1e5c76669ee980f88144314c851aee3e4 diff --git a/cpp/rat.cpp b/cpp/rat.cpp index 759e7d48..f159927f 100644 --- a/cpp/rat.cpp +++ b/cpp/rat.cpp @@ -395,20 +395,6 @@ struct BayesResults py::array_t chain; }; -struct Priors -{ - py::list params; - py::list backgroundParams; - py::list scalefactors; - py::list qzshifts; - py::list bulkIns; - py::list bulkOuts; - py::list resolutionParams; - py::list domainRatios; - py::list priorNames; - py::array_t priorValues; -}; - struct Checks { py::array_t params; py::array_t backgroundParams; @@ -515,6 +501,8 @@ struct ProblemDefinition { py::array_t otherParams; py::array_t fitLimits; py::array_t otherLimits; + py::list priorNames; + py::array_t priorValues; NameStore names; Checks checks {}; }; @@ -665,47 +653,6 @@ coder::array pyArrayToRatArray2d(py::array_t value) return result; } -coder::array pyListToUnboundedCell0(py::list values) -{ - coder::array result; - result.set_size(values.size()); - int32_T idx {0}; - for (py::handle list: values) - { - py::list value = py::cast(list); - if (py::len(list) != 4 || !py::isinstance(value[0]) || !py::isinstance(value[1]) || - !py::isinstance(value[2]) || !py::isinstance(value[3])) - throw std::runtime_error("Expects a 2D list where each row must contain 4 elements. " - "Columns 1 and 2 must be strings and Columns 3 and 4 must be numeric arrays"); - - stringToRatBoundedArray(value[0].cast(), result[idx].f1.data, result[idx].f1.size); - stringToRatBoundedArray(value[1].cast(), result[idx].f2.data, result[idx].f2.size); - result[idx].f3 = value[2].cast(); - result[idx].f4 = value[3].cast(); - idx++; - } - - return result; -} - -coder::array pyListToUnboundedCell1(py::list values) -{ - coder::array result; - result.set_size(values.size()); - int32_T idx {0}; - for (py::handle list: values) - { - if (py::isinstance(list)) { - std::string value = py::cast(list); - stringToRatCharArray(value, result[idx].f1); - idx++; - } - else - throw std::runtime_error("Expects a 1D list of strings"); - } - - return result; -} coder::array pyListToRatCellWrap1(py::list values) { coder::array result; @@ -800,7 +747,26 @@ coder::array pyListToRatCellWrap6(py::list values) return result; } -coder::array pyListToRatCellWrap0(py::list values) +coder::array pyListToRatCellWrap01d(py::list values) +{ + coder::array result; + result.set_size(values.size()); + int32_T idx {0}; + for (py::handle array: values) + { + if (py::isinstance(array)) { + std::string name = py::cast(array); + stringToRatBoundedArray(name, result[idx].f1.data, result[idx].f1.size); + idx++; + } + else + throw std::runtime_error("Expects a 1D list of strings"); + } + + return result; +} + +coder::array pyListToRatCellWrap02d(py::list values) { coder::array result; result.set_size(1, values.size()); @@ -839,15 +805,15 @@ coder::array py_function_array_to_rat_cell_wrap_0(py::obje RAT::struct1_T createStruct1(const NameStore& names) { RAT::struct1_T names_struct; - names_struct.params = customCaller("NameStore.params", pyListToRatCellWrap0, names.params); - names_struct.backgroundParams = customCaller("NameStore.backgroundParams", pyListToRatCellWrap0, names.backgroundParams); - names_struct.scalefactors = customCaller("NameStore.scalefactors", pyListToRatCellWrap0, names.scalefactors); - names_struct.qzshifts = customCaller("NameStore.qzshifts", pyListToRatCellWrap0, names.qzshifts); - names_struct.bulkIns = customCaller("NameStore.bulkIns", pyListToRatCellWrap0, names.bulkIns); - names_struct.bulkOuts = customCaller("NameStore.bulkOuts", pyListToRatCellWrap0, names.bulkOuts); - names_struct.resolutionParams = customCaller("NameStore.resolutionParams", pyListToRatCellWrap0, names.resolutionParams); - names_struct.domainRatios = customCaller("NameStore.domainRatios", pyListToRatCellWrap0, names.domainRatios); - names_struct.contrasts = customCaller("NameStore.contrasts", pyListToRatCellWrap0, names.contrasts); + names_struct.params = customCaller("NameStore.params", pyListToRatCellWrap02d, names.params); + names_struct.backgroundParams = customCaller("NameStore.backgroundParams", pyListToRatCellWrap02d, names.backgroundParams); + names_struct.scalefactors = customCaller("NameStore.scalefactors", pyListToRatCellWrap02d, names.scalefactors); + names_struct.qzshifts = customCaller("NameStore.qzshifts", pyListToRatCellWrap02d, names.qzshifts); + names_struct.bulkIns = customCaller("NameStore.bulkIns", pyListToRatCellWrap02d, names.bulkIns); + names_struct.bulkOuts = customCaller("NameStore.bulkOuts", pyListToRatCellWrap02d, names.bulkOuts); + names_struct.resolutionParams = customCaller("NameStore.resolutionParams", pyListToRatCellWrap02d, names.resolutionParams); + names_struct.domainRatios = customCaller("NameStore.domainRatios", pyListToRatCellWrap02d, names.domainRatios); + names_struct.contrasts = customCaller("NameStore.contrasts", pyListToRatCellWrap02d, names.contrasts); return names_struct; } @@ -883,14 +849,14 @@ RAT::struct0_T createStruct0(const ProblemDefinition& problem) problem_struct.useImaginary = problem.useImaginary; problem_struct.repeatLayers = customCaller("Problem.repeatLayers", pyListToRatCellWrap2, problem.repeatLayers); problem_struct.contrastBackgroundParams = customCaller("Problem.contrastBackgroundParams", pyListToRatCellWrap3, problem.contrastBackgroundParams); - problem_struct.contrastBackgroundTypes = customCaller("Problem.contrastBackgroundTypes", pyListToRatCellWrap0, problem.contrastBackgroundTypes); - problem_struct.contrastBackgroundActions = customCaller("Problem.contrastBackgroundActions", pyListToRatCellWrap0, problem.contrastBackgroundActions); + problem_struct.contrastBackgroundTypes = customCaller("Problem.contrastBackgroundTypes", pyListToRatCellWrap02d, problem.contrastBackgroundTypes); + problem_struct.contrastBackgroundActions = customCaller("Problem.contrastBackgroundActions", pyListToRatCellWrap02d, problem.contrastBackgroundActions); problem_struct.contrastQzshifts = customCaller("Problem.contrastQzshifts", pyArrayToRatRowArray1d, problem.contrastQzshifts); problem_struct.contrastScalefactors = customCaller("Problem.contrastScalefactors", pyArrayToRatRowArray1d, problem.contrastScalefactors); problem_struct.contrastBulkIns = customCaller("Problem.contrastBulkIns", pyArrayToRatRowArray1d, problem.contrastBulkIns); problem_struct.contrastBulkOuts = customCaller("Problem.contrastBulkOuts", pyArrayToRatRowArray1d, problem.contrastBulkOuts); problem_struct.contrastResolutionParams = customCaller("Problem.contrastResolutionParams", pyListToRatCellWrap4, problem.contrastResolutionParams); - problem_struct.contrastResolutionTypes = customCaller("Problem.contrastResolutionTypes", pyListToRatCellWrap0, problem.contrastResolutionTypes); + problem_struct.contrastResolutionTypes = customCaller("Problem.contrastResolutionTypes", pyListToRatCellWrap02d, problem.contrastResolutionTypes); problem_struct.backgroundParams = customCaller("Problem.backgroundParams", pyArrayToRatRowArray1d, problem.backgroundParams); problem_struct.qzshifts = customCaller("Problem.qzshifts", pyArrayToRatRowArray1d, problem.qzshifts); problem_struct.scalefactors = customCaller("Problem.scalefactors", pyArrayToRatRowArray1d, problem.scalefactors); @@ -912,6 +878,8 @@ RAT::struct0_T createStruct0(const ProblemDefinition& problem) problem_struct.otherParams = customCaller("Problem.otherParams", pyArrayToRatRowArray1d, problem.otherParams); problem_struct.fitLimits = customCaller("Problem.fitLimits", pyArrayToRatArray2d, problem.fitLimits); problem_struct.otherLimits = customCaller("Problem.otherLimits", pyArrayToRatArray2d, problem.otherLimits); + problem_struct.priorNames = customCaller("Problem.priorNames", pyListToRatCellWrap01d, problem.priorNames); + problem_struct.priorValues = customCaller("Problem.priorValues", pyArrayToRatArray2d, problem.priorValues); problem_struct.names = createStruct1(problem.names); problem_struct.checks = createStruct2(problem.checks); @@ -936,22 +904,6 @@ RAT::struct3_T createStruct3(const Limits& limits) return limits_struct; } -RAT::struct5_T createStruct5(const Priors& priors) -{ - RAT::struct5_T priors_struct; - priors_struct.params = customCaller("Priors.params", pyListToUnboundedCell0, priors.params); - priors_struct.backgroundParams = customCaller("Priors.backgroundParams", pyListToUnboundedCell0, priors.backgroundParams); - priors_struct.scalefactors = customCaller("Priors.scalefactors", pyListToUnboundedCell0, priors.scalefactors); - priors_struct.qzshifts = customCaller("Priors.qzshifts", pyListToUnboundedCell0, priors.qzshifts); - priors_struct.bulkIns = customCaller("Priors.bulkIns", pyListToUnboundedCell0, priors.bulkIns); - priors_struct.bulkOuts = customCaller("Priors.bulkOuts", pyListToUnboundedCell0, priors.bulkOuts); - priors_struct.resolutionParams = customCaller("Priors.resolutionParams", pyListToUnboundedCell0, priors.resolutionParams); - priors_struct.domainRatios = customCaller("Priors.domainRatios", pyListToUnboundedCell0, priors.domainRatios); - priors_struct.priorNames = customCaller("Priors.priorNames", pyListToUnboundedCell1, priors.priorNames); - priors_struct.priorValues = customCaller("Priors.priorValues", pyArrayToRatArray2d, priors.priorValues); - - return priors_struct; -} RAT::struct4_T createStruct4(const Control& control) { @@ -1008,7 +960,19 @@ py::array_t pyArrayFromRatArray2d(coder::array array) return result_array; } -py::list pyListFromRatCellWrap0(coder::array values) +py::list pyListFromRatCellWrap01d(coder::array values) +{ + py::list result; + for (int32_T idx0{0}; idx0 < values.size(0); idx0++) { + std::string tmp; + stringFromRatBoundedArray(values[idx0].f1.data, values[idx0].f1.size, tmp); + result.append(tmp); + } + + return result; +} + +py::list pyListFromRatCellWrap02d(coder::array values) { py::list result; for (int32_T idx0{0}; idx0 < values.size(1); idx0++) { @@ -1114,7 +1078,7 @@ py::array_t pyArrayFromRatArray3d(coder::array array) return result_array; } -OutputResult OutputResultFromStruct6T(const RAT::struct6_T result) +OutputResult OutputResultFromStruct5T(const RAT::struct5_T result) { // Copy problem to output OutputResult output_result; @@ -1240,14 +1204,14 @@ ProblemDefinition problemDefinitionFromStruct0T(const RAT::struct0_T problem) problem_def.useImaginary = problem.useImaginary; problem_def.repeatLayers = pyListFromRatCellWrap2(problem.repeatLayers); problem_def.contrastBackgroundParams = pyListFromBoundedCellWrap>(problem.contrastBackgroundParams); - problem_def.contrastBackgroundTypes = pyListFromRatCellWrap0(problem.contrastBackgroundTypes); - problem_def.contrastBackgroundActions = pyListFromRatCellWrap0(problem.contrastBackgroundActions); + problem_def.contrastBackgroundTypes = pyListFromRatCellWrap02d(problem.contrastBackgroundTypes); + problem_def.contrastBackgroundActions = pyListFromRatCellWrap02d(problem.contrastBackgroundActions); problem_def.contrastQzshifts = pyArrayFromRatArray1d>(problem.contrastQzshifts); problem_def.contrastScalefactors = pyArrayFromRatArray1d>(problem.contrastScalefactors); problem_def.contrastBulkIns = pyArrayFromRatArray1d>(problem.contrastBulkIns); problem_def.contrastBulkOuts = pyArrayFromRatArray1d>(problem.contrastBulkOuts); problem_def.contrastResolutionParams = pyListFromBoundedCellWrap>(problem.contrastResolutionParams); - problem_def.contrastResolutionTypes = pyListFromRatCellWrap0(problem.contrastResolutionTypes); + problem_def.contrastResolutionTypes = pyListFromRatCellWrap02d(problem.contrastResolutionTypes); problem_def.backgroundParams = pyArrayFromRatArray1d>(problem.backgroundParams); problem_def.qzshifts = pyArrayFromRatArray1d>(problem.qzshifts); problem_def.scalefactors = pyArrayFromRatArray1d>(problem.scalefactors); @@ -1269,16 +1233,18 @@ ProblemDefinition problemDefinitionFromStruct0T(const RAT::struct0_T problem) problem_def.otherParams = pyArrayFromRatArray1d>(problem.otherParams); problem_def.fitLimits = pyArrayFromRatArray2d(problem.fitLimits); problem_def.otherLimits = pyArrayFromRatArray2d(problem.otherLimits); + problem_def.priorNames = pyListFromRatCellWrap01d(problem.priorNames); + problem_def.priorValues = pyArrayFromRatArray2d(problem.priorValues); - problem_def.names.params = pyListFromRatCellWrap0(problem.names.params); - problem_def.names.backgroundParams = pyListFromRatCellWrap0(problem.names.backgroundParams); - problem_def.names.scalefactors = pyListFromRatCellWrap0(problem.names.scalefactors); - problem_def.names.qzshifts = pyListFromRatCellWrap0(problem.names.qzshifts); - problem_def.names.bulkIns = pyListFromRatCellWrap0(problem.names.bulkIns); - problem_def.names.bulkOuts = pyListFromRatCellWrap0(problem.names.bulkOuts); - problem_def.names.resolutionParams = pyListFromRatCellWrap0(problem.names.resolutionParams); - problem_def.names.domainRatios = pyListFromRatCellWrap0(problem.names.domainRatios); - problem_def.names.contrasts = pyListFromRatCellWrap0(problem.names.contrasts); + problem_def.names.params = pyListFromRatCellWrap02d(problem.names.params); + problem_def.names.backgroundParams = pyListFromRatCellWrap02d(problem.names.backgroundParams); + problem_def.names.scalefactors = pyListFromRatCellWrap02d(problem.names.scalefactors); + problem_def.names.qzshifts = pyListFromRatCellWrap02d(problem.names.qzshifts); + problem_def.names.bulkIns = pyListFromRatCellWrap02d(problem.names.bulkIns); + problem_def.names.bulkOuts = pyListFromRatCellWrap02d(problem.names.bulkOuts); + problem_def.names.resolutionParams = pyListFromRatCellWrap02d(problem.names.resolutionParams); + problem_def.names.domainRatios = pyListFromRatCellWrap02d(problem.names.domainRatios); + problem_def.names.contrasts = pyListFromRatCellWrap02d(problem.names.contrasts); problem_def.checks.params = pyArrayFromRatArray1d>(problem.checks.params); problem_def.checks.backgroundParams = pyArrayFromRatArray1d>(problem.checks.backgroundParams); @@ -1292,14 +1258,14 @@ ProblemDefinition problemDefinitionFromStruct0T(const RAT::struct0_T problem) return problem_def; } -BayesResults bayesResultsFromStruct9T(const RAT::struct9_T results) +BayesResults bayesResultsFromStruct8T(const RAT::struct8_T results) { BayesResults bayesResults; bayesResults.chain = pyArrayFromRatArray2d(results.chain); - bayesResults.predictionIntervals.reflectivity = pyList1DFromRatCellWrap1D>(results.predictionIntervals.reflectivity); - bayesResults.predictionIntervals.sld = pyList2dFromRatCellWrap>(results.predictionIntervals.sld); + bayesResults.predictionIntervals.reflectivity = pyList1DFromRatCellWrap1D>(results.predictionIntervals.reflectivity); + bayesResults.predictionIntervals.sld = pyList2dFromRatCellWrap>(results.predictionIntervals.sld); bayesResults.predictionIntervals.sampleChi = pyArray1dFromBoundedArray>(results.predictionIntervals.sampleChi); bayesResults.confidenceIntervals.percentile95 = pyArrayFromRatArray2d(results.confidenceIntervals.percentile95); @@ -1343,23 +1309,22 @@ BayesResults bayesResultsFromStruct9T(const RAT::struct9_T results) return bayesResults; } -py::tuple RATMain(const ProblemDefinition& problem_def, const Limits& limits, const Control& control, const Priors& priors) +py::tuple RATMain(const ProblemDefinition& problem_def, const Limits& limits, const Control& control) { RAT::struct0_T problem_def_struct = createStruct0(problem_def); RAT::struct3_T limits_struct = createStruct3(limits); RAT::struct4_T control_struct = createStruct4(control); - RAT::struct5_T priors_struct = createStruct5(priors); // Output - RAT::struct6_T results; - RAT::struct9_T bayesResults; + RAT::struct5_T results; + RAT::struct8_T bayesResults; // Call the entry-point - RAT::RATMain(&problem_def_struct, &limits_struct, &control_struct, &priors_struct, &results, &bayesResults); + RAT::RATMain(&problem_def_struct, &limits_struct, &control_struct, &results, &bayesResults); // Copy result to output auto out_problem_def = problemDefinitionFromStruct0T(problem_def_struct); out_problem_def.customFiles = problem_def.customFiles.attr("copy")(); return py::make_tuple(out_problem_def, - OutputResultFromStruct6T(results), - bayesResultsFromStruct9T(bayesResults)); + OutputResultFromStruct5T(results), + bayesResultsFromStruct8T(bayesResults)); } py::array_t makeSLDProfileXY(real_T bulk_in, @@ -1677,45 +1642,6 @@ PYBIND11_MODULE(rat_core, m) { return lim; })); - py::class_(m, "Priors") - .def(py::init<>()) - .def_readwrite("params", &Priors::params) - .def_readwrite("backgroundParams", &Priors::backgroundParams) - .def_readwrite("scalefactors", &Priors::scalefactors) - .def_readwrite("qzshifts", &Priors::qzshifts) - .def_readwrite("bulkIns", &Priors::bulkIns) - .def_readwrite("bulkOuts", &Priors::bulkOuts) - .def_readwrite("resolutionParams", &Priors::resolutionParams) - .def_readwrite("domainRatios", &Priors::domainRatios) - .def_readwrite("priorNames", &Priors::priorNames) - .def_readwrite("priorValues", &Priors::priorValues) - .def(py::pickle( - [](const Priors &prior) { // __getstate__ - /* Return a tuple that fully encodes the state of the object */ - return py::make_tuple(prior.params, prior.backgroundParams, prior.scalefactors, prior.qzshifts, prior.bulkIns, - prior.bulkOuts, prior.resolutionParams, prior.domainRatios, prior.priorNames, prior.priorValues); - }, - [](py::tuple t) { // __setstate__ - if (t.size() != 10) - throw std::runtime_error("Encountered invalid state unpickling Limits object!"); - - /* Create a new C++ instance */ - Priors prior; - - prior.params = t[0].cast(); - prior.backgroundParams = t[1].cast(); - prior.scalefactors = t[2].cast(); - prior.qzshifts = t[3].cast(); - prior.bulkIns = t[4].cast(); - prior.bulkOuts = t[5].cast(); - prior.resolutionParams = t[6].cast(); - prior.domainRatios = t[7].cast(); - prior.priorNames = t[8].cast(); - prior.priorValues = t[9].cast>(); - - return prior; - })); - py::class_(m, "Control") .def(py::init<>()) .def_readwrite("parallel", &Control::parallel) @@ -1840,6 +1766,8 @@ PYBIND11_MODULE(rat_core, m) { .def_readwrite("otherParams", &ProblemDefinition::otherParams) .def_readwrite("fitLimits", &ProblemDefinition::fitLimits) .def_readwrite("otherLimits", &ProblemDefinition::otherLimits) + .def_readwrite("priorNames", &ProblemDefinition::priorNames) + .def_readwrite("priorValues", &ProblemDefinition::priorValues) .def_readwrite("names", &ProblemDefinition::names) .def_readwrite("checks", &ProblemDefinition::checks) .def(py::pickle( @@ -1853,13 +1781,13 @@ PYBIND11_MODULE(rat_core, m) { p.bulkIns, p.bulkOuts, p.resolutionParams, p.params, p.numberOfLayers, p.contrastLayers, p.layersDetails, p.customFiles, p.modelType, p.contrastCustomFiles, p.contrastDomainRatios, p.domainRatios, p.numberOfDomainContrasts, p.domainContrastLayers, p.fitParams, p.otherParams, p.fitLimits, - p.otherLimits, p.names.params, p.names.backgroundParams, p.names.scalefactors, p.names.qzshifts, - p.names.bulkIns, p.names.bulkOuts, p.names.resolutionParams, p.names.domainRatios, p.names.contrasts, - p.checks.params, p.checks.backgroundParams, p.checks.scalefactors, p.checks.qzshifts, + p.otherLimits, p.priorNames, p.priorValues, p.names.params, p.names.backgroundParams, p.names.scalefactors, + p.names.qzshifts, p.names.bulkIns, p.names.bulkOuts, p.names.resolutionParams, p.names.domainRatios, + p.names.contrasts, p.checks.params, p.checks.backgroundParams, p.checks.scalefactors, p.checks.qzshifts, p.checks.bulkIns, p.checks.bulkOuts, p.checks.resolutionParams, p.checks.domainRatios); }, [](py::tuple t) { // __setstate__ - if (t.size() != 58) + if (t.size() != 60) throw std::runtime_error("Encountered invalid state unpickling ProblemDefinition object!"); /* Create a new C++ instance */ @@ -1906,25 +1834,27 @@ PYBIND11_MODULE(rat_core, m) { p.otherParams = t[38].cast>(); p.fitLimits = t[39].cast>(); p.otherLimits = t[40].cast>(); + p.priorNames = t[41].cast(); + p.priorValues = t[42].cast>(); - p.names.params = t[41].cast(); - p.names.backgroundParams = t[42].cast(); - p.names.scalefactors = t[43].cast(); - p.names.qzshifts = t[44].cast(); - p.names.bulkIns = t[45].cast(); - p.names.bulkOuts = t[46].cast(); - p.names.resolutionParams = t[47].cast(); - p.names.domainRatios = t[48].cast(); - p.names.contrasts = t[49].cast(); - - p.checks.params = t[50].cast>(); - p.checks.backgroundParams = t[51].cast>(); - p.checks.scalefactors = t[52].cast>(); - p.checks.qzshifts = t[53].cast>(); - p.checks.bulkIns = t[54].cast>(); - p.checks.bulkOuts = t[55].cast>(); - p.checks.resolutionParams = t[56].cast>(); - p.checks.domainRatios = t[57].cast>(); + p.names.params = t[43].cast(); + p.names.backgroundParams = t[44].cast(); + p.names.scalefactors = t[45].cast(); + p.names.qzshifts = t[46].cast(); + p.names.bulkIns = t[47].cast(); + p.names.bulkOuts = t[48].cast(); + p.names.resolutionParams = t[49].cast(); + p.names.domainRatios = t[50].cast(); + p.names.contrasts = t[51].cast(); + + p.checks.params = t[52].cast>(); + p.checks.backgroundParams = t[53].cast>(); + p.checks.scalefactors = t[54].cast>(); + p.checks.qzshifts = t[55].cast>(); + p.checks.bulkIns = t[56].cast>(); + p.checks.bulkOuts = t[57].cast>(); + p.checks.resolutionParams = t[58].cast>(); + p.checks.domainRatios = t[59].cast>(); return p; })); diff --git a/tests/test_inputs.py b/tests/test_inputs.py index 32fc4835..f9bf3ae8 100644 --- a/tests/test_inputs.py +++ b/tests/test_inputs.py @@ -9,7 +9,7 @@ import RATapi import RATapi.wrappers from RATapi.inputs import FileHandles, check_indices, make_controls, make_input, make_problem -from RATapi.rat_core import Checks, Control, Limits, NameStore, Priors, ProblemDefinition +from RATapi.rat_core import Checks, Control, Limits, NameStore, ProblemDefinition from RATapi.utils.enums import ( BackgroundActions, BoundHandling, @@ -197,6 +197,28 @@ def standard_layers_problem(test_names, test_checks): [6.2e-06, 6.35e-06], [0.01, 0.05], ] + problem.priorNames = [ + "Substrate Roughness", + "Test Thickness", + "Test SLD", + "Test Roughness", + "Background Param 1", + "Scalefactor 1", + "SLD Air", + "SLD D2O", + "Resolution Param 1", + ] + problem.priorValues = [ + [1, 0.0, np.inf], + [1, 0.0, np.inf], + [1, 0.0, np.inf], + [1, 0.0, np.inf], + [1, 0.0, np.inf], + [1, 0.0, np.inf], + [1, 0.0, np.inf], + [1, 0.0, np.inf], + [1, 0.0, np.inf], + ] problem.customFiles = FileHandles([]) problem.names = test_names problem.checks = test_checks @@ -258,6 +280,30 @@ def domains_problem(test_names, test_checks): [0.01, 0.05], [0.4, 0.6], ] + problem.priorNames = [ + "Substrate Roughness", + "Test Thickness", + "Test SLD", + "Test Roughness", + "Background Param 1", + "Scalefactor 1", + "SLD Air", + "SLD D2O", + "Resolution Param 1", + "Domain Ratio 1", + ] + problem.priorValues = [ + [1, 0.0, np.inf], + [1, 0.0, np.inf], + [1, 0.0, np.inf], + [1, 0.0, np.inf], + [1, 0.0, np.inf], + [1, 0.0, np.inf], + [1, 0.0, np.inf], + [1, 0.0, np.inf], + [1, 0.0, np.inf], + [1, 0.0, np.inf], + ] problem.customFiles = FileHandles([]) problem.names = test_names problem.names.domainRatios = ["Domain Ratio 1"] @@ -319,6 +365,28 @@ def custom_xy_problem(test_names, test_checks): [6.2e-06, 6.35e-06], [0.01, 0.05], ] + problem.priorNames = [ + "Substrate Roughness", + "Test Thickness", + "Test SLD", + "Test Roughness", + "Background Param 1", + "Scalefactor 1", + "SLD Air", + "SLD D2O", + "Resolution Param 1", + ] + problem.priorValues = [ + [1, 0.0, np.inf], + [1, 0.0, np.inf], + [1, 0.0, np.inf], + [1, 0.0, np.inf], + [1, 0.0, np.inf], + [1, 0.0, np.inf], + [1, 0.0, np.inf], + [1, 0.0, np.inf], + [1, 0.0, np.inf], + ] problem.customFiles = FileHandles( [RATapi.models.CustomFile(name="Test Custom File", filename="cpp_test.dll", language="cpp")] ) @@ -360,94 +428,6 @@ def domains_limits(): return limits -@pytest.fixture -def normal_priors(): - """The expected priors object from "standard_layers_project" and "custom_xy_project".""" - priors = Priors() - priors.params = [ - ["Substrate Roughness", RATapi.utils.enums.Priors.Uniform, 0.0, np.inf], - ["Test Thickness", RATapi.utils.enums.Priors.Uniform, 0.0, np.inf], - ["Test SLD", RATapi.utils.enums.Priors.Uniform, 0.0, np.inf], - ["Test Roughness", RATapi.utils.enums.Priors.Uniform, 0.0, np.inf], - ] - priors.backgroundParams = [["Background Param 1", RATapi.utils.enums.Priors.Uniform, 0.0, np.inf]] - priors.scalefactors = [["Scalefactor 1", RATapi.utils.enums.Priors.Uniform, 0.0, np.inf]] - priors.qzshifts = [] - priors.bulkIns = [["SLD Air", RATapi.utils.enums.Priors.Uniform, 0.0, np.inf]] - priors.bulkOuts = [["SLD D2O", RATapi.utils.enums.Priors.Uniform, 0.0, np.inf]] - priors.resolutionParams = [["Resolution Param 1", RATapi.utils.enums.Priors.Uniform, 0.0, np.inf]] - priors.domainRatios = [] - priors.priorNames = [ - "Substrate Roughness", - "Test Thickness", - "Test SLD", - "Test Roughness", - "Background Param 1", - "Scalefactor 1", - "SLD Air", - "SLD D2O", - "Resolution Param 1", - ] - priors.priorValues = [ - [1, 0.0, np.inf], - [1, 0.0, np.inf], - [1, 0.0, np.inf], - [1, 0.0, np.inf], - [1, 0.0, np.inf], - [1, 0.0, np.inf], - [1, 0.0, np.inf], - [1, 0.0, np.inf], - [1, 0.0, np.inf], - ] - - return priors - - -@pytest.fixture -def domains_priors(): - """The expected priors object from "domains_project".""" - priors = Priors() - priors.params = [ - ["Substrate Roughness", RATapi.utils.enums.Priors.Uniform, 0.0, np.inf], - ["Test Thickness", RATapi.utils.enums.Priors.Uniform, 0.0, np.inf], - ["Test SLD", RATapi.utils.enums.Priors.Uniform, 0.0, np.inf], - ["Test Roughness", RATapi.utils.enums.Priors.Uniform, 0.0, np.inf], - ] - priors.backgroundParams = [["Background Param 1", RATapi.utils.enums.Priors.Uniform, 0.0, np.inf]] - priors.scalefactors = [["Scalefactor 1", RATapi.utils.enums.Priors.Uniform, 0.0, np.inf]] - priors.qzshifts = [] - priors.bulkIns = [["SLD Air", RATapi.utils.enums.Priors.Uniform, 0.0, np.inf]] - priors.bulkOuts = [["SLD D2O", RATapi.utils.enums.Priors.Uniform, 0.0, np.inf]] - priors.resolutionParams = [["Resolution Param 1", RATapi.utils.enums.Priors.Uniform, 0.0, np.inf]] - priors.domainRatios = [["Domain Ratio 1", RATapi.utils.enums.Priors.Uniform, 0.0, np.inf]] - priors.priorNames = [ - "Substrate Roughness", - "Test Thickness", - "Test SLD", - "Test Roughness", - "Background Param 1", - "Scalefactor 1", - "SLD Air", - "SLD D2O", - "Resolution Param 1", - "Domain Ratio 1", - ] - priors.priorValues = [ - [1, 0.0, np.inf], - [1, 0.0, np.inf], - [1, 0.0, np.inf], - [1, 0.0, np.inf], - [1, 0.0, np.inf], - [1, 0.0, np.inf], - [1, 0.0, np.inf], - [1, 0.0, np.inf], - [1, 0.0, np.inf], - [1, 0.0, np.inf], - ] - - return priors - - @pytest.fixture def standard_layers_controls(): """The expected controls object for input to the compiled RAT code given the default inputs and @@ -525,39 +505,35 @@ def custom_xy_controls(): @pytest.mark.parametrize( - ["test_project", "test_problem", "test_limits", "test_priors", "test_controls"], + ["test_project", "test_problem", "test_limits", "test_controls"], [ ( "standard_layers_project", "standard_layers_problem", "normal_limits", - "normal_priors", "standard_layers_controls", ), ( "custom_xy_project", "custom_xy_problem", "normal_limits", - "normal_priors", "custom_xy_controls", ), ( "domains_project", "domains_problem", "domains_limits", - "domains_priors", "standard_layers_controls", ), ], ) -def test_make_input(test_project, test_problem, test_limits, test_priors, test_controls, request) -> None: +def test_make_input(test_project, test_problem, test_limits, test_controls, request) -> None: """When converting the "project" and "controls", we should obtain the five input objects required for the compiled RAT code. """ test_project = request.getfixturevalue(test_project) test_problem = request.getfixturevalue(test_problem) test_limits = request.getfixturevalue(test_limits) - test_priors = request.getfixturevalue(test_priors) test_controls = request.getfixturevalue(test_controls) parameter_fields = [ @@ -571,7 +547,7 @@ def test_make_input(test_project, test_problem, test_limits, test_priors, test_c "domainRatios", ] - problem, limits, priors, controls = make_input(test_project, RATapi.Controls()) + problem, limits, controls = make_input(test_project, RATapi.Controls()) problem = pickle.loads(pickle.dumps(problem)) check_problem_equal(problem, test_problem) @@ -579,32 +555,24 @@ def test_make_input(test_project, test_problem, test_limits, test_priors, test_c for limit_field in parameter_fields: assert np.all(getattr(limits, limit_field) == getattr(test_limits, limit_field)) - priors = pickle.loads(pickle.dumps(priors)) - for prior_field in parameter_fields: - assert getattr(priors, prior_field) == getattr(test_priors, prior_field) - - assert priors.priorNames == test_priors.priorNames - assert np.all(priors.priorValues == test_priors.priorValues) - controls = pickle.loads(pickle.dumps(controls)) check_controls_equal(controls, test_controls) @pytest.mark.parametrize( - ["test_project", "test_check", "test_problem"], + ["test_project", "test_problem"], [ - ("standard_layers_project", "test_checks", "standard_layers_problem"), - ("custom_xy_project", "test_checks", "custom_xy_problem"), - ("domains_project", "test_checks", "domains_problem"), + ("standard_layers_project", "standard_layers_problem"), + ("custom_xy_project", "custom_xy_problem"), + ("domains_project", "domains_problem"), ], ) -def test_make_problem(test_project, test_problem, test_check, request) -> None: +def test_make_problem(test_project, test_problem, request) -> None: """The problem object should contain the relevant parameters defined in the input project object.""" test_project = request.getfixturevalue(test_project) test_problem = request.getfixturevalue(test_problem) - test_check = request.getfixturevalue(test_check) - problem = make_problem(test_project, test_check) + problem = make_problem(test_project) check_problem_equal(problem, test_problem) @@ -749,6 +717,7 @@ def check_problem_equal(actual_problem, expected_problem) -> None: "numberOfContrasts", "numberOfLayers", "numberOfDomainContrasts", + "priorNames", ] array_fields = [ @@ -780,6 +749,7 @@ def check_problem_equal(actual_problem, expected_problem) -> None: "otherParams", "fitLimits", "otherLimits", + "priorValues", ] checks_fields = [ "params", From 0bd4fadae7502e397cf0c5a41c0b91b883b8be63 Mon Sep 17 00:00:00 2001 From: StephenNneji <34302892+StephenNneji@users.noreply.github.com> Date: Mon, 24 Feb 2025 09:53:47 +0000 Subject: [PATCH 04/20] Adds some docstring to pybind function and some refactors (#133) Co-authored-by: Alex H. Room <69592136+alexhroom@users.noreply.github.com> --- MANIFEST.in | 2 +- RATapi/utils/plotting.py | 3 +- RATapi/wrappers.py | 9 +- cpp/includes/defines.h | 737 +++++++++++++++++++++++++++++++++++ cpp/includes/functions.h | 500 ++++++++++++++++++++++++ cpp/rat.cpp | 809 ++++----------------------------------- setup.py | 1 + tests/test_plotting.py | 22 +- tests/test_wrappers.py | 2 +- 9 files changed, 1323 insertions(+), 762 deletions(-) create mode 100644 cpp/includes/defines.h create mode 100644 cpp/includes/functions.h diff --git a/MANIFEST.in b/MANIFEST.in index 086d5f71..a4a255ab 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,5 @@ include README.md -recursive-include cpp/RAT * +recursive-include cpp * prune tests prune */__pycache__ global-exclude .git diff --git a/RATapi/utils/plotting.py b/RATapi/utils/plotting.py index 7b8c03e8..e1291cf0 100644 --- a/RATapi/utils/plotting.py +++ b/RATapi/utils/plotting.py @@ -158,8 +158,7 @@ def plot_ref_sld_helper( layers[-1, 1], # Bulk Out data.subRoughs[i], # roughness layer, - len(layer), - 1.0, + 1, ) sld_plot.plot( diff --git a/RATapi/wrappers.py b/RATapi/wrappers.py index c428c254..c0489227 100644 --- a/RATapi/wrappers.py +++ b/RATapi/wrappers.py @@ -66,12 +66,17 @@ def handle(*args): ) return np.array(output, "float").tolist() else: - output, sub_rough = getattr(self.engine, self.function_name)( + matlab_args = [ np.array(args[0], "float"), # params np.array(args[1], "float"), # bulk in np.array(args[2], "float"), # bulk out float(args[3] + 1), # contrast - float(-1 if len(args) < 5 else args[4] + 1), # domain + ] + if len(args) > 4: + matlab_args.append(float(args[4] + 1)) # domain number + + output, sub_rough = getattr(self.engine, self.function_name)( + *matlab_args, nargout=2, ) return np.array(output, "float").tolist(), float(sub_rough) diff --git a/cpp/includes/defines.h b/cpp/includes/defines.h new file mode 100644 index 00000000..a9a59b7f --- /dev/null +++ b/cpp/includes/defines.h @@ -0,0 +1,737 @@ +#ifndef RAT_DEFINES_H +#define RAT_DEFINES_H + +#include +#include +#include + +namespace py = pybind11; + +const std::string docsProgressEventData = R"(The Python binding for the C++ progressEventData struct. +The progress event shows the percentage completion for the calculation. This can be emitted by +the DREAM algorithm only. + +Parameters +---------- +message : str + The title text for the event. +percent : float + The percentage of the calculation completed (as a number between 0 and 1) +)"; + +struct ProgressEventData +{ + std::string message; + double percent; +}; + +const std::string docsPlotEventData = R"(The Python binding for the C++ plotEventData struct. +The plot event data contains intermediate results from the calculation. This can be emitted +by the Simplex and DE algorithms only. + +Parameters +---------- +reflectivity : list + The reflectivity curves for each contrast, with the same range as the data + (``data_range`` in the contrast's ``Data`` object) +shiftedData : list + The data with scalefactors and background corrections applied. +sldProfiles : list + The SLD profiles for each contrast. +resampledLayers : list + If resampling is used, the SLD for each contrast after resampling has been performed. +subRoughs : np.ndarray[np.float] + The substrate roughness values for each contrast. +resample : np.ndarray[np.float] + An array containing whether each contrast was resampled. +dataPresent : np.ndarray[np.float] + Non-zero values indicates if data is present for the contrast. +modelType : str + The model type for the project. +contrastNames : list + The names of all contrasts in the project. +)"; + +struct PlotEventData +{ + py::list reflectivity; + py::list shiftedData; + py::list sldProfiles; + py::list resampledLayers; + py::array_t subRoughs; + py::array_t resample; + py::array_t dataPresent; + std::string modelType; + py::list contrastNames; +}; + + +const std::string docsPredictionIntervals = R"(The Python binding for the C++ predictionIntervals struct. +The Bayesian prediction intervals for 95% and 65% confidence. + +For ``reflectivity`` and ``sld``, each list item is an array +with five rows. The rows represent: + +- 0: the 5th percentile; +- 1: the 35th percentile; +- 2: the mean value of the interval; +- 3: the 65th percentile; +- 4: the 95th percentile. + +Parameters +---------- +reflectivity : list + The prediction interval data for reflectivity of each contrast. +sld : list + The prediction interval data for SLD of each contrast. +sampleChi : np.ndarray[np.float] + The value of sumChi at each point of the Markov chain. +)"; + +struct PredictionIntervals +{ + py::list reflectivity; + py::list sld; + py::array_t sampleChi; +}; + +const std::string docsConfidenceIntervals = R"(The Python binding for the C++ confidenceIntervals struct. +The 65% and 95% confidence intervals for the best fit results. + +Parameters +---------- +percentile95 : np.ndarray[np.float] + The 95% confidence intervals for each fit parameter. +percentile65 : np.ndarray[np.float] + The 65% confidence intervals for each fit parameter. +mean : np.ndarray[np.float] + The mean values for each fit parameter. +)"; + +struct ConfidenceIntervals +{ + py::array_t percentile95; + py::array_t percentile65; + py::array_t mean; +}; + +const std::string docsNestedSamplerOutput = R"(The Python binding for the C++ nestedSamplerOutput struct. +The output information from the Nested Sampler (ns). + +Parameters +---------- +logZ : float + The natural logarithm of the evidence Z for the parameter values. +logZErr : float + The estimated uncertainty in the final value of logZ. +nestSamples : np.ndarray[np.float] + ``NestedSamplerOutput.nestSamples[i, j]`` represents the values + sampled at iteration ``i``, where this value is: + + - the value sampled for parameter ``j``, for ``j`` in ``0:nParams``, + - the minimum log-likelihood for ``j = nParams + 1``. + +postSamples : np.ndarray[np.float] + The posterior values at the points sampled in ``NestedSamplerOutput.nestSamples``. +)"; + +struct NestedSamplerOutput +{ + real_T logZ; + real_T logZErr; + py::array_t nestSamples; + py::array_t postSamples; +}; + +const std::string docsDreamParams = R"(The Python binding for the C++ dreamParams struct. +The parameters used by the inner DREAM algorithm. + +Parameters +---------- +nParams : float + The number of parameters used by the algorithm. +nChains : float + The number of MCMC chains used by the algorithm. +nGenerations : float + The number of DE generations calculated per iteration. +parallel : bool + Whether the algorithm should run chains in parallel. +CPU : float + The number of processor cores used for parallel chains. +jumpProbability : float + A probability range for the size of jumps when performing subspace sampling. +pUnitGamma : float + The probability that the scaling-down factor of jumps will be ignored + and a larger jump will be taken for one iteration. +nCR : float + The number of crossovers performed each iteration. +delta : float + The number of chain mutation pairs proposed each iteration. +steps : float + The number of MCMC steps to perform between conversion checks. +zeta : float + The ergodicity of the algorithm. +outlier : str + What test should be used to detect outliers. +adaptPCR : bool + Whether the crossover probability for differential evolution should be + adapted by the algorithm as it runs. +thinning : float + The thinning rate of each Markov chain (to reduce memory intensity) +epsilon : float + The cutoff threshold for Approximate Bayesian Computation (if used) +ABC : bool + Whether Approximate Bayesian Computation is used. +IO : bool + Whether the algorithm should perform IO writes of the model in parallel. +storeOutput : bool + Whether output model simulations are performed. +R : np.np.ndarray[np.float] + An array where row ``i`` is the list of chains + with which chain ``i`` can mutate. +)"; + +struct DreamParams +{ + real_T nParams; + real_T nChains; + real_T nGenerations; + boolean_T parallel; + real_T CPU; + real_T jumpProbability; + real_T pUnitGamma; + real_T nCR; + real_T delta; + real_T steps; + real_T zeta; + std::string outlier; + boolean_T adaptPCR; + real_T thinning; + real_T epsilon; + boolean_T ABC; + boolean_T IO; + boolean_T storeOutput; + py::array_t R; +}; + +const std::string docsDreamOutput = R"(The Python binding for the C++ DreamOutput struct. +The diagnostic output information from DREAM. + +Parameters +---------- +allChains : np.ndarray[np.float] + An ``nGenerations`` x ``nParams + 2`` x ``nChains`` size array, + where ``chain_k = DreamOutput.allChains[:, :, k]`` + is the data of chain ``k`` in the final iteration; + for generation i of the final iteration, ``chain_k[i, j]`` represents: + + - the sampled value of parameter ``j`` for ``j in 0:nParams``; + - the associated log-prior for those sampled values for ``j = nParams + 1``; + - the associated log-likelihood for those sampled values for ``j = nParams + 2``. + +outlierChains : np.ndarray[np.float] + A two-column array where ``DreamOutput.AR[i, 1]`` is the index of a chain + and ``DreamOutput.AR[i, 0]`` is the length of that chain when it was removed + for being an outlier. +runtime : float + The runtime of the DREAM algorithm in seconds. +iteration : float + The number of iterations performed. +modelOutput : float + Unused. Will always be 0. +AR : np.ndarray[np.float] + A two-column array where ``DreamOutput.AR[i, 0]`` is an iteration number + and ``DreamOutput.AR[i, 1]`` is the average acceptance rate of chain step + proposals for that iteration. +R_stat : np.ndarray[np.float] + An array where ``DreamOutput.R_stat[i, 0]`` is an iteration number and + ``DreamOutput.R_stat[i, j]`` is the convergence statistic for parameter ``j`` + at that iteration (where chains are indexed 1 to ``nParams`` inclusive). +CR : np.ndarray[np.float] + A four-column array where ``DreamOutput.CR[i, 0]`` is an iteration number, + ``and DreamOutput.CR[i, j]`` is the selection probability of the ``j``'th crossover + value for that iteration. +)"; + +struct DreamOutput +{ + py::array_t allChains; + py::array_t outlierChains; + real_T runtime; + real_T iteration; + real_T modelOutput; + py::array_t AR; + py::array_t R_stat; + py::array_t CR; +}; + +const std::string docsBayesResults = R"(The Python binding for the C++ bayesResults struct. +The results of a Bayesian RAT calculation. + +Parameters +---------- +predictionIntervals : RATapi.rat_core.orePredictionIntervals + The prediction intervals. +confidenceIntervals : RATapi.rat_core.ConfidenceIntervals + The 65% and 95% confidence intervals for the best fit results. +dreamParams : RATapi.rat_core.DreamParams + The parameters used by DREAM, if relevant. +dreamOutput : RATapi.rat_core.DreamOutput + The output from DREAM if DREAM was used. +nestedSamplerOutput : RATapi.rat_core.NestedSamplerOutput + The output from nested sampling if ns was used. +chain : np.ndarray + The MCMC chains for each parameter. + The ``i``'th column of the array contains the chain for parameter ``fitNames[i]``. +)"; + +struct BayesResults +{ + PredictionIntervals predictionIntervals; + ConfidenceIntervals confidenceIntervals; + DreamParams dreamParams; + DreamOutput dreamOutput; + NestedSamplerOutput nestedSamplerOutput; + py::array_t chain; +}; + +const std::string docsCalculation = R"(The Python binding for the C++ calculationResult struct. +The goodness of fit from the Abeles calculation. + +Parameters +---------- +chiValues : np.ndarray[np.float] + The chi-squared value for each contrast. +sumChi : float + The sum of the chiValues array. +)"; + +struct Calculation +{ + py::array_t chiValues; + real_T sumChi; +}; + +const std::string docsContrastParams = R"(The Python binding for the C++ contrastParams struct. +The experimental parameters for each contrast. + +Parameters +---------- +scalefactors : np.ndarray[np.float] + The scalefactor values for each contrast. +bulkIn : np.ndarray[np.float] + The bulk in values for each contrast. +bulkOut : np.ndarray[np.float] + The bulk out values for each contrast. +subRoughs : np.ndarray[np.float] + The substrate roughness values for each contrast. +resample : np.ndarray[np.float] + An array containing whether each contrast was resampled. +)"; + +struct ContrastParams +{ + py::array_t scalefactors; + py::array_t bulkIn; + py::array_t bulkOut; + py::array_t subRoughs; + py::array_t resample; +}; + +const std::string docsOutputResult = R"(The C++ result struct of a RAT calculation. + +Parameters +---------- +reflectivity : list + The reflectivity curves for each contrast, with the same range as the data + (``data_range`` in the contrast's ``Data`` object) +simulation : list + The reflectivity curves for each contrast, which can be a wider range to allow extrapolation + (``simulation_range`` in the contrast's ``Data`` object). +shiftedData : list + The data with scalefactors and background corrections applied. +backgrounds : list + The background for each contrast defined over the simulation range. +resolutions : list + The resolution for each contrast defined over the simulation range. +layerSlds : list + The array of layer parameter values for each contrast. +sldProfiles : list + The SLD profiles for each contrast. +resampledLayers : list + If resampling is used, the SLD for each contrast after resampling has been performed. +calculationResults : RATapi.rat_core.Calculation + The chi-squared fit results from the final calculation and fit. +contrastParams : RATapi.rat_core.ContrastParams + The experimental parameters for the contrasts. +fitParams : np.ndarray[np.float] + The best fit value of the parameter with name ``fitNames[i]``. +fitNames : list[str] + The names of the fit parameters, where ``fitNames[i]`` is the name + of the parameter with value given in ``fitParams[i]``. +)"; + +struct OutputResult { + py::list reflectivity; + py::list simulation; + py::list shiftedData; + py::list backgrounds; + py::list resolutions; + py::list layerSlds; + py::list sldProfiles; + py::list resampledLayers; + Calculation calculationResults {}; + ContrastParams contrastParams {}; + py::array_t fitParams; + py::list fitNames; +}; + +const std::string docsLimits = R"(The Python binding for the C++ limit struct which contains +Min and max values for each parameter defined in the project. + +Parameters +---------- +params : np.ndarray[np.float] + Limits for params in the problem definition. +backgroundParams : np.ndarray[np.float] + Limits for backgroundParams in the problem definition. +scalefactors : np.ndarray[np.float] + Limits for scalefactors in the problem definition. +qzshifts : np.ndarray[np.float] + Limits for qzshifts in the problem definition. +bulkIns : np.ndarray[np.float] + Limits for bulkIns in the problem definition. +bulkOuts : np.ndarray[np.float] + Limits for bulkOuts in the problem definition. +resolutionParams : np.ndarray[np.float] + Limits for resolutionParams in the problem definition. +domainRatios : np.ndarray[np.float] + Limits for domainRatios in the problem definition. +)"; + +struct Limits { + py::array_t params; + py::array_t backgroundParams; + py::array_t scalefactors; + py::array_t qzshifts; + py::array_t bulkIns; + py::array_t bulkOuts; + py::array_t resolutionParams; + py::array_t domainRatios; +}; + +const std::string docsNameStore = R"(The Python binding for the C++ names struct which +contains names of all parameters in the project. + +Parameters +---------- +params : list + Names of params in the problem definition. +backgroundParams : list + Names of backgroundParams in the problem definition. +scalefactors : list + Names of scalefactors in the problem definition. +qzshifts : list + Names of qzshifts in the problem definition. +bulkIns : list + Names of bulkIns in the problem definition. +bulkName: list + Names of bulkOuts in the problem definition. +resolutionParams : list + Names of resolutionParams in the problem definition. +domainRatios : list + Names of domainRatios in the problem definition. +)"; + +struct NameStore { + py::list params; + py::list backgroundParams; + py::list scalefactors; + py::list qzshifts; + py::list bulkIns; + py::list bulkOuts; + py::list resolutionParams; + py::list domainRatios; + py::list contrasts; +}; + + +const std::string docsChecks = R"(The Python binding for the C++ checks struct which contains +flags indicating which parameters should be fitted in the project. + +For each attribute, if index ``i`` is non-zero, then parameter ``i`` in that attribute is fitted, e.g. if ``Checks.scalefactors = [0.0, 1.0, 1.0]``, then the second and third scalefactors are fitted and the first is not. + +Parameters +---------- +params : np.ndarray[np.float] + Non-zero values indicates which params is fitted. +backgroundParams : np.ndarray[np.float] + Non-zero values indicates which backgroundParams is fitted. +scalefactors : np.ndarray[np.float] + Non-zero values indicates which scalefactors is fitted. +qzshifts : np.ndarray[np.float] + Non-zero values indicates which qzshifts is fitted. +bulkIns : np.ndarray[np.float] + Non-zero values indicates which bulkIns is fitted. +bulkOuts : np.ndarray[np.float] + Non-zero values indicates which bulkOuts is fitted. +resolutionParams : np.ndarray[np.float] + Non-zero values indicates which resolutionParams is fitted. +domainRatios : np.ndarray[np.float] + Non-zero values indicates which domainRatios is fitted. +)"; + +struct Checks { + py::array_t params; + py::array_t backgroundParams; + py::array_t qzshifts; + py::array_t scalefactors; + py::array_t bulkIns; + py::array_t bulkOuts; + py::array_t resolutionParams; + py::array_t domainRatios; +}; + +const std::string docsProblemDefinition = R"(The Python binding for the C++ problem struct. + +Parameters +---------- +TF : str + The target function for the calculation which can be 'normal' or 'domains'. +resample : np.ndarray[np.float] + If ``resample[i]`` is non-zero, then contrast ``i`` will be resampled. +data : list + Data for each contrast. +dataPresent : np.ndarray[np.float] + If ``dataPresent[i]`` is non-zero, then contrast ``i`` has experimental data. +dataLimits : list + Data limits for each contrast. +simulationLimits : list; + Simulation for each contrast. +oilChiDataPresent : np.ndarray[np.float] + If ``dataPresent[i]`` is non-zero, then contrast ``i`` has oilChi data. This is currently not being used. +numberOfContrasts : int + Number of contrasts. +geometry : str + The geometry to use which can be 'air/substrate' or 'substrate/liquid' +useImaginary : bool + Indicates whether imaginary component is used for the SLD value in layers, i.e. + absorption is set to True for the project. +repeatLayers : list + Information about repeating layers for each contrast. This is currently not being used. +contrastBackgroundParams : list + Indices of backgroundParams used for each contrast +contrastBackgroundTypes : list + Background type for each contrast. +contrastBackgroundActions : list + Background action for each contrast. +contrastQzshifts : np.ndarray[np.float] + Indices of Qzshifts used for each contrast. This is currently not being used. +contrastScalefactors : np.ndarray[np.float] + Indices of scalefactors used for each contrast. +contrastBulkIns : np.ndarray[np.float] + Indices of BulkIns used for each contrast. +contrastBulkOuts : np.ndarray[np.float] + Indices of BulkIns used for each contrast. +contrastResolutionParams : list + Indices of resolutionParams used for each contrast +contrastResolutionTypes : list + Resolution type for each contrast. +backgroundParams : np.ndarray[np.float] + Background parameter values. +qzshifts : np.ndarray[np.float] + Qzshift values. This currently not being used. +scalefactors : np.ndarray[np.float] + Scalefactors values. +bulkIns : np.ndarray[np.float] + BulkIn values. +bulkOuts : np.ndarray[np.float] + BulkOut values. +resolutionParams : np.ndarray[np.float] + Resolution parameter values. +params : np.ndarray[np.float] + Parameter values. +numberOfLayers : int + Number of layers. +contrastLayers : list + Indices of layers added to the model of each contrast. +layersDetails : list + Indices of parameters in each layer. +customFiles : object + Iterable with custom file functions +modelType : str + The layer model type which can be 'standard layers', 'custom layers', or 'custom xy'. +contrastCustomFiles : np.ndarray[np.float] + Indices of CustomFiles used for each domain contrast +contrastDomainRatios : np.ndarray[np.float] + Indices of DomainRatios used for each domain contrast +domainRatios : np.ndarray[np.float] + Domain ratio values +numberOfDomainContrasts : int + Number of domain contrasts. +domainContrastLayers : list + Indices of layers added to the model of each domain contrast. +fitParams : np.ndarray[np.float] + Values of fitted parameters. +otherParams : np.ndarray[np.float] + Values of non-fitted parameters. +fitLimits : np.ndarray[np.float] + Limits of fitted parameters. +otherLimits : np.ndarray[np.float] + Limits of non fitted parameters. +priorNames : list + Parameter names for for all parameters in the problem definition. +priorValues : np.ndarray[np.float] + Prior type, mu, and sigma for all parameters in the problem definition. +names : RATapi.rat_core.NameStore + Names of all parameters. +checks : RATapi.rat_core.Checks + Flags indicating which parameters should be fitted. +)"; + +struct ProblemDefinition { + std::string TF {}; + py::array_t resample; + py::list data; + py::array_t dataPresent; + py::list dataLimits; + py::list simulationLimits; + py::array_t oilChiDataPresent; + real_T numberOfContrasts; + std::string geometry {}; + boolean_T useImaginary {}; + py::list repeatLayers; + py::list contrastBackgroundParams; + py::list contrastBackgroundTypes; + py::list contrastBackgroundActions; + py::array_t contrastQzshifts; + py::array_t contrastScalefactors; + py::array_t contrastBulkIns; + py::array_t contrastBulkOuts; + py::list contrastResolutionParams; + py::list contrastResolutionTypes; + py::array_t backgroundParams; + py::array_t qzshifts; + py::array_t scalefactors; + py::array_t bulkIns; + py::array_t bulkOuts; + py::array_t resolutionParams; + py::array_t params; + real_T numberOfLayers {}; + py::list contrastLayers; + py::list layersDetails; + py::object customFiles; + std::string modelType {}; + py::array_t contrastCustomFiles; + py::array_t contrastDomainRatios; + py::array_t domainRatios; + real_T numberOfDomainContrasts {}; + py::list domainContrastLayers; + py::array_t fitParams; + py::array_t otherParams; + py::array_t fitLimits; + py::array_t otherLimits; + py::list priorNames; + py::array_t priorValues; + NameStore names; + Checks checks {}; +}; + + +const std::string docsControl = R"(The Python binding for the C++ controls struct. + +Parameters +---------- +parallel : str + How the calculation should be parallelised (This uses the Parallel Computing Toolbox). Can be 'single', 'contrasts' or 'points'. +procedure : str + Which procedure RAT should execute. Can be 'calculate', 'simplex', 'de', 'ns', or 'dream'. +calcSldDuringFit : bool + Whether SLD will be calculated during fit (for live plotting etc.) +resampleMinAngle : float + The upper threshold on the angle between three sampled points for resampling, in units of radians over pi. +resampleNPoints : int + The number of initial points to use for resampling. +display : str + How much RAT should print to the terminal. Can be 'off', 'iter', 'notify', or 'final'. +IPCFilePath : str + The path of the inter process communication file. +updateFreq : int + [SIMPLEX, DE] Number of iterations between printing progress updates to the terminal. +updatePlotFreq : int + [SIMPLEX, DE] Number of iterations between updates to live plots. +xTolerance : float + [SIMPLEX] The termination tolerance for step size. +funcTolerance : float + [SIMPLEX] The termination tolerance for change in chi-squared. +maxFuncEvals : int + [SIMPLEX] The maximum number of function evaluations before the algorithm terminates. +maxIterations : int + [SIMPLEX] The maximum number of iterations before the algorithm terminates. +populationSize : int + [DE] The number of candidate solutions that exist at any time. +fWeight : float + [DE] The step size for how different mutations are to their parents. +crossoverProbability : float + [DE] The probability of exchange of parameters between individuals at any iteration. +strategy : int + [DE] The algorithm used to generate new candidates. +targetValue : float + [DE] The value of chi-squared at which the algorithm will terminate. +numGenerations : int + [DE] The maximum number of iterations before the algorithm terminates. +nLive : int + [NS] The number of points to sample. +nMCMC : int + [NS] If non-zero, an MCMC process with ``nMCMC`` chains will be used instead of MultiNest. +propScale : float + [NS] A scaling factor for the ellipsoid generated by MultiNest. +nsTolerance : float + [NS] The tolerance threshold for when the algorithm should terminate. +nSamples : int + [DREAM] The number of samples in the initial population for each chain. +nChains : int + [DREAM] The number of Markov chains to use in the algorithm. +jumpProbability : float + [DREAM] The probability range for the size of jumps in sampling. Larger values mean more variable jumps. +pUnitGamma : float + [DREAM] The probability that the scaling-down factor of jumps will be ignored and a larger jump will be taken. +boundHandling : str + [DREAM] How steps past the space boundaries should be handled. Can be 'off', 'reflect', 'bound', or 'fold'. +adaptPCR : bool + [DREAM] Whether the crossover probability for differential evolution should be adapted during the run. +)"; + +struct Control { + std::string parallel {}; + std::string procedure {}; + std::string display {}; + real_T xTolerance {}; + real_T funcTolerance {}; + real_T maxFuncEvals {}; + real_T maxIterations {}; + real_T populationSize {}; + real_T fWeight {}; + real_T crossoverProbability {}; + real_T targetValue {}; + real_T numGenerations {}; + real_T strategy {}; + real_T nLive {}; + real_T nMCMC {}; + real_T propScale {}; + real_T nsTolerance {}; + boolean_T calcSldDuringFit {}; + real_T resampleMinAngle {}; + real_T resampleNPoints {}; + real_T updateFreq {}; + real_T updatePlotFreq {}; + real_T nSamples {}; + real_T nChains {}; + real_T jumpProbability {}; + real_T pUnitGamma {}; + std::string boundHandling {}; + boolean_T adaptPCR; + std::string IPCFilePath {}; +}; + +#endif // RAT_DEFINES_H \ No newline at end of file diff --git a/cpp/includes/functions.h b/cpp/includes/functions.h new file mode 100644 index 00000000..3ced72fe --- /dev/null +++ b/cpp/includes/functions.h @@ -0,0 +1,500 @@ +#ifndef RAT_FUNCTIONS_H +#define RAT_FUNCTIONS_H + +#include +#include +#include +#include +#include "../RAT/classHandle.hpp" + +namespace py = pybind11; + +template +auto customCaller(std::string identifier, Function f, Args&& ... args) -> decltype((*f)(std::forward(args)...)) +{ + try + { + return (*f)(std::forward(args)...); + } + catch(const std::runtime_error& re) + { + std::string errorMsg; + size_t start_pos = std::string(re.what()).find("$id"); + if(start_pos == std::string::npos) + { + errorMsg = std::string("Error occurred when setting ") + identifier + ". " + re.what(); + } + else + { + errorMsg = re.what(); + errorMsg.replace(start_pos, 3, identifier); + } + + throw std::runtime_error(errorMsg); + } +} + +class Library: public CallbackInterface +{ + public: + + py::function function; + + Library(const py::function function){ + this->function = function; + }; + + + void setOutput(py::tuple& result, std::vector& output, double *outputSize) + { + int nRows = 0, idx = 0; + for (py::handle rowHandle : result[0]) + { + py::list rows = py::cast(rowHandle); + for (py::handle value : rows) + { + output.push_back(py::cast(value)); + idx++; + } + nRows++; + } + + outputSize[0] = nRows; + outputSize[1] = (nRows == 0) ? 0 : idx / nRows; + } + + // Backgrounds + void invoke(std::vector& xdata, std::vector& params, std::vector& output) + { + auto f = py::cast>(this->function); + auto result = f(py::cast(xdata), py::cast(params)); + for (py::handle rowHandle : result) + { + if (py::isinstance(rowHandle)) + output.push_back(py::cast(py::cast(rowHandle)[0])); + else + output.push_back(py::cast(rowHandle)); + + } + }; + + // Domain overload + void invoke(std::vector& params, std::vector& bulkIn, std::vector& bulkOut, + int contrast, int domainNumber, std::vector& output, double *outputSize, double *roughness) + { + auto f = py::cast>(this->function); + auto result = f(py::cast(params), py::cast(bulkIn), py::cast(bulkOut), contrast, domainNumber); + *roughness = py::cast(result[1]); + setOutput(result, output, outputSize); + }; + + // Non-Domain overload + void invoke(std::vector& params, std::vector& bulkIn, std::vector& bulkOut, + int contrast, std::vector& output, double *outputSize, double *roughness) + { + auto f = py::cast>(this->function); + auto result = f(py::cast(params), py::cast(bulkIn), py::cast(bulkOut), contrast); + *roughness = py::cast(result[1]); + setOutput(result, output, outputSize); + }; +}; + + +void stringToRatBoundedArray(std::string value, char_T result_data[], int32_T result_size[2]) +{ + result_size[0] = 1; + result_size[1] = value.length(); + + for (int32_T idx1{0}; idx1 < value.length(); idx1++) { + result_data[idx1] = value[idx1]; + } +} + +void stringToRatCharArray(std::string value, coder::array& result) +{ + result.set_size(1, value.length()); + + for (int32_T idx{0}; idx < value.length(); idx++) { + result[idx] = value[idx]; + } +} + +void stringFromRatBoundedArray(const char_T array_data[], const int32_T array_size[2], std::string& result) +{ + result.resize(array_size[1]); + memcpy(&result[0], array_data, array_size[1]); +} + + +coder::array pyArrayToRatRowArray1d(py::array_t value) +{ + coder::array result; + + py::buffer_info buffer_info = value.request(); + + if (buffer_info.size == 0) + return result; + + if (buffer_info.ndim != 1) + throw std::runtime_error("Expects a 1D numeric array"); + + result.set_size(1, buffer_info.shape[0]); + for (int32_T idx0{0}; idx0 < buffer_info.shape[0]; idx0++) { + result[idx0] = value.at(idx0); + } + + return result; +} + +coder::bounded_array pyArrayToRatBoundedArray(py::array_t value) +{ + coder::bounded_array result {}; + + py::buffer_info buffer_info = value.request(); + + if (buffer_info.size == 0) + return result; + + if (buffer_info.ndim != 1) + throw std::runtime_error("Expects a 1D numeric array"); + + result.size[0] = 1; + result.size[1] = buffer_info.shape[0]; + for (int32_T idx0{0}; idx0 < buffer_info.shape[0]; idx0++) { + result.data[idx0] = value.at(idx0); + } + + return result; +} + +coder::bounded_array pyArrayToRatBoundedArray3(py::array_t value) +{ + coder::bounded_array result {}; + + py::buffer_info buffer_info = value.request(); + + if (buffer_info.size == 0) + return result; + + if (buffer_info.ndim != 1) + throw std::runtime_error("Expects a 1D numeric array"); + + result.size[0] = 1; + result.size[1] = buffer_info.shape[0]; + for (int32_T idx0{0}; idx0 < buffer_info.shape[0]; idx0++) { + result.data[idx0] = value.at(idx0); + } + + return result; +} + +coder::array pyArrayToRatArray2d(py::array_t value) +{ + coder::array result; + + py::buffer_info buffer_info = value.request(); + + if (buffer_info.size == 0) + return result; + + if (buffer_info.ndim != 2) + throw std::runtime_error("Expects a 2D numeric array"); + + result.set_size(buffer_info.shape[0], buffer_info.shape[1]); + + int32_T idx {0}; + for (int32_T idx0{0}; idx0 < buffer_info.shape[0]; idx0++) { + for (int32_T idx1{0}; idx1 < buffer_info.shape[1]; idx1++) { + idx = idx0 + result.size(0) * idx1; + result[idx] = value.at(idx0, idx1); + } + } + + return result; +} + +coder::array pyListToRatCellWrap1(py::list values) +{ + coder::array result; + result.set_size(1, values.size()); + int32_T idx {0}; + for (py::handle array: values) + { + py::array_t casted_array = py::cast(array); + result[idx].f1 = customCaller("$id[" + std::to_string(idx) +"]", pyArrayToRatArray2d, casted_array); + idx++; + } + + return result; +} + +coder::array pyListToRatCellWrap2(py::list values) +{ + coder::array result; + result.set_size(1, values.size()); + int32_T idx {0}; + for (py::handle array: values) + { + py::array_t casted_array = py::cast(array); + if (casted_array.size() != 2) + throw std::runtime_error("Expects a 2D list where each row contains exactly 2 numbers"); + result[idx].f1[0] = casted_array.at(0); + result[idx].f1[1] = casted_array.at(1); + idx++; + } + + return result; +} + + +coder::array pyListToRatCellWrap3(py::list values) +{ + coder::array result; + result.set_size(1, values.size()); + int32_T idx {0}; + for (py::handle array: values) + { + py::array_t casted_array = py::cast(array); + result[idx].f1 = customCaller("$id[" + std::to_string(idx) +"]", pyArrayToRatBoundedArray3, casted_array); + idx++; + } + + return result; +} + +coder::array pyListToRatCellWrap4(py::list values) +{ + coder::array result; + result.set_size(1, values.size()); + int32_T idx {0}; + for (py::handle array: values) + { + py::array_t casted_array = py::cast(array); + result[idx].f1 = customCaller("$id[" + std::to_string(idx) +"]", pyArrayToRatBoundedArray3, casted_array); + idx++; + } + + return result; +} + +coder::array pyListToRatCellWrap5(py::list values) +{ + coder::array result; + result.set_size(1, values.size()); + int32_T idx {0}; + for (py::handle array: values) + { + py::array_t casted_array = py::cast(array); + result[idx].f1 = customCaller("$id[" + std::to_string(idx) +"]", pyArrayToRatRowArray1d, casted_array); + idx++; + } + + return result; +} + +coder::array pyListToRatCellWrap6(py::list values) +{ + coder::array result; + result.set_size(1, values.size()); + int32_T idx {0}; + for (py::handle array: values) + { + py::array_t casted_array = py::cast(array); + result[idx].f1 = customCaller("$id[" + std::to_string(idx) +"]", pyArrayToRatBoundedArray, casted_array); + idx++; + } + + return result; +} + +coder::array pyListToRatCellWrap01d(py::list values) +{ + coder::array result; + result.set_size(values.size()); + int32_T idx {0}; + for (py::handle array: values) + { + if (py::isinstance(array)) { + std::string name = py::cast(array); + stringToRatBoundedArray(name, result[idx].f1.data, result[idx].f1.size); + idx++; + } + else + throw std::runtime_error("Expects a 1D list of strings"); + } + + return result; +} + +coder::array pyListToRatCellWrap02d(py::list values) +{ + coder::array result; + result.set_size(1, values.size()); + int32_T idx {0}; + for (py::handle array: values) + { + if (py::isinstance(array)) { + std::string name = py::cast(array); + stringToRatBoundedArray(name, result[idx].f1.data, result[idx].f1.size); + idx++; + } + else + throw std::runtime_error("Expects a 1D list of strings"); + } + + return result; +} + +coder::array py_function_array_to_rat_cell_wrap_0(py::object values) +{ + auto handles = py::cast(values); + coder::array result; + result.set_size(1, handles.size()); + int32_T idx {0}; + for (py::handle array: handles) + { + auto func = py::cast(array); + std::string func_ptr = convertPtr2String(new Library(func)); + stringToRatBoundedArray(func_ptr, result[idx].f1.data, result[idx].f1.size); + idx++; + } + + return result; +} +template +py::array_t pyArrayFromRatArray1d(T array, bool isCol=true) +{ + auto size = isCol ? array.size(1) : array.size(0); + auto result_array = py::array_t(size); + std::memcpy(result_array.request().ptr, array.data(), result_array.nbytes()); + + return result_array; +} + +py::array_t pyArrayFromRatArray2d(coder::array array) +{ + auto result_array = py::array_t({array.size(0), array.size(1)}); + std::memcpy(result_array.request().ptr, array.data(), result_array.nbytes()); + + return result_array; +} + +py::list pyListFromRatCellWrap01d(coder::array values) +{ + py::list result; + for (int32_T idx0{0}; idx0 < values.size(0); idx0++) { + std::string tmp; + stringFromRatBoundedArray(values[idx0].f1.data, values[idx0].f1.size, tmp); + result.append(tmp); + } + + return result; +} + +py::list pyListFromRatCellWrap02d(coder::array values) +{ + py::list result; + for (int32_T idx0{0}; idx0 < values.size(1); idx0++) { + std::string tmp; + stringFromRatBoundedArray(values[idx0].f1.data, values[idx0].f1.size, tmp); + result.append(tmp); + } + + return result; +} + +py::list pyListFromRatCellWrap2(coder::array values) +{ + py::list result; + + for (int32_T idx0{0}; idx0 < values.size(1); idx0++) { + py::list inner = py::make_tuple(values[idx0].f1[0], values[idx0].f1[1]); + result.append(inner); + } + + return result; +} + +template +py::list pyList1DFromRatCellWrap2D(const T& values) +{ + py::list result; + + for (int32_T idx0{0}; idx0 < values.size(1); idx0++) { + result.append(pyArrayFromRatArray2d(values[idx0].f1)); + } + + return result; +} + +template +py::list pyList1DFromRatCellWrap1D(const T& values) +{ + py::list result; + + for (int32_T idx0{0}; idx0 < values.size(0); idx0++) { + result.append(pyArrayFromRatArray2d(values[idx0].f1)); + } + + return result; +} + +template +py::list pyList2dFromRatCellWrap(const T& values) +{ + py::list result; + int32_T idx {0}; + for (int32_T idx0{0}; idx0 < values.size(0); idx0++) { + py::list inner; + for (int32_T idx1{0}; idx1 < values.size(1); idx1++) { + idx = idx0 + values.size(0) * idx1; + inner.append(pyArrayFromRatArray2d(values[idx].f1)); + } + result.append(inner); + } + + return result; +} + +template +py::list pyListFromBoundedCellWrap(const T& values) +{ + py::list result; + + for (int32_T idx0{0}; idx0 < values.size(1); idx0++) { + auto array = py::array_t({values[idx0].f1.size[0]}); + std::memcpy(array.request().ptr, values[idx0].f1.data, array.nbytes()); + + result.append(array); + } + + return result; +} + +template +py::array_t pyArray1dFromBoundedArray(const T& array) +{ + auto result_array = py::array_t({array.size[0]}); + std::memcpy(result_array.request().ptr, array.data, result_array.nbytes()); + + return result_array; +} + +template +py::array_t pyArray2dFromBoundedArray(const T& array) +{ + auto result_array = py::array_t({array.size[0], array.size[1]}); + std::memcpy(result_array.request().ptr, array.data, result_array.nbytes()); + + return result_array; +} + +py::array_t pyArrayFromRatArray3d(coder::array array) +{ + auto result_array = py::array_t({array.size(0), array.size(1), array.size(2)}); + std::memcpy(result_array.request().ptr, array.data(), result_array.nbytes()); + + return result_array; +} + +#endif // RAT_FUNCTIONS_H \ No newline at end of file diff --git a/cpp/rat.cpp b/cpp/rat.cpp index f159927f..84c30243 100644 --- a/cpp/rat.cpp +++ b/cpp/rat.cpp @@ -17,103 +17,15 @@ setup_pybind11(cfg) #include "RAT/RATMain_terminate.h" #include "RAT/RATMain_types.h" #include "RAT/makeSLDProfileXY.h" -#include "RAT/classHandle.hpp" #include "RAT/dylib.hpp" #include "RAT/events/eventManager.h" +#include "includes/defines.h" +#include "includes/functions.h" namespace py = pybind11; const int DEFAULT_DOMAIN = -1; - -template -auto customCaller(std::string identifier, Function f, Args&& ... args) -> decltype((*f)(std::forward(args)...)) -{ - try - { - return (*f)(std::forward(args)...); - } - catch(const std::runtime_error& re) - { - std::string errorMsg; - size_t start_pos = std::string(re.what()).find("$id"); - if(start_pos == std::string::npos) - { - errorMsg = std::string("Error occurred when setting ") + identifier + ". " + re.what(); - } - else - { - errorMsg = re.what(); - errorMsg.replace(start_pos, 3, identifier); - } - - throw std::runtime_error(errorMsg); - } -} - -class Library: public CallbackInterface -{ - public: - - py::function function; - - Library(const py::function function){ - this->function = function; - }; - - - void setOutput(py::tuple& result, std::vector& output, double *outputSize) - { - int nRows = 0, idx = 0; - for (py::handle rowHandle : result[0]) - { - py::list rows = py::cast(rowHandle); - for (py::handle value : rows) - { - output.push_back(py::cast(value)); - idx++; - } - nRows++; - } - - outputSize[0] = nRows; - outputSize[1] = (nRows == 0) ? 0 : idx / nRows; - } - - // Backgrounds - void invoke(std::vector& xdata, std::vector& params, std::vector& output) - { - auto f = py::cast>(this->function); - auto result = f(py::cast(xdata), py::cast(params)); - for (py::handle rowHandle : result) - { - if (py::isinstance(rowHandle)) - output.push_back(py::cast(py::cast(rowHandle)[0])); - else - output.push_back(py::cast(rowHandle)); - - } - }; - - // Domain overload - void invoke(std::vector& params, std::vector& bulkIn, std::vector& bulkOut, - int contrast, int domainNumber, std::vector& output, double *outputSize, double *roughness) - { - auto f = py::cast>(this->function); - auto result = f(py::cast(params), py::cast(bulkIn), py::cast(bulkOut), contrast, domainNumber); - *roughness = py::cast(result[1]); - setOutput(result, output, outputSize); - }; - - // Non-Domain overload - void invoke(std::vector& params, std::vector& bulkIn, std::vector& bulkOut, - int contrast, std::vector& output, double *outputSize, double *roughness) - { - auto f = py::cast>(this->function); - auto result = f(py::cast(params), py::cast(bulkIn), py::cast(bulkOut), contrast); - *roughness = py::cast(result[1]); - setOutput(result, output, outputSize); - }; -}; +const int DEFAULT_NREPEATS = 1; class DylibEngine { @@ -185,25 +97,6 @@ class DylibEngine }; }; -struct ProgressEventData -{ - std::string message; - double percent; -}; - -struct PlotEventData -{ - py::list reflectivity; - py::list shiftedData; - py::list sldProfiles; - py::list resampledLayers; - py::array_t subRoughs; - py::array_t resample; - py::array_t dataPresent; - std::string modelType; - py::list contrastNames; -}; - class EventBridge { public: @@ -328,480 +221,6 @@ class EventBridge }; }; -struct PredictionIntervals -{ - py::list reflectivity; - py::list sld; - py::array_t sampleChi; -}; - -struct ConfidenceIntervals -{ - py::array_t percentile95; - py::array_t percentile65; - py::array_t mean; -}; - -struct NestedSamplerOutput -{ - real_T logZ; - real_T logZErr; - py::array_t nestSamples; - py::array_t postSamples; -}; - -struct DreamParams -{ - real_T nParams; - real_T nChains; - real_T nGenerations; - boolean_T parallel; - real_T CPU; - real_T jumpProbability; - real_T pUnitGamma; - real_T nCR; - real_T delta; - real_T steps; - real_T zeta; - std::string outlier; - boolean_T adaptPCR; - real_T thinning; - real_T epsilon; - boolean_T ABC; - boolean_T IO; - boolean_T storeOutput; - py::array_t R; -}; - -struct DreamOutput -{ - py::array_t allChains; - py::array_t outlierChains; - real_T runtime; - real_T iteration; - real_T modelOutput; - py::array_t AR; - py::array_t R_stat; - py::array_t CR; -}; - -struct BayesResults -{ - PredictionIntervals predictionIntervals; - ConfidenceIntervals confidenceIntervals; - DreamParams dreamParams; - DreamOutput dreamOutput; - NestedSamplerOutput nestedSamplerOutput; - py::array_t chain; -}; - -struct Checks { - py::array_t params; - py::array_t backgroundParams; - py::array_t qzshifts; - py::array_t scalefactors; - py::array_t bulkIns; - py::array_t bulkOuts; - py::array_t resolutionParams; - py::array_t domainRatios; -}; - -struct Calculation -{ - py::array_t chiValues; - real_T sumChi; -}; - -struct ContrastParams -{ - py::array_t scalefactors; - py::array_t bulkIn; - py::array_t bulkOut; - py::array_t subRoughs; - py::array_t resample; -}; - -struct OutputResult { - py::list reflectivity; - py::list simulation; - py::list shiftedData; - py::list backgrounds; - py::list resolutions; - py::list layerSlds; - py::list sldProfiles; - py::list resampledLayers; - Calculation calculationResults {}; - ContrastParams contrastParams {}; - py::array_t fitParams; - py::list fitNames; -}; - -struct Limits { - py::array_t params; - py::array_t backgroundParams; - py::array_t scalefactors; - py::array_t qzshifts; - py::array_t bulkIns; - py::array_t bulkOuts; - py::array_t resolutionParams; - py::array_t domainRatios; -}; - -struct NameStore { - py::list params; - py::list backgroundParams; - py::list scalefactors; - py::list qzshifts; - py::list bulkIns; - py::list bulkOuts; - py::list resolutionParams; - py::list domainRatios; - py::list contrasts; -}; - -struct ProblemDefinition { - std::string TF {}; - py::array_t resample; - py::list data; - py::array_t dataPresent; - py::list dataLimits; - py::list simulationLimits; - py::array_t oilChiDataPresent; - real_T numberOfContrasts; - std::string geometry {}; - boolean_T useImaginary {}; - py::list repeatLayers; - py::list contrastBackgroundParams; - py::list contrastBackgroundTypes; - py::list contrastBackgroundActions; - py::array_t contrastQzshifts; - py::array_t contrastScalefactors; - py::array_t contrastBulkIns; - py::array_t contrastBulkOuts; - py::list contrastResolutionParams; - py::list contrastResolutionTypes; - py::array_t backgroundParams; - py::array_t qzshifts; - py::array_t scalefactors; - py::array_t bulkIns; - py::array_t bulkOuts; - py::array_t resolutionParams; - py::array_t params; - real_T numberOfLayers {}; - py::list contrastLayers; - py::list layersDetails; - py::object customFiles; - std::string modelType {}; - py::array_t contrastCustomFiles; - py::array_t contrastDomainRatios; - py::array_t domainRatios; - real_T numberOfDomainContrasts {}; - py::list domainContrastLayers; - py::array_t fitParams; - py::array_t otherParams; - py::array_t fitLimits; - py::array_t otherLimits; - py::list priorNames; - py::array_t priorValues; - NameStore names; - Checks checks {}; -}; - -struct Control { - std::string parallel {}; - std::string procedure {}; - std::string display {}; - real_T xTolerance {}; - real_T funcTolerance {}; - real_T maxFuncEvals {}; - real_T maxIterations {}; - real_T populationSize {}; - real_T fWeight {}; - real_T crossoverProbability {}; - real_T targetValue {}; - real_T numGenerations {}; - real_T strategy {}; - real_T nLive {}; - real_T nMCMC {}; - real_T propScale {}; - real_T nsTolerance {}; - boolean_T calcSldDuringFit {}; - real_T resampleMinAngle {}; - real_T resampleNPoints {}; - real_T updateFreq {}; - real_T updatePlotFreq {}; - real_T nSamples {}; - real_T nChains {}; - real_T jumpProbability {}; - real_T pUnitGamma {}; - std::string boundHandling {}; - boolean_T adaptPCR; - std::string IPCFilePath {}; -}; - - -void stringToRatBoundedArray(std::string value, char_T result_data[], int32_T result_size[2]) -{ - result_size[0] = 1; - result_size[1] = value.length(); - - for (int32_T idx1{0}; idx1 < value.length(); idx1++) { - result_data[idx1] = value[idx1]; - } -} - -void stringToRatCharArray(std::string value, coder::array& result) -{ - result.set_size(1, value.length()); - - for (int32_T idx{0}; idx < value.length(); idx++) { - result[idx] = value[idx]; - } -} - -void stringFromRatBoundedArray(const char_T array_data[], const int32_T array_size[2], std::string& result) -{ - result.resize(array_size[1]); - memcpy(&result[0], array_data, array_size[1]); -} - - -coder::array pyArrayToRatRowArray1d(py::array_t value) -{ - coder::array result; - - py::buffer_info buffer_info = value.request(); - - if (buffer_info.size == 0) - return result; - - if (buffer_info.ndim != 1) - throw std::runtime_error("Expects a 1D numeric array"); - - result.set_size(1, buffer_info.shape[0]); - for (int32_T idx0{0}; idx0 < buffer_info.shape[0]; idx0++) { - result[idx0] = value.at(idx0); - } - - return result; -} - -coder::bounded_array pyArrayToRatBoundedArray(py::array_t value) -{ - coder::bounded_array result {}; - - py::buffer_info buffer_info = value.request(); - - if (buffer_info.size == 0) - return result; - - if (buffer_info.ndim != 1) - throw std::runtime_error("Expects a 1D numeric array"); - - result.size[0] = 1; - result.size[1] = buffer_info.shape[0]; - for (int32_T idx0{0}; idx0 < buffer_info.shape[0]; idx0++) { - result.data[idx0] = value.at(idx0); - } - - return result; -} - -coder::bounded_array pyArrayToRatBoundedArray3(py::array_t value) -{ - coder::bounded_array result {}; - - py::buffer_info buffer_info = value.request(); - - if (buffer_info.size == 0) - return result; - - if (buffer_info.ndim != 1) - throw std::runtime_error("Expects a 1D numeric array"); - - result.size[0] = 1; - result.size[1] = buffer_info.shape[0]; - for (int32_T idx0{0}; idx0 < buffer_info.shape[0]; idx0++) { - result.data[idx0] = value.at(idx0); - } - - return result; -} - -coder::array pyArrayToRatArray2d(py::array_t value) -{ - coder::array result; - - py::buffer_info buffer_info = value.request(); - - if (buffer_info.size == 0) - return result; - - if (buffer_info.ndim != 2) - throw std::runtime_error("Expects a 2D numeric array"); - - result.set_size(buffer_info.shape[0], buffer_info.shape[1]); - - int32_T idx {0}; - for (int32_T idx0{0}; idx0 < buffer_info.shape[0]; idx0++) { - for (int32_T idx1{0}; idx1 < buffer_info.shape[1]; idx1++) { - idx = idx0 + result.size(0) * idx1; - result[idx] = value.at(idx0, idx1); - } - } - - return result; -} - -coder::array pyListToRatCellWrap1(py::list values) -{ - coder::array result; - result.set_size(1, values.size()); - int32_T idx {0}; - for (py::handle array: values) - { - py::array_t casted_array = py::cast(array); - result[idx].f1 = customCaller("$id[" + std::to_string(idx) +"]", pyArrayToRatArray2d, casted_array); - idx++; - } - - return result; -} - -coder::array pyListToRatCellWrap2(py::list values) -{ - coder::array result; - result.set_size(1, values.size()); - int32_T idx {0}; - for (py::handle array: values) - { - py::array_t casted_array = py::cast(array); - if (casted_array.size() != 2) - throw std::runtime_error("Expects a 2D list where each row contains exactly 2 numbers"); - result[idx].f1[0] = casted_array.at(0); - result[idx].f1[1] = casted_array.at(1); - idx++; - } - - return result; -} - - -coder::array pyListToRatCellWrap3(py::list values) -{ - coder::array result; - result.set_size(1, values.size()); - int32_T idx {0}; - for (py::handle array: values) - { - py::array_t casted_array = py::cast(array); - result[idx].f1 = customCaller("$id[" + std::to_string(idx) +"]", pyArrayToRatBoundedArray3, casted_array); - idx++; - } - - return result; -} - -coder::array pyListToRatCellWrap4(py::list values) -{ - coder::array result; - result.set_size(1, values.size()); - int32_T idx {0}; - for (py::handle array: values) - { - py::array_t casted_array = py::cast(array); - result[idx].f1 = customCaller("$id[" + std::to_string(idx) +"]", pyArrayToRatBoundedArray3, casted_array); - idx++; - } - - return result; -} - -coder::array pyListToRatCellWrap5(py::list values) -{ - coder::array result; - result.set_size(1, values.size()); - int32_T idx {0}; - for (py::handle array: values) - { - py::array_t casted_array = py::cast(array); - result[idx].f1 = customCaller("$id[" + std::to_string(idx) +"]", pyArrayToRatRowArray1d, casted_array); - idx++; - } - - return result; -} - -coder::array pyListToRatCellWrap6(py::list values) -{ - coder::array result; - result.set_size(1, values.size()); - int32_T idx {0}; - for (py::handle array: values) - { - py::array_t casted_array = py::cast(array); - result[idx].f1 = customCaller("$id[" + std::to_string(idx) +"]", pyArrayToRatBoundedArray, casted_array); - idx++; - } - - return result; -} - -coder::array pyListToRatCellWrap01d(py::list values) -{ - coder::array result; - result.set_size(values.size()); - int32_T idx {0}; - for (py::handle array: values) - { - if (py::isinstance(array)) { - std::string name = py::cast(array); - stringToRatBoundedArray(name, result[idx].f1.data, result[idx].f1.size); - idx++; - } - else - throw std::runtime_error("Expects a 1D list of strings"); - } - - return result; -} - -coder::array pyListToRatCellWrap02d(py::list values) -{ - coder::array result; - result.set_size(1, values.size()); - int32_T idx {0}; - for (py::handle array: values) - { - if (py::isinstance(array)) { - std::string name = py::cast(array); - stringToRatBoundedArray(name, result[idx].f1.data, result[idx].f1.size); - idx++; - } - else - throw std::runtime_error("Expects a 1D list of strings"); - } - - return result; -} - -coder::array py_function_array_to_rat_cell_wrap_0(py::object values) -{ - auto handles = py::cast(values); - coder::array result; - result.set_size(1, handles.size()); - int32_T idx {0}; - for (py::handle array: handles) - { - auto func = py::cast(array); - std::string func_ptr = convertPtr2String(new Library(func)); - stringToRatBoundedArray(func_ptr, result[idx].f1.data, result[idx].f1.size); - idx++; - } - - return result; -} - RAT::struct1_T createStruct1(const NameStore& names) { RAT::struct1_T names_struct; @@ -941,143 +360,6 @@ RAT::struct4_T createStruct4(const Control& control) return control_struct; } - -template -py::array_t pyArrayFromRatArray1d(T array, bool isCol=true) -{ - auto size = isCol ? array.size(1) : array.size(0); - auto result_array = py::array_t(size); - std::memcpy(result_array.request().ptr, array.data(), result_array.nbytes()); - - return result_array; -} - -py::array_t pyArrayFromRatArray2d(coder::array array) -{ - auto result_array = py::array_t({array.size(0), array.size(1)}); - std::memcpy(result_array.request().ptr, array.data(), result_array.nbytes()); - - return result_array; -} - -py::list pyListFromRatCellWrap01d(coder::array values) -{ - py::list result; - for (int32_T idx0{0}; idx0 < values.size(0); idx0++) { - std::string tmp; - stringFromRatBoundedArray(values[idx0].f1.data, values[idx0].f1.size, tmp); - result.append(tmp); - } - - return result; -} - -py::list pyListFromRatCellWrap02d(coder::array values) -{ - py::list result; - for (int32_T idx0{0}; idx0 < values.size(1); idx0++) { - std::string tmp; - stringFromRatBoundedArray(values[idx0].f1.data, values[idx0].f1.size, tmp); - result.append(tmp); - } - - return result; -} - -py::list pyListFromRatCellWrap2(coder::array values) -{ - py::list result; - - for (int32_T idx0{0}; idx0 < values.size(1); idx0++) { - py::list inner = py::make_tuple(values[idx0].f1[0], values[idx0].f1[1]); - result.append(inner); - } - - return result; -} - -template -py::list pyList1DFromRatCellWrap2D(const T& values) -{ - py::list result; - - for (int32_T idx0{0}; idx0 < values.size(1); idx0++) { - result.append(pyArrayFromRatArray2d(values[idx0].f1)); - } - - return result; -} - -template -py::list pyList1DFromRatCellWrap1D(const T& values) -{ - py::list result; - - for (int32_T idx0{0}; idx0 < values.size(0); idx0++) { - result.append(pyArrayFromRatArray2d(values[idx0].f1)); - } - - return result; -} - -template -py::list pyList2dFromRatCellWrap(const T& values) -{ - py::list result; - int32_T idx {0}; - for (int32_T idx0{0}; idx0 < values.size(0); idx0++) { - py::list inner; - for (int32_T idx1{0}; idx1 < values.size(1); idx1++) { - idx = idx0 + values.size(0) * idx1; - inner.append(pyArrayFromRatArray2d(values[idx].f1)); - } - result.append(inner); - } - - return result; -} - -template -py::list pyListFromBoundedCellWrap(const T& values) -{ - py::list result; - - for (int32_T idx0{0}; idx0 < values.size(1); idx0++) { - auto array = py::array_t({values[idx0].f1.size[0]}); - std::memcpy(array.request().ptr, values[idx0].f1.data, array.nbytes()); - - result.append(array); - } - - return result; -} - -template -py::array_t pyArray1dFromBoundedArray(const T& array) -{ - auto result_array = py::array_t({array.size[0]}); - std::memcpy(result_array.request().ptr, array.data, result_array.nbytes()); - - return result_array; -} - -template -py::array_t pyArray2dFromBoundedArray(const T& array) -{ - auto result_array = py::array_t({array.size[0], array.size[1]}); - std::memcpy(result_array.request().ptr, array.data, result_array.nbytes()); - - return result_array; -} - -py::array_t pyArrayFromRatArray3d(coder::array array) -{ - auto result_array = py::array_t({array.size(0), array.size(1), array.size(2)}); - std::memcpy(result_array.request().ptr, array.data(), result_array.nbytes()); - - return result_array; -} - OutputResult OutputResultFromStruct5T(const RAT::struct5_T result) { // Copy problem to output @@ -1309,6 +591,27 @@ BayesResults bayesResultsFromStruct8T(const RAT::struct8_T results) return bayesResults; } +const std::string docsRATMain = R"(Entry point for the main reflectivity computation. + +Parameters +---------- +problem_def : Rat.rat_core.ProblemDefinition + The project input for the RAT calculation. +limits : RATapi.rat_core.Limits + Min and max values for each parameter defined in the problem definition. +control : RATapi.rat_core.Control + The controls object for the RAT calculation. + +Returns +------- +out_problem_def : Rat.rat_core.ProblemDefinition + The project input with the updated fit values. +results : Rat.rat_core.OutputResult + The results from a RAT calculation. +bayes_result : Rat.rat_core.BayesResults + The extra results if RAT calculation is Bayesian. +)"; + py::tuple RATMain(const ProblemDefinition& problem_def, const Limits& limits, const Control& control) { RAT::struct0_T problem_def_struct = createStruct0(problem_def); @@ -1327,21 +630,42 @@ py::tuple RATMain(const ProblemDefinition& problem_def, const Limits& limits, co bayesResultsFromStruct8T(bayesResults)); } +const std::string docsMakeSLDProfileXY = R"(Creates the profiles for the SLD plots + +Parameters +---------- +bulk_in : float + Bulk in value for contrast. +bulk_out : float + Bulk out value for contrast. +ssub : float + Substrate roughness. +layers : np.ndarray[np.float] + Array of parameters for each layer in the contrast. +number_of_repeats : int, default: 1 + Number of times the layers are repeated. + +Returns +------- +sld_profile : np.ndarray[np.float] + Computed SLD profile +)"; + py::array_t makeSLDProfileXY(real_T bulk_in, real_T bulk_out, real_T ssub, const py::array_t &layers, - real_T number_of_layers, - real_T repeats) + int number_of_repeats=DEFAULT_NREPEATS) { coder::array out; coder::array layers_array = pyArrayToRatArray2d(layers); + py::buffer_info buffer_info = layers.request(); RAT::makeSLDProfileXY(bulk_in, bulk_out, ssub, layers_array, - number_of_layers, - repeats, + buffer_info.shape[0], + number_of_repeats, out); return pyArrayFromRatArray2d(out); @@ -1387,13 +711,13 @@ PYBIND11_MODULE(rat_core, m) { .def("invoke", overload_cast_&, std::vector&>()(&DylibEngine::invoke), py::arg("xdata"), py::arg("param")); - py::class_(m, "PredictionIntervals") + py::class_(m, "PredictionIntervals", docsPredictionIntervals.c_str()) .def(py::init<>()) .def_readwrite("reflectivity", &PredictionIntervals::reflectivity) .def_readwrite("sld", &PredictionIntervals::sld) .def_readwrite("sampleChi", &PredictionIntervals::sampleChi); - py::class_(m, "PlotEventData") + py::class_(m, "PlotEventData", docsPlotEventData.c_str()) .def(py::init<>()) .def_readwrite("reflectivity", &PlotEventData::reflectivity) .def_readwrite("shiftedData", &PlotEventData::shiftedData) @@ -1430,7 +754,7 @@ PYBIND11_MODULE(rat_core, m) { return evt; })); - py::class_(m, "ProgressEventData") + py::class_(m, "ProgressEventData", docsProgressEventData.c_str()) .def(py::init<>()) .def_readwrite("message", &ProgressEventData::message) .def_readwrite("percent", &ProgressEventData::percent) @@ -1452,7 +776,7 @@ PYBIND11_MODULE(rat_core, m) { return evt; })); - py::class_(m, "ConfidenceIntervals") + py::class_(m, "ConfidenceIntervals", docsConfidenceIntervals.c_str()) .def(py::init<>()) .def_readwrite("percentile95", &ConfidenceIntervals::percentile95) .def_readwrite("percentile65", &ConfidenceIntervals::percentile65) @@ -1480,14 +804,14 @@ PYBIND11_MODULE(rat_core, m) { .def_readwrite("storeOutput", &DreamParams::storeOutput) .def_readwrite("R", &DreamParams::R); - py::class_(m, "NestedSamplerOutput") + py::class_(m, "NestedSamplerOutput", docsNestedSamplerOutput.c_str()) .def(py::init<>()) .def_readwrite("logZ", &NestedSamplerOutput::logZ) .def_readwrite("logZErr", &NestedSamplerOutput::logZErr) .def_readwrite("nestSamples", &NestedSamplerOutput::nestSamples) .def_readwrite("postSamples", &NestedSamplerOutput::postSamples); - py::class_(m, "DreamOutput") + py::class_(m, "DreamOutput", docsDreamOutput.c_str()) .def(py::init<>()) .def_readwrite("allChains", &DreamOutput::allChains) .def_readwrite("outlierChains", &DreamOutput::outlierChains) @@ -1498,7 +822,7 @@ PYBIND11_MODULE(rat_core, m) { .def_readwrite("R_stat", &DreamOutput::R_stat) .def_readwrite("CR", &DreamOutput::CR); - py::class_(m, "BayesResults") + py::class_(m, "BayesResults", docsBayesResults.c_str()) .def(py::init<>()) .def_readwrite("predictionIntervals", &BayesResults::predictionIntervals) .def_readwrite("confidenceIntervals", &BayesResults::confidenceIntervals) @@ -1507,12 +831,12 @@ PYBIND11_MODULE(rat_core, m) { .def_readwrite("nestedSamplerOutput", &BayesResults::nestedSamplerOutput) .def_readwrite("chain", &BayesResults::chain); - py::class_(m, "Calculation") + py::class_(m, "Calculation", docsCalculation.c_str()) .def(py::init<>()) .def_readwrite("chiValues", &Calculation::chiValues) .def_readwrite("sumChi", &Calculation::sumChi); - py::class_(m, "ContrastParams") + py::class_(m, "ContrastParams", docsContrastParams.c_str()) .def(py::init<>()) .def_readwrite("scalefactors", &ContrastParams::scalefactors) .def_readwrite("bulkIn", &ContrastParams::bulkIn) @@ -1520,7 +844,7 @@ PYBIND11_MODULE(rat_core, m) { .def_readwrite("subRoughs", &ContrastParams::subRoughs) .def_readwrite("resample", &ContrastParams::resample); - py::class_(m, "OutputResult") + py::class_(m, "OutputResult", docsOutputResult.c_str()) .def(py::init<>()) .def_readwrite("reflectivity", &OutputResult::reflectivity) .def_readwrite("simulation", &OutputResult::simulation) @@ -1535,7 +859,7 @@ PYBIND11_MODULE(rat_core, m) { .def_readwrite("fitParams", &OutputResult::fitParams) .def_readwrite("fitNames", &OutputResult::fitNames); - py::class_(m, "NameStore") + py::class_(m, "NameStore", docsNameStore.c_str()) .def(py::init<>()) .def_readwrite("params", &NameStore::params) .def_readwrite("backgroundParams", &NameStore::backgroundParams) @@ -1572,7 +896,7 @@ PYBIND11_MODULE(rat_core, m) { return names; })); - py::class_(m, "Checks") + py::class_(m, "Checks", docsChecks.c_str()) .def(py::init<>()) .def_readwrite("params", &Checks::params) .def_readwrite("backgroundParams", &Checks::backgroundParams) @@ -1607,7 +931,7 @@ PYBIND11_MODULE(rat_core, m) { return chk; })); - py::class_(m, "Limits") + py::class_(m, "Limits", docsLimits.c_str()) .def(py::init<>()) .def_readwrite("params", &Limits::params) .def_readwrite("backgroundParams", &Limits::backgroundParams) @@ -1642,7 +966,7 @@ PYBIND11_MODULE(rat_core, m) { return lim; })); - py::class_(m, "Control") + py::class_(m, "Control", docsControl.c_str()) .def(py::init<>()) .def_readwrite("parallel", &Control::parallel) .def_readwrite("procedure", &Control::procedure) @@ -1723,7 +1047,7 @@ PYBIND11_MODULE(rat_core, m) { return ctrl; })); - py::class_(m, "ProblemDefinition") + py::class_(m, "ProblemDefinition", docsProblemDefinition.c_str()) .def(py::init<>()) .def_readwrite("TF", &ProblemDefinition::TF) .def_readwrite("resample", &ProblemDefinition::resample) @@ -1859,7 +1183,8 @@ PYBIND11_MODULE(rat_core, m) { return p; })); - m.def("RATMain", &RATMain, "Entry point for the main reflectivity computation."); + m.def("RATMain", &RATMain, docsRATMain.c_str(), py::arg("problem_def"), py::arg("limits"), py::arg("control")); - m.def("makeSLDProfileXY", &makeSLDProfileXY, "Creates the profiles for the SLD plots"); + m.def("makeSLDProfileXY", &makeSLDProfileXY, docsMakeSLDProfileXY.c_str(), + py::arg("bulk_in"), py::arg("bulk_out"), py::arg("ssub"), py::arg("layers"), py::arg("number_of_repeats") = DEFAULT_NREPEATS); } diff --git a/setup.py b/setup.py index a21faae8..84e7c9ce 100644 --- a/setup.py +++ b/setup.py @@ -26,6 +26,7 @@ pybind11.get_include(), pybind11.get_include(True), "cpp/RAT/", + "cpp/includes/", ], language="c++", ), diff --git a/tests/test_plotting.py b/tests/test_plotting.py index a1954834..78ddd08c 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -73,8 +73,8 @@ def bayes_fig(request) -> plt.figure: @pytest.mark.parametrize("fig", [False, True], indirect=True) -def test_figure_axis_formating(fig: plt.figure) -> None: - """Tests the axis formating of the figure.""" +def test_figure_axis_formatting(fig: plt.figure) -> None: + """Tests the axis formatting of the figure.""" ref_plot = fig.axes[0] sld_plot = fig.axes[1] @@ -151,20 +151,17 @@ def test_sld_profile_function_call(mock: MagicMock) -> None: assert mock.call_args_list[0].args[0] == 2.07e-06 assert mock.call_args_list[0].args[1] == 6.28e-06 assert mock.call_args_list[0].args[2] == 0.0 - assert mock.call_args_list[0].args[4] == 82 - assert mock.call_args_list[0].args[5] == 1.0 + assert mock.call_args_list[0].args[4] == 1 assert mock.call_args_list[1].args[0] == 2.07e-06 assert mock.call_args_list[1].args[1] == 1.83e-06 assert mock.call_args_list[1].args[2] == 0.0 - assert mock.call_args_list[1].args[4] == 128 - assert mock.call_args_list[1].args[5] == 1.0 + assert mock.call_args_list[1].args[4] == 1 assert mock.call_args_list[2].args[0] == 2.07e-06 assert mock.call_args_list[2].args[1] == -5.87e-07 assert mock.call_args_list[2].args[2] == 0.0 - assert mock.call_args_list[2].args[4] == 153 - assert mock.call_args_list[2].args[5] == 1.0 + assert mock.call_args_list[2].args[4] == 1 @patch("RATapi.utils.plotting.makeSLDProfileXY") @@ -181,20 +178,17 @@ def test_live_plot(mock: MagicMock) -> None: assert mock.call_args_list[0].args[0] == 2.07e-06 assert mock.call_args_list[0].args[1] == 6.28e-06 assert mock.call_args_list[0].args[2] == 0.0 - assert mock.call_args_list[0].args[4] == 82 - assert mock.call_args_list[0].args[5] == 1.0 + assert mock.call_args_list[0].args[4] == 1 assert mock.call_args_list[1].args[0] == 2.07e-06 assert mock.call_args_list[1].args[1] == 1.83e-06 assert mock.call_args_list[1].args[2] == 0.0 - assert mock.call_args_list[1].args[4] == 128 - assert mock.call_args_list[1].args[5] == 1.0 + assert mock.call_args_list[1].args[4] == 1 assert mock.call_args_list[2].args[0] == 2.07e-06 assert mock.call_args_list[2].args[1] == -5.87e-07 assert mock.call_args_list[2].args[2] == 0.0 - assert mock.call_args_list[2].args[4] == 153 - assert mock.call_args_list[2].args[5] == 1.0 + assert mock.call_args_list[2].args[4] == 1 @patch("RATapi.utils.plotting.plot_ref_sld_helper") diff --git a/tests/test_wrappers.py b/tests/test_wrappers.py index a50127e4..2ced5ad0 100644 --- a/tests/test_wrappers.py +++ b/tests/test_wrappers.py @@ -27,7 +27,7 @@ def test_matlab_wrapper() -> None: mocked_engine.demo.return_value = ([2], 5) result = handle([1], [2], [3], 0) assert result == ([2], 5) - assert wrapper.engine.demo.call_args[0] == ([1], [2], [3], 1, -1) + assert wrapper.engine.demo.call_args[0] == ([1], [2], [3], 1) mocked_engine.demo.assert_called_once() mocked_engine.demo.return_value = ([3, 1], 7) From abded50cb12894c7c92da5246e5d26c9c6d562dd Mon Sep 17 00:00:00 2001 From: StephenNneji <34302892+StephenNneji@users.noreply.github.com> Date: Tue, 25 Feb 2025 09:51:49 +0000 Subject: [PATCH 05/20] Updates cpp with new c struct names (#136) --- cpp/RAT | 2 +- cpp/rat.cpp | 46 +++++++++++++++++++++++----------------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/cpp/RAT b/cpp/RAT index b46dd2d1..7effee9d 160000 --- a/cpp/RAT +++ b/cpp/RAT @@ -1 +1 @@ -Subproject commit b46dd2d1e5c76669ee980f88144314c851aee3e4 +Subproject commit 7effee9dc3190ff064187bf69d7c66f3ee828622 diff --git a/cpp/rat.cpp b/cpp/rat.cpp index 84c30243..039be9de 100644 --- a/cpp/rat.cpp +++ b/cpp/rat.cpp @@ -221,9 +221,9 @@ class EventBridge }; }; -RAT::struct1_T createStruct1(const NameStore& names) +RAT::b_ParamNames createParamNamesStruct(const NameStore& names) { - RAT::struct1_T names_struct; + RAT::b_ParamNames names_struct; names_struct.params = customCaller("NameStore.params", pyListToRatCellWrap02d, names.params); names_struct.backgroundParams = customCaller("NameStore.backgroundParams", pyListToRatCellWrap02d, names.backgroundParams); names_struct.scalefactors = customCaller("NameStore.scalefactors", pyListToRatCellWrap02d, names.scalefactors); @@ -237,9 +237,9 @@ RAT::struct1_T createStruct1(const NameStore& names) return names_struct; } -RAT::struct2_T createStruct2(const Checks& checks) +RAT::CheckFlags createCheckFlagsStruct(const Checks& checks) { - RAT::struct2_T checks_struct; + RAT::CheckFlags checks_struct; checks_struct.params = customCaller("Checks.params", pyArrayToRatRowArray1d, checks.params); checks_struct.backgroundParams = customCaller("Checks.backgroundParams", pyArrayToRatRowArray1d, checks.backgroundParams); checks_struct.scalefactors = customCaller("Checks.scalefactors", pyArrayToRatRowArray1d, checks.scalefactors); @@ -252,9 +252,9 @@ RAT::struct2_T createStruct2(const Checks& checks) return checks_struct; } -RAT::struct0_T createStruct0(const ProblemDefinition& problem) +RAT::b_ProblemDefinition createProblemDefinitionStruct(const ProblemDefinition& problem) { - RAT::struct0_T problem_struct; + RAT::b_ProblemDefinition problem_struct; stringToRatBoundedArray(problem.TF, problem_struct.TF.data, problem_struct.TF.size); problem_struct.resample = customCaller("Problem.resample", pyArrayToRatRowArray1d, problem.resample); @@ -300,17 +300,17 @@ RAT::struct0_T createStruct0(const ProblemDefinition& problem) problem_struct.priorNames = customCaller("Problem.priorNames", pyListToRatCellWrap01d, problem.priorNames); problem_struct.priorValues = customCaller("Problem.priorValues", pyArrayToRatArray2d, problem.priorValues); - problem_struct.names = createStruct1(problem.names); - problem_struct.checks = createStruct2(problem.checks); + problem_struct.names = createParamNamesStruct(problem.names); + problem_struct.checks = createCheckFlagsStruct(problem.checks); return problem_struct; } -RAT::struct3_T createStruct3(const Limits& limits) +RAT::ProblemLimits createProblemLimitsStruct(const Limits& limits) { - RAT::struct3_T limits_struct; + RAT::ProblemLimits limits_struct; limits_struct.params = customCaller("Limits.params", pyArrayToRatArray2d, limits.params); limits_struct.backgroundParams = customCaller("Limits.backgroundParams", pyArrayToRatArray2d, limits.backgroundParams); limits_struct.scalefactors = customCaller("Limits.scalefactors", pyArrayToRatArray2d, limits.scalefactors); @@ -324,9 +324,9 @@ RAT::struct3_T createStruct3(const Limits& limits) } -RAT::struct4_T createStruct4(const Control& control) +RAT::Controls createControlsStruct(const Control& control) { - RAT::struct4_T control_struct; + RAT::Controls control_struct; control_struct.funcTolerance = control.funcTolerance; control_struct.maxFuncEvals = control.maxFuncEvals; control_struct.maxIterations = control.maxIterations; @@ -360,7 +360,7 @@ RAT::struct4_T createStruct4(const Control& control) return control_struct; } -OutputResult OutputResultFromStruct5T(const RAT::struct5_T result) +OutputResult OutputResultFromStruct(const RAT::Results result) { // Copy problem to output OutputResult output_result; @@ -470,7 +470,7 @@ OutputResult OutputResultFromStruct5T(const RAT::struct5_T result) return output_result; } -ProblemDefinition problemDefinitionFromStruct0T(const RAT::struct0_T problem) +ProblemDefinition problemDefinitionFromStruct(const RAT::b_ProblemDefinition problem) { ProblemDefinition problem_def; @@ -540,7 +540,7 @@ ProblemDefinition problemDefinitionFromStruct0T(const RAT::struct0_T problem) return problem_def; } -BayesResults bayesResultsFromStruct8T(const RAT::struct8_T results) +BayesResults bayesResultsFromStruct(const RAT::BayesResults results) { BayesResults bayesResults; @@ -614,20 +614,20 @@ bayes_result : Rat.rat_core.BayesResults py::tuple RATMain(const ProblemDefinition& problem_def, const Limits& limits, const Control& control) { - RAT::struct0_T problem_def_struct = createStruct0(problem_def); - RAT::struct3_T limits_struct = createStruct3(limits); - RAT::struct4_T control_struct = createStruct4(control); + RAT::b_ProblemDefinition problem_def_struct = createProblemDefinitionStruct(problem_def); + RAT::ProblemLimits limits_struct = createProblemLimitsStruct(limits); + RAT::Controls control_struct = createControlsStruct(control); // Output - RAT::struct5_T results; - RAT::struct8_T bayesResults; + RAT::Results results; + RAT::BayesResults bayesResults; // Call the entry-point RAT::RATMain(&problem_def_struct, &limits_struct, &control_struct, &results, &bayesResults); // Copy result to output - auto out_problem_def = problemDefinitionFromStruct0T(problem_def_struct); + auto out_problem_def = problemDefinitionFromStruct(problem_def_struct); out_problem_def.customFiles = problem_def.customFiles.attr("copy")(); return py::make_tuple(out_problem_def, - OutputResultFromStruct5T(results), - bayesResultsFromStruct8T(bayesResults)); + OutputResultFromStruct(results), + bayesResultsFromStruct(bayesResults)); } const std::string docsMakeSLDProfileXY = R"(Creates the profiles for the SLD plots From 147cbee348734f28ed93bd9014f6779847a2db5e Mon Sep 17 00:00:00 2001 From: "Alex H. Room" <69592136+alexhroom@users.noreply.github.com> Date: Tue, 25 Feb 2025 14:11:01 +0000 Subject: [PATCH 06/20] Fixes all docstrings and adds lint for docstrings (#134) * ran ruff pydocstyle ruleset * attributes -> parameters * fixed all docstrings * set convention and fix some issues * review fixes --- RATapi/__init__.py | 2 + RATapi/classlist.py | 84 +++++++++-------- RATapi/controls.py | 7 +- RATapi/events.py | 17 ++-- RATapi/examples/__init__.py | 2 + RATapi/examples/absorption/__init__.py | 1 + RATapi/examples/absorption/absorption.py | 14 ++- .../absorption/volume_thiol_bilayer.py | 3 + .../bayes_benchmark/bayes_benchmark.py | 7 +- .../convert_rascal_project/Model_IIb.py | 3 + .../convert_rascal_project/__init__.py | 1 + .../convert_rascal_project/convert_rascal.py | 19 ++++ RATapi/examples/data/__init__.py | 1 + RATapi/examples/domains/__init__.py | 1 + RATapi/examples/domains/alloy_domains.py | 7 +- RATapi/examples/domains/domains_XY_model.py | 6 +- RATapi/examples/domains/domains_custom_XY.py | 13 ++- .../examples/domains/domains_custom_layers.py | 10 ++- .../domains/domains_standard_layers.py | 12 +++ .../examples/extras/two_contrast_example.py | 4 +- RATapi/examples/languages/__init__.py | 1 + RATapi/examples/languages/custom_bilayer.py | 3 + RATapi/examples/languages/setup_problem.py | 4 +- .../normal_reflectivity/DSPC_custom_XY.py | 4 +- .../normal_reflectivity/DSPC_custom_layers.py | 7 +- .../DSPC_data_background.py | 4 +- .../DSPC_function_background.py | 4 +- .../DSPC_standard_layers.py | 7 +- .../examples/normal_reflectivity/__init__.py | 1 + .../background_function.py | 3 + .../normal_reflectivity/custom_XY_DSPC.py | 7 +- .../custom_bilayer_DSPC.py | 4 +- RATapi/inputs.py | 37 ++++---- RATapi/models.py | 38 +++++--- RATapi/outputs.py | 47 +++++++--- RATapi/project.py | 36 ++++---- RATapi/run.py | 19 ++-- RATapi/utils/__init__.py | 1 + RATapi/utils/convert.py | 8 +- RATapi/utils/custom_errors.py | 8 +- RATapi/utils/enums.py | 89 +++++++++++++++++-- RATapi/utils/plotting.py | 29 +++--- RATapi/wrappers.py | 10 ++- pyproject.toml | 26 +++++- tests/conftest.py | 1 - tests/test_classlist.py | 3 +- tests/test_models.py | 1 - 47 files changed, 452 insertions(+), 164 deletions(-) diff --git a/RATapi/__init__.py b/RATapi/__init__.py index a8ce43b3..8484ea45 100644 --- a/RATapi/__init__.py +++ b/RATapi/__init__.py @@ -1,3 +1,5 @@ +"""RATapi is a Python package for modelling, fitting and optimising reflectivity problems.""" + import RATapi.examples as examples from RATapi import events, models from RATapi.classlist import ClassList diff --git a/RATapi/classlist.py b/RATapi/classlist.py index 6c050daa..23e85477 100644 --- a/RATapi/classlist.py +++ b/RATapi/classlist.py @@ -1,6 +1,4 @@ -"""The classlist module. Contains the ClassList class, which defines a list containing instances of a particular -class. -""" +"""The ClassList class, which defines a list containing instances of a particular class.""" import collections import contextlib @@ -24,12 +22,12 @@ class ClassList(collections.UserList, Generic[T]): attribute given in the ClassList's "name_field" attribute (the default is "name"), the ClassList will ensure that all objects within the ClassList have unique values for that attribute. It is then possible to use this attribute of an object in the .remove(), .count(), and .index() routines in place of the full object. Due to the requirement - of unique values of the name_field attribute, the multiplication operators __mul__, __rmul__, and __imul__ have + of unique values of the ``name_field`` attribute, the multiplication operators __mul__, __rmul__, and __imul__ have been disabled, since they cannot allow for unique attribute values by definition. We extend the UserList class to enable objects to be added and modified using just the keyword arguments, enable - the object name_field attribute to be used in place of the full object, and ensure all elements are of the - specified type, with unique name_field attributes defined. + the object ``name_field`` attribute to be used in place of the full object, and ensure all elements are of the + specified type, with unique ``name_field`` attributes defined. Parameters ---------- @@ -171,8 +169,9 @@ def __imul__(self, n: int) -> None: raise TypeError(f"unsupported operand type(s) for *=: '{self.__class__.__name__}' and '{n.__class__.__name__}'") def append(self, obj: T = None, **kwargs) -> None: - """Append a new object to the ClassList using either the object itself, or keyword arguments to set attribute - values. + """Append a new object to the ClassList. + + This method can use the object itself, or can provide attribute values as keyword arguments for a new object. Parameters ---------- @@ -184,7 +183,7 @@ def append(self, obj: T = None, **kwargs) -> None: Raises ------ ValueError - Raised if the input arguments contain a name_field value already defined in the ClassList. + Raised if the input arguments contain a ``name_field`` value already defined in the ClassList. Warnings -------- @@ -216,8 +215,9 @@ def append(self, obj: T = None, **kwargs) -> None: self.data.append(self._class_handle(**kwargs)) def insert(self, index: int, obj: T = None, **kwargs) -> None: - """Insert a new object into the ClassList at a given index using either the object itself, or keyword arguments - to set attribute values. + """Insert a new object at a given index. + + This method can use the object itself, or can provide attribute values as keyword arguments for a new object. Parameters ---------- @@ -231,7 +231,7 @@ def insert(self, index: int, obj: T = None, **kwargs) -> None: Raises ------ ValueError - Raised if the input arguments contain a name_field value already defined in the ClassList. + Raised if the input arguments contain a ``name_field`` value already defined in the ClassList. Warnings -------- @@ -263,20 +263,25 @@ def insert(self, index: int, obj: T = None, **kwargs) -> None: self.data.insert(index, self._class_handle(**kwargs)) def remove(self, item: Union[T, str]) -> None: - """Remove an object from the ClassList using either the object itself or its name_field value.""" + """Remove an object from the ClassList using either the object itself or its ``name_field`` value.""" item = self._get_item_from_name_field(item) self.data.remove(item) def count(self, item: Union[T, str]) -> int: - """Return the number of times an object appears in the ClassList using either the object itself or its - name_field value. + """Return the number of times an object appears in the ClassList. + + This method can use either the object itself or its ``name_field`` value. + """ item = self._get_item_from_name_field(item) return self.data.count(item) def index(self, item: Union[T, str], offset: bool = False, *args) -> int: - """Return the index of a particular object in the ClassList using either the object itself or its - name_field value. If offset is specified, add one to the index. This is used to account for one-based indexing. + """Return the index of a particular object in the ClassList. + + This method can use either the object itself or its ``name_field`` value. + If offset is specified, add one to the index. This is used to account for one-based indexing. + """ item = self._get_item_from_name_field(item) return self.data.index(item, *args) + int(offset) @@ -357,12 +362,12 @@ def __exit__(self, exctype, excinst, exctb): self._class_handle.model_validate(self.data[index]) def get_names(self) -> list[str]: - """Return a list of the values of the name_field attribute of each class object in the list. + """Return a list of the values of the ``name_field`` attribute of each class object in the list. Returns ------- names : list [str] - The value of the name_field attribute of each object in the ClassList. + The value of the ``name_field`` attribute of each object in the ClassList. """ return [getattr(model, self.name_field) for model in self.data if hasattr(model, self.name_field)] @@ -389,8 +394,7 @@ def get_all_matches(self, value: Any) -> list[tuple]: ] def _validate_name_field(self, input_args: dict[str, Any]) -> None: - """Raise a ValueError if the name_field attribute is passed as an object parameter, and its value is already - used within the ClassList. + """Raise a ValueError if the user tries to add an object with a ``name_field`` already in the ClassList. Parameters ---------- @@ -400,7 +404,7 @@ def _validate_name_field(self, input_args: dict[str, Any]) -> None: Raises ------ ValueError - Raised if the input arguments contain a name_field value already defined in the ClassList. + Raised if the input arguments contain a ``name_field`` value already defined in the ClassList. """ names = [name.lower() for name in self.get_names()] @@ -413,8 +417,7 @@ def _validate_name_field(self, input_args: dict[str, Any]) -> None: ) def _check_unique_name_fields(self, input_list: Sequence[T]) -> None: - """Raise a ValueError if any value of the name_field attribute is used more than once in a list of class - objects. + """Raise a ValueError if any value of the ``name_field`` attribute is repeated in a list of class objects. Parameters ---------- @@ -470,17 +473,17 @@ def _check_unique_name_fields(self, input_list: Sequence[T]) -> None: ) def _check_classes(self, input_list: Sequence[T]) -> None: - """Raise a ValueError if any object in a list of objects is not of the type specified by self._class_handle. + """Raise a ValueError if any object in a list of objects is not of the type specified by ``self._class_handle``. Parameters ---------- input_list : iterable - A list of instances of the class given in self._class_handle. + A list of instances of the class given in ``self._class_handle``. Raises ------ ValueError - Raised if the input list contains objects of any type other than that given in self._class_handle. + If the input list contains objects of any type other than that given in ``self._class_handle``. """ error_list = [] @@ -495,18 +498,18 @@ def _check_classes(self, input_list: Sequence[T]) -> None: ) def _get_item_from_name_field(self, value: Union[T, str]) -> Union[T, str]: - """Return the object with the given value of the name_field attribute in the ClassList. + """Return the object with the given value of the ``name_field`` attribute in the ClassList. Parameters ---------- value : T or str - Either an object in the ClassList, or the value of the name_field attribute of an object in the ClassList. + Either an object in the ClassList, or the value of the ``name_field`` for an object in the ClassList. Returns ------- instance : T or str - Either the object with the value of the name_field attribute given by value, or the input value if an - object with that value of the name_field attribute cannot be found. + Either the object with the value of the ``name_field`` attribute given by value, or the input value if an + object with that value of the ``name_field`` attribute cannot be found. """ try: @@ -518,24 +521,27 @@ def _get_item_from_name_field(self, value: Union[T, str]) -> Union[T, str]: @staticmethod def _determine_class_handle(input_list: Sequence[T]): - """When inputting a sequence of object to a ClassList, the _class_handle should be set as the type of the - element which satisfies "issubclass" for all the other elements. + """Determine the class handle from a sequence of objects. + + The ``_class_handle`` of the sequence is the type of the first element in the sequence + which is a subclass of all elements in the sequence. If no such element exists, the handle + is set to be the type of the first element in the list. Parameters ---------- - input_list : Sequence [object] + input_list : Sequence[T] A list of instances to populate the ClassList. Returns ------- class_handle : type - The type object of the element fulfilling the condition of satisfying "issubclass" for all of the other - elements. + The type object of the first element which is a subclass of all of the other + elements, or the first element if no such element exists. """ - for this_element in input_list: - if all([issubclass(type(instance), type(this_element)) for instance in input_list]): - class_handle = type(this_element) + for element in input_list: + if all(issubclass(type(instance), type(element)) for instance in input_list): + class_handle = type(element) break else: class_handle = type(input_list[0]) diff --git a/RATapi/controls.py b/RATapi/controls.py index e874e6f3..50f18c99 100644 --- a/RATapi/controls.py +++ b/RATapi/controls.py @@ -1,3 +1,5 @@ +"""The Controls class for providing RAT algorithm settings.""" + import contextlib import os import tempfile @@ -196,19 +198,20 @@ def __str__(self) -> str: return table.get_string() def initialise_IPC(self): - """Setup the inter-process communication file.""" + """Set up the inter-process communication file.""" IPC_obj, self._IPCFilePath = tempfile.mkstemp() os.write(IPC_obj, b"0") os.close(IPC_obj) return None def sendStopEvent(self): - """Sends the stop event via the inter-process communication file. + """Send the stop event via the inter-process communication file. Warnings -------- UserWarning Raised if we try to delete an IPC file that was not initialised. + """ if os.path.isfile(self._IPCFilePath): with open(self._IPCFilePath, "wb") as f: diff --git a/RATapi/events.py b/RATapi/events.py index e3ba19f5..c2348ecc 100644 --- a/RATapi/events.py +++ b/RATapi/events.py @@ -1,3 +1,5 @@ +"""Hooks for connecting to run callback events.""" + import os from typing import Callable, Union @@ -5,8 +7,7 @@ def notify(event_type: EventTypes, data: Union[str, PlotEventData, ProgressEventData]) -> None: - """Calls registered callbacks with the data when event type has - been triggered. + """Call registered callbacks with data when event type has been triggered. Parameters ---------- @@ -22,7 +23,7 @@ def notify(event_type: EventTypes, data: Union[str, PlotEventData, ProgressEvent def get_event_callback(event_type: EventTypes) -> list[Callable[[Union[str, PlotEventData, ProgressEventData]], None]]: - """Returns all callbacks registered for the given event type. + """Return all callbacks registered for the given event type. Parameters ---------- @@ -39,7 +40,7 @@ def get_event_callback(event_type: EventTypes) -> list[Callable[[Union[str, Plot def register(event_type: EventTypes, callback: Callable[[Union[str, PlotEventData, ProgressEventData]], None]) -> None: - """Registers a new callback for the event type. + """Register a new callback for the event type. Parameters ---------- @@ -58,12 +59,14 @@ def register(event_type: EventTypes, callback: Callable[[Union[str, PlotEventDat def clear(key=None, callback=None) -> None: - """Clears all event callbacks or specific callback. + """Clear all event callbacks or specific callback. Parameters ---------- - callback : Callable[[Union[str, PlotEventData, ProgressEventData]], None] - The callback for when the event is triggered. + key : EventTypes, optional + The event type of the callback to clear if given. + callback : Callable[[Union[str, PlotEventData, ProgressEventData]], None], optional + A callback for an event which will be cleared if given. """ if key is None and callback is None: diff --git a/RATapi/examples/__init__.py b/RATapi/examples/__init__.py index d2bb8877..86de2334 100644 --- a/RATapi/examples/__init__.py +++ b/RATapi/examples/__init__.py @@ -1,3 +1,5 @@ +"""Usage examples for the Python RAT API.""" + from RATapi.examples.absorption.absorption import absorption from RATapi.examples.convert_rascal_project.convert_rascal import convert_rascal from RATapi.examples.domains.domains_custom_layers import domains_custom_layers diff --git a/RATapi/examples/absorption/__init__.py b/RATapi/examples/absorption/__init__.py index e69de29b..bf48e7ae 100644 --- a/RATapi/examples/absorption/__init__.py +++ b/RATapi/examples/absorption/__init__.py @@ -0,0 +1 @@ +"""An example of using absorption in a RAT project.""" diff --git a/RATapi/examples/absorption/absorption.py b/RATapi/examples/absorption/absorption.py index ce7407d4..63b1ba3e 100644 --- a/RATapi/examples/absorption/absorption.py +++ b/RATapi/examples/absorption/absorption.py @@ -1,3 +1,5 @@ +"""An example for using absorption in RAT.""" + import pathlib import numpy as np @@ -6,7 +8,17 @@ def absorption(): - """Custom layers model including absorption""" + """Run a custom layers model including absorption. + + RAT allows the use of an imaginary, as well as real part of the SLD. + The effect of this is usually seen below the critical edge, and must sometimes be accounted for. + + This is an example of a Custom Layers project using absorption. used here is Custom Layers. + It analyses a bilayer sample on a permalloy / gold substrate, + measured using polarised neutrons, against D2O and H2O, leading to 4 contrasts in total. + Absorption (i.e. imaginary SLD) is defined for Gold and the Permalloy, + to account for non-flat data below the critical edge. + """ problem = RAT.Project( name="Absorption example", calculation="normal", diff --git a/RATapi/examples/absorption/volume_thiol_bilayer.py b/RATapi/examples/absorption/volume_thiol_bilayer.py index f64f16ff..4398f2b6 100644 --- a/RATapi/examples/absorption/volume_thiol_bilayer.py +++ b/RATapi/examples/absorption/volume_thiol_bilayer.py @@ -1,3 +1,6 @@ +"""Custom layer model file for the absorption example.""" + + def volume_thiol_bilayer(params, bulk_in, bulk_out, contrast): """VolumeThiolBilayer RAT Custom Layer Model File. diff --git a/RATapi/examples/bayes_benchmark/bayes_benchmark.py b/RATapi/examples/bayes_benchmark/bayes_benchmark.py index 2cec2e3e..80ac9a24 100644 --- a/RATapi/examples/bayes_benchmark/bayes_benchmark.py +++ b/RATapi/examples/bayes_benchmark/bayes_benchmark.py @@ -1,4 +1,5 @@ -""" +"""An example script to compare different methods of Bayesian fitting. + This example compares three Bayesian posteriors for a low-dimensional example: a posterior generated by DREAM, one generated by NS, and one calculated directly. @@ -34,6 +35,7 @@ # this is the RasCAL-1 default project # it is a bare D2O substrate def get_project() -> RAT.Project: + """Create the project used as our example.""" return RAT.Project( name="Bare D2O Substrate", calculation="normal", @@ -191,6 +193,7 @@ def calculate_posterior(roughness_index: int, background_index: int) -> float: ------- float The value of exp(-chi^2 / 2) for the given roughness and background values. + """ problem.parameters[0].value = roughness[roughness_index] problem.background_parameters[0].value = background[background_index] @@ -265,6 +268,7 @@ def calculate_posterior(roughness_index: int, background_index: int, scalefactor ------- float The value of exp(-chi^2 / 2) for the given roughness and background values. + """ problem.parameters[0].value = roughness[roughness_index] problem.background_parameters[0].value = background[background_index] @@ -300,6 +304,7 @@ def plot_posterior_comparison( The BayesResults object from a DREAM calculation. calc_results : CalculationResults The results from a direct calculation. + """ num_params = calc_results.distribution.ndim fig, axes = plt.subplots(3, num_params, figsize=(3 * num_params, 9)) diff --git a/RATapi/examples/convert_rascal_project/Model_IIb.py b/RATapi/examples/convert_rascal_project/Model_IIb.py index ae36ba5f..d7c61526 100644 --- a/RATapi/examples/convert_rascal_project/Model_IIb.py +++ b/RATapi/examples/convert_rascal_project/Model_IIb.py @@ -1,7 +1,10 @@ +"""A custom model file for a monolayer volume model.""" + from math import cos, radians def Model_IIb(params, bulk_in, bulk_out, contrast): + """Calculate layer parameters for a monolayer volume model at two deuterations.""" # converted from matlab file Model_IIb.m Roughness, APM, thickHead, theta = params diff --git a/RATapi/examples/convert_rascal_project/__init__.py b/RATapi/examples/convert_rascal_project/__init__.py index e69de29b..0648853e 100644 --- a/RATapi/examples/convert_rascal_project/__init__.py +++ b/RATapi/examples/convert_rascal_project/__init__.py @@ -0,0 +1 @@ +"""An example of converting between RAT and RasCAL-1.""" diff --git a/RATapi/examples/convert_rascal_project/convert_rascal.py b/RATapi/examples/convert_rascal_project/convert_rascal.py index f83cb38c..b0b945f7 100644 --- a/RATapi/examples/convert_rascal_project/convert_rascal.py +++ b/RATapi/examples/convert_rascal_project/convert_rascal.py @@ -1,3 +1,5 @@ +"""An example of how to convert a RasCAL-1 project to RAT.""" + import pathlib from pprint import pp @@ -6,6 +8,23 @@ # convert R1 project to Project class def convert_rascal(mat_filename="lipid_bilayer.mat"): + """Convert a project from RasCAL-1 and a project to RasCAL-1. + + We convert a RasCAL-1 monolayer volume model to RAT, and the DSPC Standard Layers + example to RasCAL-1. + + Parameters + ---------- + mat_filename : str + The filename of the output of the RAT project converted to RasCAL-2. + + Returns + ------- + project, struct + A RasCAL-1 monolayer volume model converted to a RAT project, and the + struct of the DSPC standard layers example converted to a RasCAL-1 struct. + + """ project_path = pathlib.Path(__file__).parent / "R1monolayerVolumeModel.mat" project = RAT.utils.convert.r1_to_project_class(project_path) diff --git a/RATapi/examples/data/__init__.py b/RATapi/examples/data/__init__.py index e69de29b..9fc1b9dd 100644 --- a/RATapi/examples/data/__init__.py +++ b/RATapi/examples/data/__init__.py @@ -0,0 +1 @@ +"""Data files used by the examples.""" diff --git a/RATapi/examples/domains/__init__.py b/RATapi/examples/domains/__init__.py index e69de29b..ed955f76 100644 --- a/RATapi/examples/domains/__init__.py +++ b/RATapi/examples/domains/__init__.py @@ -0,0 +1 @@ +"""Examples for how to use RAT with domains models.""" diff --git a/RATapi/examples/domains/alloy_domains.py b/RATapi/examples/domains/alloy_domains.py index f7d9ed4c..33454dcc 100644 --- a/RATapi/examples/domains/alloy_domains.py +++ b/RATapi/examples/domains/alloy_domains.py @@ -1,5 +1,10 @@ +"""Custom model file for the domains custom layers example.""" + + def alloy_domains(params, bulkIn, bulkOut, contrast, domain): - """Simple custom model for testing incoherent summing. + """Calculate custom model layers for a permalloy/gold model with domains. + + Simple custom model for testing incoherent summing. Simple two layer of permalloy / gold, with up/down domains. """ # Split up the parameters diff --git a/RATapi/examples/domains/domains_XY_model.py b/RATapi/examples/domains/domains_XY_model.py index fd1823a4..8aeb8c77 100644 --- a/RATapi/examples/domains/domains_XY_model.py +++ b/RATapi/examples/domains/domains_XY_model.py @@ -1,9 +1,12 @@ +"""Custom model file for the domains custom XY example.""" + import math import numpy as np def domains_XY_model(params, bulk_in, bulk_out, contrast, domain): + """Calculate the SLD profile for a domains custom XY model.""" # Split up the parameters for convenience subRough = params[0] oxideThick = params[1] @@ -51,7 +54,8 @@ def domains_XY_model(params, bulk_in, bulk_out, contrast, domain): def makeLayer(z, prevLaySurf, thickness, height, Sigma_L, Sigma_R): - """This produces a layer, with a defined thickness, height and roughness. + """Produce a layer, with a defined thickness, height and roughness. + Each side of the layer has its own roughness value. """ # Find the edges diff --git a/RATapi/examples/domains/domains_custom_XY.py b/RATapi/examples/domains/domains_custom_XY.py index e069540f..1eb7222d 100644 --- a/RATapi/examples/domains/domains_custom_XY.py +++ b/RATapi/examples/domains/domains_custom_XY.py @@ -1,10 +1,21 @@ +"""An example of using domains with a Custom XY model.""" + import pathlib import RATapi as RAT def domains_custom_XY(): - """Simple example of a layer containing domains using a custom XY model""" + """Calculate an example of a layer containing domains using a custom XY model. + + Domains custom XY models operate in the same way as domains custom layer models, + in that there is an additional input to the custom model + specifying the domain to be calculated. + + This is then used within the function to calculate the correct SLD profile + for each contrast and domain. In this example, we simulate a hydrogenated layer + on a silicon substrate, containing domains of a larger SLD, against D2O, SMW and water. + """ problem = RAT.Project(calculation="domains", model="custom xy", geometry="substrate/liquid") problem.parameters.append(name="Oxide Thickness", min=10.0, value=20.0, max=50.0, fit=True) diff --git a/RATapi/examples/domains/domains_custom_layers.py b/RATapi/examples/domains/domains_custom_layers.py index 7ebf0132..99c2d372 100644 --- a/RATapi/examples/domains/domains_custom_layers.py +++ b/RATapi/examples/domains/domains_custom_layers.py @@ -1,10 +1,18 @@ +"""An example of using domains with custom layer models.""" + import pathlib import RATapi as RAT def domains_custom_layers(): - """An example custom layers domains project involving incoherent summing on a permalloy layer""" + """Calculate an example custom layers domains project involving incoherent summing on a permalloy layer. + + For a custom layers model, rather than being forced to define our layers as [Thick SLD Rough…. etc], + we can parameterise however we like and then use a function to calculate the parameters for each layer. + So for example, if the volume of lipid tails are known (from the literature), + then all we need is the Area per molecule to calculate the layers. + """ problem = RAT.Project(calculation="domains", model="custom layers", geometry="substrate/liquid") # Make some parameters... diff --git a/RATapi/examples/domains/domains_standard_layers.py b/RATapi/examples/domains/domains_standard_layers.py index b1d08778..9e821a8d 100644 --- a/RATapi/examples/domains/domains_standard_layers.py +++ b/RATapi/examples/domains/domains_standard_layers.py @@ -1,7 +1,19 @@ +"""An example using domains with standard layers.""" + import RATapi as RAT def domains_standard_layers(): + """Calculate an example for using standard layers with domains. + + Domains standard layers projects proceed in much the same way as a normal standard layers problem, + except that there is an additional grouping step between layers and contrasts. + + Layers are grouped into ‘Domain Contrasts’. The model for the actual experimental contrast + is built from these domain contrasts rather than from layers. There are exactly + two domains for each contrast, with the the ratio of them controlled by + a fittable ‘domain ratio’ parameter. + """ problem = RAT.Project(calculation="domains") # Define the parameters we need to define our two domains diff --git a/RATapi/examples/extras/two_contrast_example.py b/RATapi/examples/extras/two_contrast_example.py index 8d95f1b8..ca441e14 100644 --- a/RATapi/examples/extras/two_contrast_example.py +++ b/RATapi/examples/extras/two_contrast_example.py @@ -1,10 +1,12 @@ +"""The example project with two contrasts from the user guide.""" + import numpy as np import RATapi as RAT def two_contrast_example(): - # Two contrast example for the user guide + """Generate the two-contrast example project from the user guide.""" problem = RAT.Project(name="DSPC monolayers") parameters = [ RAT.models.Parameter(name="Tails Thickness", min=10, value=20, max=30, fit=True), diff --git a/RATapi/examples/languages/__init__.py b/RATapi/examples/languages/__init__.py index e69de29b..99fd5e1f 100644 --- a/RATapi/examples/languages/__init__.py +++ b/RATapi/examples/languages/__init__.py @@ -0,0 +1 @@ +"""A benchmark for using RAT custom files in three different languages.""" diff --git a/RATapi/examples/languages/custom_bilayer.py b/RATapi/examples/languages/custom_bilayer.py index dc978e31..1777b358 100644 --- a/RATapi/examples/languages/custom_bilayer.py +++ b/RATapi/examples/languages/custom_bilayer.py @@ -1,7 +1,10 @@ +"""A custom bilayer model for the custom file languages benchmark.""" + import numpy as np def custom_bilayer(params, bulk_in, bulk_out, contrast): + """Calculate the layer parameters for a custom bilayer model.""" sub_rough = params[0] oxide_thick = params[1] oxide_hydration = params[2] diff --git a/RATapi/examples/languages/setup_problem.py b/RATapi/examples/languages/setup_problem.py index 979c5ed1..4eea0965 100644 --- a/RATapi/examples/languages/setup_problem.py +++ b/RATapi/examples/languages/setup_problem.py @@ -1,3 +1,5 @@ +"""A custom example problem for the languages benchmark.""" + import pathlib import numpy as np @@ -6,7 +8,7 @@ def make_example_problem(): - """Custom Layers example for Supported DSPC layer. + """Generate a Custom Layers example for Supported DSPC layer. Example of using custom layers to model a DSPC supported bilayer. """ diff --git a/RATapi/examples/normal_reflectivity/DSPC_custom_XY.py b/RATapi/examples/normal_reflectivity/DSPC_custom_XY.py index ab1492da..27b15ffe 100644 --- a/RATapi/examples/normal_reflectivity/DSPC_custom_XY.py +++ b/RATapi/examples/normal_reflectivity/DSPC_custom_XY.py @@ -1,3 +1,5 @@ +"""An example of analysing a Custom XY model.""" + import pathlib import numpy as np @@ -6,7 +8,7 @@ def DSPC_custom_XY(): - r"""Custom XY Example for Supported DSPC layer. + r"""Calculate a Custom XY Example for Supported DSPC layer. In this example, we model the same data (DSPC supported bilayer) as the Custom Layers example, but this time we will use continuous distributions of the volume fractions of each component to build up the SLD profiles (as described in diff --git a/RATapi/examples/normal_reflectivity/DSPC_custom_layers.py b/RATapi/examples/normal_reflectivity/DSPC_custom_layers.py index 508653fb..66a53589 100644 --- a/RATapi/examples/normal_reflectivity/DSPC_custom_layers.py +++ b/RATapi/examples/normal_reflectivity/DSPC_custom_layers.py @@ -1,3 +1,5 @@ +"""Example of using custom layers to model a DSPC supported bilayer.""" + import pathlib import numpy as np @@ -6,10 +8,7 @@ def DSPC_custom_layers(): - """Custom Layers example for Supported DSPC layer. - - Example of using custom layers to model a DSPC supported bilayer. - """ + """Calculate a Custom Layers example for a supported DSPC layer.""" problem = RAT.Project(name="Orso lipid example - custom layers", model="custom layers", geometry="substrate/liquid") # First we need to set up a parameters group. We will be using a pre-prepared custom model file, so we need to add diff --git a/RATapi/examples/normal_reflectivity/DSPC_data_background.py b/RATapi/examples/normal_reflectivity/DSPC_data_background.py index 02987226..e4b6869f 100644 --- a/RATapi/examples/normal_reflectivity/DSPC_data_background.py +++ b/RATapi/examples/normal_reflectivity/DSPC_data_background.py @@ -1,3 +1,5 @@ +"""A standard layers example with a data background.""" + import pathlib import numpy as np @@ -6,7 +8,7 @@ def DSPC_data_background(): - """Standard Layers fit of a DSPC floating bilayer""" + """Calculate a Standard Layers fit of a DSPC floating bilayer with a data background.""" problem = RAT.Project(name="original_dspc_bilayer", model="standard layers", geometry="substrate/liquid") # Set up the relevant parameters diff --git a/RATapi/examples/normal_reflectivity/DSPC_function_background.py b/RATapi/examples/normal_reflectivity/DSPC_function_background.py index 8a691107..54cc9d47 100644 --- a/RATapi/examples/normal_reflectivity/DSPC_function_background.py +++ b/RATapi/examples/normal_reflectivity/DSPC_function_background.py @@ -1,3 +1,5 @@ +"""A standard layers example with a function background.""" + import pathlib import numpy as np @@ -6,7 +8,7 @@ def DSPC_function_background(): - """Standard Layers fit of a DSPC floating bilayer""" + """Calculate a standard Layers fit of a DSPC floating bilayer with a function background.""" problem = RAT.Project(name="original_dspc_bilayer", model="standard layers", geometry="substrate/liquid") # Set up the relevant parameters diff --git a/RATapi/examples/normal_reflectivity/DSPC_standard_layers.py b/RATapi/examples/normal_reflectivity/DSPC_standard_layers.py index 0d0d1181..f4cc2ce4 100644 --- a/RATapi/examples/normal_reflectivity/DSPC_standard_layers.py +++ b/RATapi/examples/normal_reflectivity/DSPC_standard_layers.py @@ -1,3 +1,5 @@ +"""An example of a standard layers model in RAT.""" + import pathlib import numpy as np @@ -6,7 +8,10 @@ def DSPC_standard_layers(): - """Standard Layers fit of a DSPC floating bilayer""" + """Calculate a standard Layers fit of a DSPC floating bilayer. + + The sample consists of a DSPC bilayer, on a silane SAM on Silicon. + """ problem = RAT.Project(name="original_dspc_bilayer", model="standard layers", geometry="substrate/liquid") # Set up the relevant parameters diff --git a/RATapi/examples/normal_reflectivity/__init__.py b/RATapi/examples/normal_reflectivity/__init__.py index e69de29b..382a9167 100644 --- a/RATapi/examples/normal_reflectivity/__init__.py +++ b/RATapi/examples/normal_reflectivity/__init__.py @@ -0,0 +1 @@ +"""Examples of normal reflectivity calculations in RAT.""" diff --git a/RATapi/examples/normal_reflectivity/background_function.py b/RATapi/examples/normal_reflectivity/background_function.py index 68993814..43624a08 100644 --- a/RATapi/examples/normal_reflectivity/background_function.py +++ b/RATapi/examples/normal_reflectivity/background_function.py @@ -1,7 +1,10 @@ +"""A background function for an example.""" + import numpy as np def background_function(xdata, params): + """Return the background function for a given set of points in q.""" # Split up the params array Ao = params[0] k = params[1] diff --git a/RATapi/examples/normal_reflectivity/custom_XY_DSPC.py b/RATapi/examples/normal_reflectivity/custom_XY_DSPC.py index 0a8c1167..dc1d1013 100644 --- a/RATapi/examples/normal_reflectivity/custom_XY_DSPC.py +++ b/RATapi/examples/normal_reflectivity/custom_XY_DSPC.py @@ -1,10 +1,12 @@ +"""A custom XY model for a supported DSPC bilayer.""" + import math import numpy as np def custom_XY_DSPC(params, bulk_in, bulk_out, contrast): - """This function makes a model of a supported DSPC bilayer using volume restricted distribution functions.""" + """Calculate the continuous SLD of a supported DSPC bilayer using volume restricted distribution functions.""" # Split up the parameters subRough = params[0] oxideThick = params[1] @@ -118,7 +120,8 @@ def custom_XY_DSPC(params, bulk_in, bulk_out, contrast): def layer(z, prevLaySurf, thickness, height, Sigma_L, Sigma_R): - """This produces a layer, with a defined thickness, height and roughness. + """Produce a layer, with a defined thickness, height and roughness. + Each side of the layer has its own roughness value. """ # Find the edges diff --git a/RATapi/examples/normal_reflectivity/custom_bilayer_DSPC.py b/RATapi/examples/normal_reflectivity/custom_bilayer_DSPC.py index c97cde4e..005147ff 100644 --- a/RATapi/examples/normal_reflectivity/custom_bilayer_DSPC.py +++ b/RATapi/examples/normal_reflectivity/custom_bilayer_DSPC.py @@ -1,8 +1,10 @@ +"""A custom layer model for a DSPC supported bilayer.""" + import numpy as np def custom_bilayer_DSPC(params, bulk_in, bulk_out, contrast): - """CUSTOMBILAYER RAT Custom Layer Model File. + """Calculate layer parameters for a DSPC supported bilayer. This file accepts 3 vectors containing the values for params, bulk in and bulk out. The final parameter is an index of the contrast being calculated. diff --git a/RATapi/inputs.py b/RATapi/inputs.py index 812b9ac4..69080636 100644 --- a/RATapi/inputs.py +++ b/RATapi/inputs.py @@ -1,4 +1,4 @@ -"""Converts python models to the necessary inputs for the compiled RAT code""" +"""Converts python models to the necessary inputs for the compiled RAT code.""" import importlib import os @@ -56,6 +56,7 @@ class FileHandles: ---------- files : ClassList[CustomFile] A list of custom file models. + """ def __init__(self, files=None): @@ -66,8 +67,8 @@ def __iter__(self): self.index = 0 return self - def get_handle(self, index): - """Returns file handle for a given custom file. + def get_handle(self, index: int): + """Return file handle for a given custom file. Parameters ---------- @@ -86,7 +87,15 @@ def get_handle(self, index): return file_handle - def copy(self): + def copy(self) -> "FileHandles": + """Create a copy of the FileHandles object. + + Returns + ------- + FileHandles + The copy of this FileHandles object. + + """ handles = FileHandles() handles.files = [file.copy() for file in self.files] @@ -105,8 +114,7 @@ def __len__(self): def make_input(project: RATapi.Project, controls: RATapi.Controls) -> tuple[ProblemDefinition, Limits, Control]: - """Constructs the inputs required for the compiled RAT code using the data defined in the input project and - controls. + """Construct the inputs required for the compiled RAT code using the data defined in the input project and controls. Parameters ---------- @@ -147,7 +155,7 @@ def make_input(project: RATapi.Project, controls: RATapi.Controls) -> tuple[Prob def make_problem(project: RATapi.Project) -> ProblemDefinition: - """Constructs the problem input required for the compiled RAT code. + """Construct the problem input required for the compiled RAT code. Parameters ---------- @@ -394,7 +402,7 @@ def make_problem(project: RATapi.Project) -> ProblemDefinition: def make_resample(project: RATapi.Project) -> list[int]: - """Constructs the "resample" field of the problem input required for the compiled RAT code. + """Construct the "resample" field of the problem input required for the compiled RAT code. Parameters ---------- @@ -403,7 +411,7 @@ def make_resample(project: RATapi.Project) -> list[int]: Returns ------- - : list[int] + list[int] The "resample" field of the problem input used in the compiled RAT code. """ @@ -411,7 +419,7 @@ def make_resample(project: RATapi.Project) -> list[int]: def make_data_present(project: RATapi.Project) -> list[int]: - """Constructs the "dataPresent" field of the problem input required for the compiled RAT code. + """Construct the "dataPresent" field of the problem input required for the compiled RAT code. Parameters ---------- @@ -420,7 +428,7 @@ def make_data_present(project: RATapi.Project) -> list[int]: Returns ------- - : list[int] + list[int] The "dataPresent" field of the problem input used in the compiled RAT code. """ @@ -428,8 +436,7 @@ def make_data_present(project: RATapi.Project) -> list[int]: def check_indices(problem: ProblemDefinition) -> None: - """Checks the indices in contrast lists in a ProblemDefinition object lie within the range of the corresponding - parameter lists. + """Check the indices given in a problem's contrasts lie within the range of the corresponding parameter lists. Parameters ---------- @@ -517,14 +524,12 @@ def append_data_background(data: np.array, background: np.array) -> np.array: def make_controls(input_controls: RATapi.Controls) -> Control: - """Converts the controls object to the format required by the compiled RAT code. + """Convert the controls object to the format required by the compiled RAT code. Parameters ---------- input_controls : RAT.Controls The controls model, which defines algorithmic properties. - checks : Rat.rat_core.Checks - States whether or not to fit each parameter defined in the project. Returns ------- diff --git a/RATapi/models.py b/RATapi/models.py index 3d9f7097..b43112d8 100644 --- a/RATapi/models.py +++ b/RATapi/models.py @@ -74,6 +74,14 @@ def __setattr__(self, name, value): @property def display_fields(self) -> dict: + """The fields which should be visible in a table and their values. + + Returns + ------- + dict + A dictionary of the fields which should be visible in a table and their values. + + """ visible_fields = ["name", "type", "source"] if self.type != TypeOptions.Constant: visible_fields.append("value_1") @@ -179,7 +187,6 @@ class Contrast(RATModel): @classmethod def domain_ratio_error(cls, data: Any): """If the extra input 'domain_ratio' is given, give a more descriptive error.""" - if isinstance(data, dict) and data.get("domain_ratio", False): raise ValueError( "The Contrast class does not support domain ratios. Use the ContrastWithRatio class instead." @@ -301,17 +308,18 @@ class CustomFile(RATModel): path: pathlib.Path = pathlib.Path(".") def model_post_init(self, __context: Any) -> None: - """If a "filename" is supplied but the "function_name" field is not set, the "function_name" should be set to - the file name without the extension. + """Autogenerate the function name from the filename if not set. + + If a filename is supplied but the ``function_name`` field is not set, the ``function_name`` should be set to + the filename without the extension. + """ if "filename" in self.model_fields_set and "function_name" not in self.model_fields_set: self.function_name = pathlib.Path(self.filename).stem @model_validator(mode="after") def set_matlab_function_name(self): - """If we have a matlab custom function, the "function_name" should be set to the filename without the - extension. - """ + """For a matlab custom function, ``function_name`` should be set to the filename without the extension.""" if self.language == Languages.Matlab and self.function_name != pathlib.Path(self.filename).stem: self.function_name = pathlib.Path(self.filename).stem @@ -340,7 +348,7 @@ class Data(RATModel, arbitrary_types_allowed=True): @field_validator("data") @classmethod def check_data_dimension(cls, data: np.ndarray[float]) -> np.ndarray[float]: - """The data must be a two-dimensional array containing at least three columns.""" + """Ensure the data is be a two-dimensional array containing at least three columns.""" try: data.shape[1] except IndexError: @@ -353,13 +361,15 @@ def check_data_dimension(cls, data: np.ndarray[float]) -> np.ndarray[float]: @field_validator("data_range", "simulation_range") @classmethod def check_min_max(cls, limits: list[float], info: ValidationInfo) -> list[float]: - """The data range and simulation range maximum must be greater than the minimum.""" + """Ensure the data range and simulation range maximum is greater than the minimum.""" if limits[0] > limits[1]: raise ValueError(f'{info.field_name} "min" value is greater than the "max" value') return limits def model_post_init(self, __context: Any) -> None: - """If the "data_range" and "simulation_range" fields are not set, but "data" is supplied, the ranges should be + """Automatically generate ``data_range`` and ``simulation_range`` from the data. + + If the ``data_range`` and ``simulation_range`` fields are not set, but ``data`` is supplied, the ranges are set to the min and max values of the first column (assumed to be q) of the supplied data. """ if self.data.shape[0] > 0: @@ -371,7 +381,9 @@ def model_post_init(self, __context: Any) -> None: @model_validator(mode="after") def check_ranges(self) -> "Data": - """The limits of the "data_range" field must lie within the range of the supplied data, whilst the limits + """Check that ``data_range`` is within the q-range of the data, and ``simulation_range`` is outside it. + + The limits of the "data_range" field must lie within the range of the supplied data, whilst the limits of the "simulation_range" field must lie outside the range of the supplied data. """ if self.data.shape[0] > 0: @@ -474,7 +486,6 @@ class Layer(RATModel, populate_by_name=True): @classmethod def sld_imaginary_error(cls, data: Any): """If the extra input 'sld_imaginary' is given, give a more descriptive error.""" - if isinstance(data, dict) and data.get("SLD_imaginary", False): raise ValueError("The Layer class does not support imaginary SLD. Use the AbsorptionLayer class instead.") @@ -549,20 +560,21 @@ class Parameter(RATModel): @model_validator(mode="after") def check_min_max(self) -> "Parameter": - """The maximum value of a parameter must be greater than the minimum.""" + """Ensure the maximum value of a parameter is greater than the minimum.""" if self.min > self.max: raise ValueError(f"The maximum value {self.max} must be greater than the minimum value {self.min}") return self @model_validator(mode="after") def check_value_in_range(self) -> "Parameter": - """The value of a parameter must lie within its defined bounds.""" + """Ensure the value of a parameter lies within its defined bounds.""" if self.value < self.min or self.value > self.max: raise ValueError(f"value {self.value} is not within the defined range: {self.min} <= value <= {self.max}") return self @property def display_fields(self) -> dict: + """Only display Prior information if ``show_priors`` is true.""" visible_fields = ["name", "min", "value", "max", "fit"] if self.show_priors: visible_fields.append("prior_type") diff --git a/RATapi/outputs.py b/RATapi/outputs.py index 353459eb..84b3222d 100644 --- a/RATapi/outputs.py +++ b/RATapi/outputs.py @@ -1,4 +1,4 @@ -"""Converts outputs from the compiled RAT code to python dataclasses""" +"""Converts results from the compiled RAT code to python dataclasses.""" from dataclasses import dataclass from typing import Any, Optional, Union @@ -10,8 +10,9 @@ def get_field_string(field: str, value: Any, array_limit: int): - """Returns a string representation of class fields where large and multidimensional arrays are represented by their - shape. + """Return a string representation of class fields where large arrays are represented by their shape. + + An array will be displayed as just its shape if it is multidimensional or 1D and longer than ``array_limit``. Parameters ---------- @@ -26,6 +27,21 @@ def get_field_string(field: str, value: Any, array_limit: int): ------- field_string : str The string representation of the field in the RAT output class. + + Examples + -------- + >>> get_field_string("data", 130, 5) + "data = 130" + + >>> get_field_string("data", array([1, 2, 3, 4, 5]), 10) + "data = [1 2 3 4 5]" + + >>> get_field_string("data", array([1, 2, 3, 4, 5]), 3) + "data = Data array: [5]," + + >>> get_field_string("data", array([[1, 2, 3], [4, 5, 6]]), 10) + "data = Data array: [2 x 3]," + """ array_text = "Data array: " if isinstance(value, list) and len(value) > 0: @@ -52,6 +68,8 @@ def get_field_string(field: str, value: Any, array_limit: int): class RATResult: + """A mixin class which truncates arrays when the class is displayed.""" + def __str__(self): output = f"{self.__class__.__name__}(\n" for key, value in self.__dict__.items(): @@ -64,7 +82,7 @@ def __str__(self): class CalculationResults(RATResult): """The goodness of fit from the Abeles calculation. - Attributes + Parameters ---------- chiValues : np.ndarray The chi-squared value for each contrast. @@ -81,7 +99,7 @@ class CalculationResults(RATResult): class ContrastParams(RATResult): """The experimental parameters for each contrast. - Attributes + Parameters ---------- scalefactors : np.ndarray The scalefactor values for each contrast. @@ -107,7 +125,7 @@ class ContrastParams(RATResult): class Results: """The results of a RAT calculation. - Attributes + Parameters ---------- reflectivity : list The reflectivity curves for each contrast, @@ -174,7 +192,7 @@ class PredictionIntervals(RATResult): - 3: the 65th percentile; - 4: the 95th percentile. - Attributes + Parameters ---------- reflectivity : list The prediction interval data for reflectivity of each contrast. @@ -182,6 +200,7 @@ class PredictionIntervals(RATResult): The prediction interval data for SLD of each contrast. sampleChi : np.ndarray The value of sumChi at each point of the Markov chain. + """ reflectivity: list @@ -191,10 +210,9 @@ class PredictionIntervals(RATResult): @dataclass class ConfidenceIntervals(RATResult): - """ - The 65% and 95% confidence intervals for the best fit results. + """The 65% and 95% confidence intervals for the best fit results. - Attributes + Parameters ---------- percentile95 : np.ndarray The 95% confidence intervals for each fit parameter. @@ -202,6 +220,7 @@ class ConfidenceIntervals(RATResult): The 65% confidence intervals for each fit parameter. mean : np.ndarray The mean values for each fit parameter. + """ percentile95: np.ndarray @@ -213,7 +232,7 @@ class ConfidenceIntervals(RATResult): class DreamParams(RATResult): """The parameters used by the inner DREAM algorithm. - Attributes + Parameters ---------- nParams : float The number of parameters used by the algorithm. @@ -284,7 +303,7 @@ class DreamParams(RATResult): class DreamOutput(RATResult): """The diagnostic output information from DREAM. - Attributes + Parameters ---------- allChains : np.ndarray An ``nGenerations`` x ``nParams + 2`` x ``nChains`` size array, @@ -335,7 +354,7 @@ class DreamOutput(RATResult): class NestedSamplerOutput(RATResult): """The output information from the Nested Sampler (ns). - Attributes + Parameters ---------- logZ : float The natural logarithm of the evidence Z for the parameter values. @@ -363,7 +382,7 @@ class NestedSamplerOutput(RATResult): class BayesResults(Results): """The results of a Bayesian RAT calculation. - Attributes + Parameters ---------- predictionIntervals : PredictionIntervals The prediction intervals. diff --git a/RATapi/project.py b/RATapi/project.py index da36b5ed..03f31e5e 100644 --- a/RATapi/project.py +++ b/RATapi/project.py @@ -358,9 +358,11 @@ def check_contrasts(cls, value: ClassList, info: ValidationInfo): return value def model_post_init(self, __context: Any) -> None: - """Initialises the class in the ClassLists for empty data fields, sets protected parameters, gets names of all - defined parameters, determines the contents of the "model" field in contrasts, and wraps ClassList routines to - control revalidation. + """Set up the Class to protect against disallowed modification. + + We initialise the class handle in the ClassLists for empty data fields, set protected parameters, get names of + all defined parameters, determine the contents of the "model" field in contrasts, + and wrap ClassList routines to control revalidation. """ # Ensure all ClassLists have the correct _class_handle defined for field in (fields := self.model_fields): @@ -440,9 +442,7 @@ def set_domain_ratios(self) -> "Project": @model_validator(mode="after") def set_domain_contrasts(self) -> "Project": - """If we are not running a domains calculation with standard layers, ensure the domain_contrasts component of - the model is empty. - """ + """Ensure ``domain_contrasts`` is empty if we are not running a standard layer domains calculation.""" if not (self.calculation == Calculations.Domains and self.model == LayerModels.StandardLayers): self.domain_contrasts.data = [] return self @@ -487,8 +487,10 @@ def set_calculation(self) -> "Project": @model_validator(mode="after") def set_contrast_model_field(self) -> "Project": - """The contents of the "model" field of "contrasts" depend on the values of the "calculation" and "model_type" - defined in the project. If they have changed, clear the contrast models. + """Clear the contrast models if ``calculation`` or ``model_type`` has changed. + + The contents of the "model" field of "contrasts" depend on the values of the "calculation" and "model_type" + defined in the project. """ model_field = self.get_contrast_model_field() if model_field != self._contrast_model_field: @@ -499,8 +501,10 @@ def set_contrast_model_field(self) -> "Project": @model_validator(mode="after") def check_contrast_model_length(self) -> "Project": - """Given certain values of the "calculation" and "model" defined in the project, the "model" field of - "contrasts" may be constrained in its length. + """Ensure the contrast model isn't too long for a domains, custom layers, or custom XY calculation. + + If a custom model is used, the ``model`` field of the contrast should just be one item long. For + a standard layers domain calculation, it should be exactly two items long. """ if self.model == LayerModels.StandardLayers and self.calculation == Calculations.Domains: for contrast in self.contrasts: @@ -695,8 +699,7 @@ def check_contrast_model_allowed_values( allowed_values: list[str], allowed_field: str, ) -> None: - """The contents of the "model" field of "contrasts" and "domain_contrasts" must be defined elsewhere in the - project. + """Ensure the contents of the ``model`` for a contrast or domain contrast exist in the required project fields. Parameters ---------- @@ -911,7 +914,7 @@ def load(cls, path: Union[str, Path]) -> "Project": return cls.model_validate(model_dict) def _classlist_wrapper(self, class_list: ClassList, func: Callable): - """Defines the function used to wrap around ClassList routines to force revalidation. + """Define the function used to wrap around ClassList routines to force revalidation. Parameters ---------- @@ -929,8 +932,11 @@ def _classlist_wrapper(self, class_list: ClassList, func: Callable): @functools.wraps(func) def wrapped_func(*args, **kwargs): - """Run the given function and then revalidate the "Project" model. If any exception is raised, restore - the previous state of the given ClassList and report details of the exception. + """Run the given function and then revalidate the "Project" model. + + If any exception is raised, restore the previous state of the given ClassList + and report details of the exception. + """ previous_state = copy.deepcopy(class_list.data) return_value = None diff --git a/RATapi/run.py b/RATapi/run.py index 6b158255..17f47ecc 100644 --- a/RATapi/run.py +++ b/RATapi/run.py @@ -1,3 +1,5 @@ +"""The function used to run a RAT algorithm for a given project and controls.""" + import time from tqdm.auto import tqdm @@ -9,13 +11,13 @@ class ProgressBar: - """Creates a progress bar that gets updates from the progress event during a - calculation + """Create a progress bar that gets updates from the progress event during a calculation. Parameters ---------- - display : bool, default: True - Indicates if displaying is allowed + display : bool, default True + Indicates if displaying is allowed + """ def __init__(self, display=True): @@ -35,12 +37,13 @@ def __enter__(self): return self def updateProgress(self, event): - """Callback for the progress event. + """Update the progress bar with progress event data. Parameters ---------- event: ProgressEventData The progress event data. + """ if self.pbar is None: self.pbar = tqdm(**self.tqdm_kwargs) @@ -58,12 +61,13 @@ def __exit__(self, _exc_type, _exc_val, _traceback): class TextOutput: - """Pipes the message event to stdout + """Context manager to pipe message events to stdout. Parameters ---------- display : bool, default: True Indicates if displaying is allowed + """ def __init__(self, display=True): @@ -76,12 +80,13 @@ def __enter__(self): return self def printMessage(self, msg): - """Callback for the message event. + """Print an event message. Parameters ---------- msg: str The event message. + """ print(msg, end="") diff --git a/RATapi/utils/__init__.py b/RATapi/utils/__init__.py index e69de29b..f229eb5c 100644 --- a/RATapi/utils/__init__.py +++ b/RATapi/utils/__init__.py @@ -0,0 +1 @@ +"""Additional utilities for RATapi.""" diff --git a/RATapi/utils/convert.py b/RATapi/utils/convert.py index e831e905..35bc216b 100644 --- a/RATapi/utils/convert.py +++ b/RATapi/utils/convert.py @@ -46,7 +46,8 @@ def r1_to_project_class(filename: Union[str, PathLike]) -> Project: def zip_if_several(*params) -> Union[tuple, list[tuple]]: """Zips parameters if necessary, but can handle single-item parameters. - Examples: + Examples + -------- zip_if_several([1, 2], [3, 4]) = [(1, 3), (2, 4)] zip_if_several(1, 2, 3) = [(1, 2, 3)] @@ -79,6 +80,7 @@ def read_param(names, constrs, values, fits): ------- ClassList A list of all relevant parameters. + """ def fix_invalid_constraints(name: str, constrs: tuple[float, float], value: float) -> tuple[float, float]: @@ -334,6 +336,7 @@ def project_class_to_r1( ------- dict or None If `return_struct` is True, return the r1 struct. Else, return nothing. + """ def convert_parameters( @@ -345,7 +348,7 @@ def convert_parameters( ---------- params: ClassList A list of parameter type from the Project. - names, constrs, values, fits : str + name, constr, value, fit : str The keys for names, constraints, values and whether to fit for a type of parameter. number : str, optional, default "" @@ -355,6 +358,7 @@ def convert_parameters( ------- dict A dict of the relevant struct fields. + """ output = { name: [p.name for p in params], diff --git a/RATapi/utils/custom_errors.py b/RATapi/utils/custom_errors.py index 1778841a..425cf9ef 100644 --- a/RATapi/utils/custom_errors.py +++ b/RATapi/utils/custom_errors.py @@ -9,11 +9,11 @@ def custom_pydantic_validation_error( error_list: list[pydantic_core.ErrorDetails], custom_error_msgs: Optional[dict[str, str]] = None, ) -> list[pydantic_core.ErrorDetails]: - """Run through the list of errors generated from a pydantic ValidationError, substituting the standard error for a - PydanticCustomError for a given set of error types. + """Give Pydantic errors a better custom message with extraneous information removed. - For errors that do not have a custom error message defined, we redefine them using a PydanticCustomError to remove - the url from the error message. + We substitute the standard error for a PydanticCustomError for a given set of error types. + For errors that do not have a custom error message defined, + we redefine them using a PydanticCustomError to remove the url from the error message. Parameters ---------- diff --git a/RATapi/utils/enums.py b/RATapi/utils/enums.py index dd8e8660..06da0b1d 100644 --- a/RATapi/utils/enums.py +++ b/RATapi/utils/enums.py @@ -1,3 +1,5 @@ +"""The Enum values used in the parameters of various RATapi classes and functions.""" + from typing import Union try: @@ -7,6 +9,8 @@ class RATEnum(StrEnum): + """A subclass of StrEnum with some adjustments for variable spellings.""" + @classmethod def _missing_(cls, value: str): value = value.lower() @@ -22,25 +26,39 @@ def _missing_(cls, value: str): # Controls class Procedures(RATEnum): - """Defines the available options for procedures""" + """The available options for procedures.""" Calculate = "calculate" + """Run an Abelès reflectivity calculation and calculate chi-squared to the data.""" + Simplex = "simplex" + """Run a Nelder-Mead simplex optimisation over the fit parameters.""" + DE = "de" + """Run a Differential Evolution optimisation over the fit parameters.""" + NS = "ns" + """Run Bayesian Nested Sampling over the fit parameters.""" + DREAM = "dream" + """Run the Bayesian DREAM algorithm over the fit parameters.""" class Parallel(RATEnum): - """Defines the available options for parallelization""" + """The available options for parallelisation.""" Single = "single" + """Do not parallelise.""" + Points = "points" + """Split all contrasts into groups of points, and assign a process to each group.""" + Contrasts = "contrasts" + """Assign one process to each contrast.""" class Display(RATEnum): - """Defines the available options for display""" + """The available options for terminal output.""" Off = "off" Iter = "iter" @@ -49,14 +67,29 @@ class Display(RATEnum): class Strategies(RATEnum): - """Defines the available options for differential evolution strategies""" + """The available strategies for generating base vectors in differential evolution.""" Random = "random" + """The base vector is random.""" + LocalToBest = "local to best" + """The base vector is a combination of one randomly-selected local solution + and the best solution of the previous iteration.""" + BestWithJitter = "best jitter" + """The base vector is the best solution of the previous iteration, with a small random perturbation applied.""" + RandomWithPerVectorDither = "vector dither" + """The base vector is random, with a random scaling factor applied to each mutant. + This scaling factor is different for each mutant.""" + RandomWithPerGenerationDither = "generation dither" + """The base vector is random, with a random scaling factor applied to each mutant. + This scaling factor is the same for every mutant, and randomised every generation.""" + RandomEitherOrAlgorithm = "either or" + """The base vector is randomly chosen from either a pure random mutation, + or a pure recombination of parent parameter values.""" @classmethod def _missing_(cls, value: Union[int, str]): @@ -68,60 +101,104 @@ def _missing_(cls, value: Union[int, str]): return super()._missing_(value) def __int__(self): - # as RAT core expects strategy as an integer + """Convert the DE strategy to its hardcoded index in the internal code. + + RAT core expects strategy to be an integer, as this is how it is given to + the internal DE algorithm. + """ return list(Strategies).index(self) + 1 class BoundHandling(RATEnum): - """Defines the available options for bound handling""" + """The available options for bound handling in DREAM.""" Off = "off" + """Allow points to be sampled out of the parameter bounds.""" + Reflect = "reflect" + """If a step passes a boundary, reflect it back across the boundary.""" + Bound = "bound" + """If a step passes a boundary, set it equal to the nearest point on the boundary.""" + Fold = "fold" + """Treat the boundary as periodic and ‘wrap the step around’ to the other side of the space.""" # Models class TypeOptions(RATEnum): + """The types of signal (``Background`` and ``Resolution``).""" + Constant = "constant" + """A uniform constant signal given by a parameter.""" + Data = "data" + """A signal for each q-value given by a dataset.""" + Function = "function" + """A signal defined by a custom function.""" class BackgroundActions(RATEnum): + """Ways that the background can be applied to the model.""" + Add = "add" Subtract = "subtract" class Languages(RATEnum): + """Language options for custom files.""" + Cpp = "cpp" Python = "python" Matlab = "matlab" class Hydration(RATEnum): + """Options for the 'hydrate with' parameter of a Layer.""" + None_ = "none" BulkIn = "bulk in" BulkOut = "bulk out" class Priors(RATEnum): + """Prior distributions for parameters.""" + Uniform = "uniform" + """A uniform distribution over the parameter range.""" + Gaussian = "gaussian" + """A Gaussian distribution centred on the parameter value, + with shape defined by ``mu`` and ``sigma`` for the parameter.""" # Project class Calculations(RATEnum): + """Types of calculations that can be performed by RAT.""" + Normal = "normal" Domains = "domains" class LayerModels(RATEnum): + """Types of layer model supported by RAT.""" + CustomLayers = "custom layers" + """The layer model is given by a custom function.""" + CustomXY = "custom xy" + """The continuous SLD of the layer model is given by a custom function.""" + StandardLayers = "standard layers" + """The layer model is given by a list of ``Layer``s or ``DomainContrast``s.""" class Geometries(RATEnum): + """Where the substrate roughness is placed.""" + AirSubstrate = "air/substrate" + """The substrate roughness is placed at the end of the stack.""" + SubstrateLiquid = "substrate/liquid" + """The substrate roughness is placed at the beginning of the stack.""" diff --git a/RATapi/utils/plotting.py b/RATapi/utils/plotting.py index e1291cf0..19596198 100644 --- a/RATapi/utils/plotting.py +++ b/RATapi/utils/plotting.py @@ -1,4 +1,4 @@ -"""Plots using the matplotlib library""" +"""Plot results using the matplotlib library.""" import copy from functools import partial, wraps @@ -21,7 +21,7 @@ def plot_errorbars(ax: Axes, x: np.ndarray, y: np.ndarray, err: np.ndarray, one_sided: bool, color: str): - """Plots the error bars. + """Plot the error bars. Parameters ---------- @@ -55,7 +55,7 @@ def plot_ref_sld_helper( show_grid: bool = False, show_legend: bool = True, ): - """Clears the previous plots and updates the ref and SLD plots. + """Clear the previous plots and updates the ref and SLD plots. Parameters ---------- @@ -79,6 +79,7 @@ def plot_ref_sld_helper( Controls whether the grid is shown show_legend : bool, default: True Controls whether the lengend is shown + Returns ------- fig : matplotlib.pyplot.figure @@ -206,7 +207,7 @@ def plot_ref_sld( show_grid: bool = False, show_legend: bool = True, ) -> Union[plt.Figure, None]: - """Plots the reflectivity and SLD profiles. + """Plot the reflectivity and SLD profiles. Parameters ---------- @@ -311,8 +312,7 @@ def plot_ref_sld( class LivePlot: - """Creates a plot that gets updates from the plot event during a - calculation + """Create a plot that gets updates from the plot event during a calculation. Parameters ---------- @@ -334,16 +334,19 @@ def __enter__(self): return self.figure def _setCloseState(self, _): - """Close event handler""" + """Close event handler.""" self.closed = True def plotEvent(self, event): - """Callback for the plot event. + """Plot the figure from plot event data. + + This is a callback for the plot event. Parameters ---------- event: PlotEventData The plot event data. + """ if not self.closed and self.figure.number in plt.get_fignums(): plot_ref_sld_helper(event, self.figure) @@ -355,7 +358,9 @@ def __exit__(self, _exc_type, _exc_val, _traceback): def assert_bayesian(name: str): - """Decorator to ensure the results passed to a function are Bayesian. + """Ensure the results passed to a function are Bayesian. + + This is a decorator. Parameters ---------- @@ -663,17 +668,20 @@ def plot_contour( def panel_plot_helper(plot_func: Callable, indices: list[int]) -> matplotlib.figure.Figure: - """Helper function for panel-based plots. + """Generate a panel-based plot from a single plot function. Parameters ---------- plot_func : Callable A function which plots one parameter on an Axes object, given its index. + indices : list[int] + The list of indices to pass into ``plot_func``. Returns ------- matplotlib.figure.Figure A figure containing a grid of plots over the indices in `indices`. + """ nplots = len(indices) nrows, ncols = ceil(sqrt(nplots)), round(sqrt(nplots)) @@ -746,7 +754,6 @@ def plot_hists( If `return_fig` is True, return the figure - otherwise, return nothing. """ - # first convert names to indices if given fitname_to_index = partial(name_to_index, names=results.fitNames) diff --git a/RATapi/wrappers.py b/RATapi/wrappers.py index c0489227..ab5f5fa6 100644 --- a/RATapi/wrappers.py +++ b/RATapi/wrappers.py @@ -1,3 +1,5 @@ +"""Wrappers for the interface between RATapi and MATLAB custom files.""" + import pathlib from contextlib import suppress from typing import Callable @@ -9,12 +11,12 @@ def start_matlab(): - """Starts MATLAB asynchronously and returns a future to retrieve the engine later + """Start MATLAB asynchronously and returns a future to retrieve the engine later. Returns ------- future : matlab.engine.futureresult.FutureResult - A future used to get the actual matlab engine + A future used to get the actual matlab engine. """ future = None @@ -48,7 +50,7 @@ def __init__(self, filename: str) -> None: self.function_name = path.stem def getHandle(self) -> Callable[[ArrayLike, ArrayLike, ArrayLike, int, int], tuple[ArrayLike, float]]: - """Returns a wrapper for the custom MATLAB function + """Return a wrapper for the custom MATLAB function. Returns ------- @@ -100,7 +102,7 @@ def __init__(self, filename, function_name) -> None: self.engine = RATapi.rat_core.DylibEngine(filename, function_name) def getHandle(self) -> Callable[[ArrayLike, ArrayLike, ArrayLike, int, int], tuple[ArrayLike, float]]: - """Returns a wrapper for the custom dynamic library function + """Return a wrapper for the custom dynamic library function. Returns ------- diff --git a/pyproject.toml b/pyproject.toml index d20e5b8d..35febb57 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,9 +11,31 @@ line-length = 120 extend-exclude = ["*.ipynb"] [tool.ruff.lint] -select = ["E", "F", "UP", "B", "SIM", "I"] -ignore = ["SIM103", "SIM108"] +select = ["E", # pycodestyle errors + "F", # pyflakes + "UP", # pyupgrade + "B", # flake8-bugbear + "SIM", # flake8-simplify + "I", # isort + "D"] # pydocstyle + +ignore = ["SIM103", # needless bool + "SIM108", # if-else block instead of ternary operator + "D105", # undocumented __init__ + "D107", # undocumented magic method + "D203", # blank line before class docstring + "D213"] # multi line summary should start at second line + +# ignore docstring lints in the tests and install script +[tool.ruff.lint.per-file-ignores] +"tests/*" = ["D"] +"setup.py" = ["D"] [tool.ruff.lint.flake8-pytest-style] fixture-parentheses = false mark-parentheses = false + +[tool.ruff.lint.pydocstyle] +convention = "numpy" + + diff --git a/tests/conftest.py b/tests/conftest.py index 0b78d192..ce580c15 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6548,7 +6548,6 @@ def dream_results(): @pytest.fixture def r1_default_project(): """The Project corresponding to the data in R1defaultProject.mat.""" - project = RATapi.Project( name="defaultProject", calculation="normal", diff --git a/tests/test_classlist.py b/tests/test_classlist.py index 2a9aa881..18b75151 100644 --- a/tests/test_classlist.py +++ b/tests/test_classlist.py @@ -829,7 +829,8 @@ def test__validate_name_field(two_name_class_list: ClassList, input_dict: dict[s ) def test__validate_name_field_not_unique(two_name_class_list: ClassList, input_dict: dict[str, Any]) -> None: """We should raise a ValueError if we input values containing a name_field defined in an object in the ClassList, - accounting for case sensitivity.""" + accounting for case sensitivity. + """ with pytest.raises( ValueError, match=f"Input arguments contain the {two_name_class_list.name_field} " diff --git a/tests/test_models.py b/tests/test_models.py index aa133c79..a387dede 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -338,7 +338,6 @@ def test_contrast_bad_ratio(): @pytest.mark.filterwarnings("ignore:The following values are not recognised by this*:UserWarning") def test_type_change_clear(model): """If the type of a background or resolution is changed, it should wipe the other fields and warn the user.""" - model_instance = model( name="Test", type="constant", From d1e8e3bfb1c49263909cc278291edf09974bb3bb Mon Sep 17 00:00:00 2001 From: Paul Sharp <44529197+DrPaulSharp@users.noreply.github.com> Date: Fri, 28 Feb 2025 16:22:30 +0000 Subject: [PATCH 07/20] Removes unused properties from the API (#137) * Removes unused properties from the API * Updates "define.h" --- RATapi/inputs.py | 22 ----- cpp/RAT | 2 +- cpp/includes/defines.h | 24 ------ cpp/rat.cpp | 187 +++++++++++++++++------------------------ tests/test_inputs.py | 54 ------------ tests/test_run.py | 72 ---------------- 6 files changed, 78 insertions(+), 283 deletions(-) diff --git a/RATapi/inputs.py b/RATapi/inputs.py index 69080636..0bc00284 100644 --- a/RATapi/inputs.py +++ b/RATapi/inputs.py @@ -142,9 +142,6 @@ def make_input(project: RATapi.Project, controls: RATapi.Controls) -> tuple[Prob [[element.min, element.max] for element in getattr(project, class_list)], ) - # Use dummy value for qzshifts - limits.qzshifts = [] - if project.model == LayerModels.CustomXY: controls.calcSldDuringFit = True @@ -298,7 +295,6 @@ def make_problem(project: RATapi.Project) -> ProblemDefinition: problem.dataPresent = make_data_present(project) problem.dataLimits = data_limits problem.simulationLimits = simulation_limits - problem.oilChiDataPresent = [0] * len(project.contrasts) problem.numberOfContrasts = len(project.contrasts) problem.geometry = project.geometry problem.useImaginary = project.absorption @@ -306,7 +302,6 @@ def make_problem(project: RATapi.Project) -> ProblemDefinition: problem.contrastBackgroundParams = contrast_background_params problem.contrastBackgroundTypes = contrast_background_types problem.contrastBackgroundActions = [contrast.background_action for contrast in project.contrasts] - problem.contrastQzshifts = [1] * len(project.contrasts) # This is marked as "to do" in RAT problem.contrastScalefactors = [ project.scalefactors.index(contrast.scalefactor, True) for contrast in project.contrasts ] @@ -317,7 +312,6 @@ def make_problem(project: RATapi.Project) -> ProblemDefinition: problem.contrastResolutionTypes = contrast_resolution_types problem.backgroundParams = [param.value for param in project.background_parameters] - problem.qzshifts = [0.0] problem.scalefactors = [param.value for param in project.scalefactors] problem.bulkIns = [param.value for param in project.bulk_in] problem.bulkOuts = [param.value for param in project.bulk_out] @@ -358,18 +352,6 @@ def make_problem(project: RATapi.Project) -> ProblemDefinition: for param in getattr(project, class_list) if param.fit ] - problem.otherParams = [ - param.value - for class_list in RATapi.project.parameter_class_lists - for param in getattr(project, class_list) - if not param.fit - ] - problem.otherLimits = [ - [param.min, param.max] - for class_list in RATapi.project.parameter_class_lists - for param in getattr(project, class_list) - if not param.fit - ] problem.priorNames = [ param.name for class_list in RATapi.project.parameter_class_lists for param in getattr(project, class_list) ] @@ -392,10 +374,6 @@ def make_problem(project: RATapi.Project) -> ProblemDefinition: problem.checks, parameter_field[class_list], [int(element.fit) for element in getattr(project, class_list)] ) - # Use dummy values for qz shifts - problem.names.qzshifts = [] - problem.checks.qzshifts = [] - check_indices(problem) return problem diff --git a/cpp/RAT b/cpp/RAT index 7effee9d..62421f6d 160000 --- a/cpp/RAT +++ b/cpp/RAT @@ -1 +1 @@ -Subproject commit 7effee9dc3190ff064187bf69d7c66f3ee828622 +Subproject commit 62421f6df7e2d5bb16c628a944d5955aea609d37 diff --git a/cpp/includes/defines.h b/cpp/includes/defines.h index a9a59b7f..ffecaec3 100644 --- a/cpp/includes/defines.h +++ b/cpp/includes/defines.h @@ -397,8 +397,6 @@ backgroundParams : np.ndarray[np.float] Limits for backgroundParams in the problem definition. scalefactors : np.ndarray[np.float] Limits for scalefactors in the problem definition. -qzshifts : np.ndarray[np.float] - Limits for qzshifts in the problem definition. bulkIns : np.ndarray[np.float] Limits for bulkIns in the problem definition. bulkOuts : np.ndarray[np.float] @@ -413,7 +411,6 @@ struct Limits { py::array_t params; py::array_t backgroundParams; py::array_t scalefactors; - py::array_t qzshifts; py::array_t bulkIns; py::array_t bulkOuts; py::array_t resolutionParams; @@ -431,8 +428,6 @@ backgroundParams : list Names of backgroundParams in the problem definition. scalefactors : list Names of scalefactors in the problem definition. -qzshifts : list - Names of qzshifts in the problem definition. bulkIns : list Names of bulkIns in the problem definition. bulkName: list @@ -447,7 +442,6 @@ struct NameStore { py::list params; py::list backgroundParams; py::list scalefactors; - py::list qzshifts; py::list bulkIns; py::list bulkOuts; py::list resolutionParams; @@ -469,8 +463,6 @@ backgroundParams : np.ndarray[np.float] Non-zero values indicates which backgroundParams is fitted. scalefactors : np.ndarray[np.float] Non-zero values indicates which scalefactors is fitted. -qzshifts : np.ndarray[np.float] - Non-zero values indicates which qzshifts is fitted. bulkIns : np.ndarray[np.float] Non-zero values indicates which bulkIns is fitted. bulkOuts : np.ndarray[np.float] @@ -484,7 +476,6 @@ domainRatios : np.ndarray[np.float] struct Checks { py::array_t params; py::array_t backgroundParams; - py::array_t qzshifts; py::array_t scalefactors; py::array_t bulkIns; py::array_t bulkOuts; @@ -508,8 +499,6 @@ dataLimits : list Data limits for each contrast. simulationLimits : list; Simulation for each contrast. -oilChiDataPresent : np.ndarray[np.float] - If ``dataPresent[i]`` is non-zero, then contrast ``i`` has oilChi data. This is currently not being used. numberOfContrasts : int Number of contrasts. geometry : str @@ -525,8 +514,6 @@ contrastBackgroundTypes : list Background type for each contrast. contrastBackgroundActions : list Background action for each contrast. -contrastQzshifts : np.ndarray[np.float] - Indices of Qzshifts used for each contrast. This is currently not being used. contrastScalefactors : np.ndarray[np.float] Indices of scalefactors used for each contrast. contrastBulkIns : np.ndarray[np.float] @@ -539,8 +526,6 @@ contrastResolutionTypes : list Resolution type for each contrast. backgroundParams : np.ndarray[np.float] Background parameter values. -qzshifts : np.ndarray[np.float] - Qzshift values. This currently not being used. scalefactors : np.ndarray[np.float] Scalefactors values. bulkIns : np.ndarray[np.float] @@ -573,12 +558,8 @@ domainContrastLayers : list Indices of layers added to the model of each domain contrast. fitParams : np.ndarray[np.float] Values of fitted parameters. -otherParams : np.ndarray[np.float] - Values of non-fitted parameters. fitLimits : np.ndarray[np.float] Limits of fitted parameters. -otherLimits : np.ndarray[np.float] - Limits of non fitted parameters. priorNames : list Parameter names for for all parameters in the problem definition. priorValues : np.ndarray[np.float] @@ -596,7 +577,6 @@ struct ProblemDefinition { py::array_t dataPresent; py::list dataLimits; py::list simulationLimits; - py::array_t oilChiDataPresent; real_T numberOfContrasts; std::string geometry {}; boolean_T useImaginary {}; @@ -604,14 +584,12 @@ struct ProblemDefinition { py::list contrastBackgroundParams; py::list contrastBackgroundTypes; py::list contrastBackgroundActions; - py::array_t contrastQzshifts; py::array_t contrastScalefactors; py::array_t contrastBulkIns; py::array_t contrastBulkOuts; py::list contrastResolutionParams; py::list contrastResolutionTypes; py::array_t backgroundParams; - py::array_t qzshifts; py::array_t scalefactors; py::array_t bulkIns; py::array_t bulkOuts; @@ -628,9 +606,7 @@ struct ProblemDefinition { real_T numberOfDomainContrasts {}; py::list domainContrastLayers; py::array_t fitParams; - py::array_t otherParams; py::array_t fitLimits; - py::array_t otherLimits; py::list priorNames; py::array_t priorValues; NameStore names; diff --git a/cpp/rat.cpp b/cpp/rat.cpp index 039be9de..d8d48d68 100644 --- a/cpp/rat.cpp +++ b/cpp/rat.cpp @@ -227,7 +227,6 @@ RAT::b_ParamNames createParamNamesStruct(const NameStore& names) names_struct.params = customCaller("NameStore.params", pyListToRatCellWrap02d, names.params); names_struct.backgroundParams = customCaller("NameStore.backgroundParams", pyListToRatCellWrap02d, names.backgroundParams); names_struct.scalefactors = customCaller("NameStore.scalefactors", pyListToRatCellWrap02d, names.scalefactors); - names_struct.qzshifts = customCaller("NameStore.qzshifts", pyListToRatCellWrap02d, names.qzshifts); names_struct.bulkIns = customCaller("NameStore.bulkIns", pyListToRatCellWrap02d, names.bulkIns); names_struct.bulkOuts = customCaller("NameStore.bulkOuts", pyListToRatCellWrap02d, names.bulkOuts); names_struct.resolutionParams = customCaller("NameStore.resolutionParams", pyListToRatCellWrap02d, names.resolutionParams); @@ -243,7 +242,6 @@ RAT::CheckFlags createCheckFlagsStruct(const Checks& checks) checks_struct.params = customCaller("Checks.params", pyArrayToRatRowArray1d, checks.params); checks_struct.backgroundParams = customCaller("Checks.backgroundParams", pyArrayToRatRowArray1d, checks.backgroundParams); checks_struct.scalefactors = customCaller("Checks.scalefactors", pyArrayToRatRowArray1d, checks.scalefactors); - checks_struct.qzshifts = customCaller("Checks.qzshifts", pyArrayToRatRowArray1d, checks.qzshifts); checks_struct.bulkIns = customCaller("Checks.bulkIns", pyArrayToRatRowArray1d, checks.bulkIns); checks_struct.bulkOuts = customCaller("Checks.bulkOuts", pyArrayToRatRowArray1d, checks.bulkOuts); checks_struct.resolutionParams = customCaller("Checks.resolutionParams", pyArrayToRatRowArray1d, checks.resolutionParams); @@ -262,7 +260,6 @@ RAT::b_ProblemDefinition createProblemDefinitionStruct(const ProblemDefinition& problem_struct.dataPresent = customCaller("Problem.dataPresent", pyArrayToRatRowArray1d, problem.dataPresent); problem_struct.dataLimits = customCaller("Problem.dataLimits", pyListToRatCellWrap2, problem.dataLimits); problem_struct.simulationLimits = customCaller("Problem.simulationLimits", pyListToRatCellWrap2, problem.simulationLimits); - problem_struct.oilChiDataPresent = customCaller("Problem.oilChiDataPresent", pyArrayToRatRowArray1d, problem.oilChiDataPresent); problem_struct.numberOfContrasts = problem.numberOfContrasts; stringToRatBoundedArray(problem.geometry, problem_struct.geometry.data, problem_struct.geometry.size); problem_struct.useImaginary = problem.useImaginary; @@ -270,14 +267,12 @@ RAT::b_ProblemDefinition createProblemDefinitionStruct(const ProblemDefinition& problem_struct.contrastBackgroundParams = customCaller("Problem.contrastBackgroundParams", pyListToRatCellWrap3, problem.contrastBackgroundParams); problem_struct.contrastBackgroundTypes = customCaller("Problem.contrastBackgroundTypes", pyListToRatCellWrap02d, problem.contrastBackgroundTypes); problem_struct.contrastBackgroundActions = customCaller("Problem.contrastBackgroundActions", pyListToRatCellWrap02d, problem.contrastBackgroundActions); - problem_struct.contrastQzshifts = customCaller("Problem.contrastQzshifts", pyArrayToRatRowArray1d, problem.contrastQzshifts); problem_struct.contrastScalefactors = customCaller("Problem.contrastScalefactors", pyArrayToRatRowArray1d, problem.contrastScalefactors); problem_struct.contrastBulkIns = customCaller("Problem.contrastBulkIns", pyArrayToRatRowArray1d, problem.contrastBulkIns); problem_struct.contrastBulkOuts = customCaller("Problem.contrastBulkOuts", pyArrayToRatRowArray1d, problem.contrastBulkOuts); problem_struct.contrastResolutionParams = customCaller("Problem.contrastResolutionParams", pyListToRatCellWrap4, problem.contrastResolutionParams); problem_struct.contrastResolutionTypes = customCaller("Problem.contrastResolutionTypes", pyListToRatCellWrap02d, problem.contrastResolutionTypes); problem_struct.backgroundParams = customCaller("Problem.backgroundParams", pyArrayToRatRowArray1d, problem.backgroundParams); - problem_struct.qzshifts = customCaller("Problem.qzshifts", pyArrayToRatRowArray1d, problem.qzshifts); problem_struct.scalefactors = customCaller("Problem.scalefactors", pyArrayToRatRowArray1d, problem.scalefactors); problem_struct.bulkIns = customCaller("Problem.bulkIns", pyArrayToRatRowArray1d, problem.bulkIns); problem_struct.bulkOuts = customCaller("Problem.bulkOuts", pyArrayToRatRowArray1d, problem.bulkOuts); @@ -294,9 +289,7 @@ RAT::b_ProblemDefinition createProblemDefinitionStruct(const ProblemDefinition& problem_struct.numberOfDomainContrasts = problem.numberOfDomainContrasts; problem_struct.domainContrastLayers = customCaller("Problem.domainContrastLayers", pyListToRatCellWrap5, problem.domainContrastLayers); problem_struct.fitParams = customCaller("Problem.fitParams", pyArrayToRatRowArray1d, problem.fitParams); - problem_struct.otherParams = customCaller("Problem.otherParams", pyArrayToRatRowArray1d, problem.otherParams); problem_struct.fitLimits = customCaller("Problem.fitLimits", pyArrayToRatArray2d, problem.fitLimits); - problem_struct.otherLimits = customCaller("Problem.otherLimits", pyArrayToRatArray2d, problem.otherLimits); problem_struct.priorNames = customCaller("Problem.priorNames", pyListToRatCellWrap01d, problem.priorNames); problem_struct.priorValues = customCaller("Problem.priorValues", pyArrayToRatArray2d, problem.priorValues); @@ -314,7 +307,6 @@ RAT::ProblemLimits createProblemLimitsStruct(const Limits& limits) limits_struct.params = customCaller("Limits.params", pyArrayToRatArray2d, limits.params); limits_struct.backgroundParams = customCaller("Limits.backgroundParams", pyArrayToRatArray2d, limits.backgroundParams); limits_struct.scalefactors = customCaller("Limits.scalefactors", pyArrayToRatArray2d, limits.scalefactors); - limits_struct.qzshifts = customCaller("Limits.qzshifts", pyArrayToRatArray2d, limits.qzshifts); limits_struct.bulkIns = customCaller("Limits.bulkIns", pyArrayToRatArray2d, limits.bulkIns); limits_struct.bulkOuts = customCaller("Limits.bulkOuts", pyArrayToRatArray2d, limits.bulkOuts); limits_struct.resolutionParams = customCaller("Limits.resolutionParams", pyArrayToRatArray2d, limits.resolutionParams); @@ -480,7 +472,6 @@ ProblemDefinition problemDefinitionFromStruct(const RAT::b_ProblemDefinition pro problem_def.dataPresent = pyArrayFromRatArray1d>(problem.dataPresent); problem_def.dataLimits = pyListFromRatCellWrap2(problem.dataLimits); problem_def.simulationLimits = pyListFromRatCellWrap2(problem.simulationLimits); - problem_def.oilChiDataPresent = pyArrayFromRatArray1d>(problem.oilChiDataPresent); problem_def.numberOfContrasts = problem.numberOfContrasts; stringFromRatBoundedArray(problem.geometry.data, problem.geometry.size, problem_def.geometry); problem_def.useImaginary = problem.useImaginary; @@ -488,14 +479,12 @@ ProblemDefinition problemDefinitionFromStruct(const RAT::b_ProblemDefinition pro problem_def.contrastBackgroundParams = pyListFromBoundedCellWrap>(problem.contrastBackgroundParams); problem_def.contrastBackgroundTypes = pyListFromRatCellWrap02d(problem.contrastBackgroundTypes); problem_def.contrastBackgroundActions = pyListFromRatCellWrap02d(problem.contrastBackgroundActions); - problem_def.contrastQzshifts = pyArrayFromRatArray1d>(problem.contrastQzshifts); problem_def.contrastScalefactors = pyArrayFromRatArray1d>(problem.contrastScalefactors); problem_def.contrastBulkIns = pyArrayFromRatArray1d>(problem.contrastBulkIns); problem_def.contrastBulkOuts = pyArrayFromRatArray1d>(problem.contrastBulkOuts); problem_def.contrastResolutionParams = pyListFromBoundedCellWrap>(problem.contrastResolutionParams); problem_def.contrastResolutionTypes = pyListFromRatCellWrap02d(problem.contrastResolutionTypes); problem_def.backgroundParams = pyArrayFromRatArray1d>(problem.backgroundParams); - problem_def.qzshifts = pyArrayFromRatArray1d>(problem.qzshifts); problem_def.scalefactors = pyArrayFromRatArray1d>(problem.scalefactors); problem_def.bulkIns = pyArrayFromRatArray1d>(problem.bulkIns); problem_def.bulkOuts = pyArrayFromRatArray1d>(problem.bulkOuts); @@ -512,16 +501,13 @@ ProblemDefinition problemDefinitionFromStruct(const RAT::b_ProblemDefinition pro problem_def.numberOfDomainContrasts = problem.numberOfDomainContrasts; problem_def.domainContrastLayers = pyList1DFromRatCellWrap2D>(problem.domainContrastLayers); problem_def.fitParams = pyArrayFromRatArray1d>(problem.fitParams); - problem_def.otherParams = pyArrayFromRatArray1d>(problem.otherParams); problem_def.fitLimits = pyArrayFromRatArray2d(problem.fitLimits); - problem_def.otherLimits = pyArrayFromRatArray2d(problem.otherLimits); problem_def.priorNames = pyListFromRatCellWrap01d(problem.priorNames); problem_def.priorValues = pyArrayFromRatArray2d(problem.priorValues); problem_def.names.params = pyListFromRatCellWrap02d(problem.names.params); problem_def.names.backgroundParams = pyListFromRatCellWrap02d(problem.names.backgroundParams); problem_def.names.scalefactors = pyListFromRatCellWrap02d(problem.names.scalefactors); - problem_def.names.qzshifts = pyListFromRatCellWrap02d(problem.names.qzshifts); problem_def.names.bulkIns = pyListFromRatCellWrap02d(problem.names.bulkIns); problem_def.names.bulkOuts = pyListFromRatCellWrap02d(problem.names.bulkOuts); problem_def.names.resolutionParams = pyListFromRatCellWrap02d(problem.names.resolutionParams); @@ -531,7 +517,6 @@ ProblemDefinition problemDefinitionFromStruct(const RAT::b_ProblemDefinition pro problem_def.checks.params = pyArrayFromRatArray1d>(problem.checks.params); problem_def.checks.backgroundParams = pyArrayFromRatArray1d>(problem.checks.backgroundParams); problem_def.checks.scalefactors = pyArrayFromRatArray1d>(problem.checks.scalefactors); - problem_def.checks.qzshifts = pyArrayFromRatArray1d>(problem.checks.qzshifts); problem_def.checks.bulkIns = pyArrayFromRatArray1d>(problem.checks.bulkIns); problem_def.checks.bulkOuts = pyArrayFromRatArray1d>(problem.checks.bulkOuts); problem_def.checks.resolutionParams = pyArrayFromRatArray1d>(problem.checks.resolutionParams); @@ -864,7 +849,6 @@ PYBIND11_MODULE(rat_core, m) { .def_readwrite("params", &NameStore::params) .def_readwrite("backgroundParams", &NameStore::backgroundParams) .def_readwrite("scalefactors", &NameStore::scalefactors) - .def_readwrite("qzshifts", &NameStore::qzshifts) .def_readwrite("bulkIns", &NameStore::bulkIns) .def_readwrite("bulkOuts", &NameStore::bulkOuts) .def_readwrite("resolutionParams", &NameStore::resolutionParams) @@ -873,11 +857,11 @@ PYBIND11_MODULE(rat_core, m) { .def(py::pickle( [](const NameStore &names) { // __getstate__ /* Return a tuple that fully encodes the state of the object */ - return py::make_tuple(names.params, names.backgroundParams, names.scalefactors, names.qzshifts, names.bulkIns, names.bulkOuts, names.resolutionParams, + return py::make_tuple(names.params, names.backgroundParams, names.scalefactors, names.bulkIns, names.bulkOuts, names.resolutionParams, names.domainRatios, names.contrasts); }, [](py::tuple t) { // __setstate__ - if (t.size() != 9) + if (t.size() != 8) throw std::runtime_error("Encountered invalid state unpickling NameStore object!"); /* Create a new C++ instance */ @@ -886,12 +870,11 @@ PYBIND11_MODULE(rat_core, m) { names.params = t[0].cast(); names.backgroundParams = t[1].cast(); names.scalefactors = t[2].cast(); - names.qzshifts = t[3].cast(); - names.bulkIns = t[4].cast(); - names.bulkOuts = t[5].cast(); - names.resolutionParams = t[6].cast(); - names.domainRatios = t[7].cast(); - names.contrasts = t[8].cast(); + names.bulkIns = t[3].cast(); + names.bulkOuts = t[4].cast(); + names.resolutionParams = t[5].cast(); + names.domainRatios = t[6].cast(); + names.contrasts = t[7].cast(); return names; })); @@ -901,7 +884,6 @@ PYBIND11_MODULE(rat_core, m) { .def_readwrite("params", &Checks::params) .def_readwrite("backgroundParams", &Checks::backgroundParams) .def_readwrite("scalefactors", &Checks::scalefactors) - .def_readwrite("qzshifts", &Checks::qzshifts) .def_readwrite("bulkIns", &Checks::bulkIns) .def_readwrite("bulkOuts", &Checks::bulkOuts) .def_readwrite("resolutionParams", &Checks::resolutionParams) @@ -909,11 +891,11 @@ PYBIND11_MODULE(rat_core, m) { .def(py::pickle( [](const Checks &chk) { // __getstate__ /* Return a tuple that fully encodes the state of the object */ - return py::make_tuple(chk.params, chk.backgroundParams, chk.scalefactors, chk.qzshifts, chk.bulkIns, chk.bulkOuts, + return py::make_tuple(chk.params, chk.backgroundParams, chk.scalefactors, chk.bulkIns, chk.bulkOuts, chk.resolutionParams, chk.domainRatios); }, [](py::tuple t) { // __setstate__ - if (t.size() != 8) + if (t.size() != 7) throw std::runtime_error("Encountered invalid state unpickling Checks object!"); /* Create a new C++ instance */ @@ -921,12 +903,11 @@ PYBIND11_MODULE(rat_core, m) { chk.params = t[0].cast>(); chk.backgroundParams = t[1].cast>(); - chk.scalefactors = t[2].cast>(); - chk.qzshifts = t[3].cast>(); - chk.bulkIns = t[4].cast>(); - chk.bulkOuts = t[5].cast>(); - chk.resolutionParams = t[6].cast>(); - chk.domainRatios = t[7].cast>(); + chk.scalefactors = t[2].cast>(); + chk.bulkIns = t[3].cast>(); + chk.bulkOuts = t[4].cast>(); + chk.resolutionParams = t[5].cast>(); + chk.domainRatios = t[6].cast>(); return chk; })); @@ -936,7 +917,6 @@ PYBIND11_MODULE(rat_core, m) { .def_readwrite("params", &Limits::params) .def_readwrite("backgroundParams", &Limits::backgroundParams) .def_readwrite("scalefactors", &Limits::scalefactors) - .def_readwrite("qzshifts", &Limits::qzshifts) .def_readwrite("bulkIns", &Limits::bulkIns) .def_readwrite("bulkOuts", &Limits::bulkOuts) .def_readwrite("resolutionParams", &Limits::resolutionParams) @@ -944,11 +924,11 @@ PYBIND11_MODULE(rat_core, m) { .def(py::pickle( [](const Limits &lim) { // __getstate__ /* Return a tuple that fully encodes the state of the object */ - return py::make_tuple(lim.params, lim.backgroundParams, lim.scalefactors, lim.qzshifts, lim.bulkIns, lim.bulkOuts, + return py::make_tuple(lim.params, lim.backgroundParams, lim.scalefactors, lim.bulkIns, lim.bulkOuts, lim.resolutionParams, lim.domainRatios); }, [](py::tuple t) { // __setstate__ - if (t.size() != 8) + if (t.size() != 7) throw std::runtime_error("Encountered invalid state unpickling Limits object!"); /* Create a new C++ instance */ @@ -957,11 +937,10 @@ PYBIND11_MODULE(rat_core, m) { lim.params = t[0].cast>(); lim.backgroundParams = t[1].cast>(); lim.scalefactors = t[2].cast>(); - lim.qzshifts = t[3].cast>(); - lim.bulkIns = t[4].cast>(); - lim.bulkOuts = t[5].cast>(); - lim.resolutionParams = t[6].cast>(); - lim.domainRatios = t[7].cast>(); + lim.bulkIns = t[3].cast>(); + lim.bulkOuts = t[4].cast>(); + lim.resolutionParams = t[5].cast>(); + lim.domainRatios = t[6].cast>(); return lim; })); @@ -1055,7 +1034,6 @@ PYBIND11_MODULE(rat_core, m) { .def_readwrite("dataPresent", &ProblemDefinition::dataPresent) .def_readwrite("dataLimits", &ProblemDefinition::dataLimits) .def_readwrite("simulationLimits", &ProblemDefinition::simulationLimits) - .def_readwrite("oilChiDataPresent", &ProblemDefinition::oilChiDataPresent) .def_readwrite("numberOfContrasts", &ProblemDefinition::numberOfContrasts) .def_readwrite("geometry", &ProblemDefinition::geometry) .def_readwrite("useImaginary", &ProblemDefinition::useImaginary) @@ -1063,14 +1041,12 @@ PYBIND11_MODULE(rat_core, m) { .def_readwrite("contrastBackgroundParams", &ProblemDefinition::contrastBackgroundParams) .def_readwrite("contrastBackgroundTypes", &ProblemDefinition::contrastBackgroundTypes) .def_readwrite("contrastBackgroundActions", &ProblemDefinition::contrastBackgroundActions) - .def_readwrite("contrastQzshifts", &ProblemDefinition::contrastQzshifts) .def_readwrite("contrastScalefactors", &ProblemDefinition::contrastScalefactors) .def_readwrite("contrastBulkIns", &ProblemDefinition::contrastBulkIns) .def_readwrite("contrastBulkOuts", &ProblemDefinition::contrastBulkOuts) .def_readwrite("contrastResolutionParams", &ProblemDefinition::contrastResolutionParams) .def_readwrite("contrastResolutionTypes", &ProblemDefinition::contrastResolutionTypes) .def_readwrite("backgroundParams", &ProblemDefinition::backgroundParams) - .def_readwrite("qzshifts", &ProblemDefinition::qzshifts) .def_readwrite("scalefactors", &ProblemDefinition::scalefactors) .def_readwrite("bulkIns", &ProblemDefinition::bulkIns) .def_readwrite("bulkOuts", &ProblemDefinition::bulkOuts) @@ -1087,9 +1063,7 @@ PYBIND11_MODULE(rat_core, m) { .def_readwrite("numberOfDomainContrasts", &ProblemDefinition::numberOfDomainContrasts) .def_readwrite("domainContrastLayers", &ProblemDefinition::domainContrastLayers) .def_readwrite("fitParams", &ProblemDefinition::fitParams) - .def_readwrite("otherParams", &ProblemDefinition::otherParams) .def_readwrite("fitLimits", &ProblemDefinition::fitLimits) - .def_readwrite("otherLimits", &ProblemDefinition::otherLimits) .def_readwrite("priorNames", &ProblemDefinition::priorNames) .def_readwrite("priorValues", &ProblemDefinition::priorValues) .def_readwrite("names", &ProblemDefinition::names) @@ -1097,21 +1071,21 @@ PYBIND11_MODULE(rat_core, m) { .def(py::pickle( [](const ProblemDefinition &p) { // __getstate__ /* Return a tuple that fully encodes the state of the object */ - return py::make_tuple(p.TF, p.resample, p.data, p.dataPresent, p.dataLimits, p.simulationLimits, p.oilChiDataPresent, + return py::make_tuple(p.TF, p.resample, p.data, p.dataPresent, p.dataLimits, p.simulationLimits, p.numberOfContrasts, p.geometry, p.useImaginary, p.repeatLayers, p.contrastBackgroundParams, p.contrastBackgroundTypes, p.contrastBackgroundActions, - p.contrastQzshifts, p.contrastScalefactors, p.contrastBulkIns, p.contrastBulkOuts, - p.contrastResolutionParams, p.contrastResolutionTypes, p.backgroundParams, p.qzshifts, p.scalefactors, - p.bulkIns, p.bulkOuts, p.resolutionParams, p.params, p.numberOfLayers, p.contrastLayers, p.layersDetails, + p.contrastScalefactors, p.contrastBulkIns, p.contrastBulkOuts, p.contrastResolutionParams, + p.contrastResolutionTypes, p.backgroundParams, p.scalefactors, p.bulkIns, p.bulkOuts, + p.resolutionParams, p.params, p.numberOfLayers, p.contrastLayers, p.layersDetails, p.customFiles, p.modelType, p.contrastCustomFiles, p.contrastDomainRatios, p.domainRatios, - p.numberOfDomainContrasts, p.domainContrastLayers, p.fitParams, p.otherParams, p.fitLimits, - p.otherLimits, p.priorNames, p.priorValues, p.names.params, p.names.backgroundParams, p.names.scalefactors, - p.names.qzshifts, p.names.bulkIns, p.names.bulkOuts, p.names.resolutionParams, p.names.domainRatios, - p.names.contrasts, p.checks.params, p.checks.backgroundParams, p.checks.scalefactors, p.checks.qzshifts, + p.numberOfDomainContrasts, p.domainContrastLayers, p.fitParams, p.fitLimits, p.priorNames, p.priorValues, + p.names.params, p.names.backgroundParams, p.names.scalefactors, p.names.bulkIns, + p.names.bulkOuts, p.names.resolutionParams, p.names.domainRatios, p.names.contrasts, + p.checks.params, p.checks.backgroundParams, p.checks.scalefactors, p.checks.bulkIns, p.checks.bulkOuts, p.checks.resolutionParams, p.checks.domainRatios); }, [](py::tuple t) { // __setstate__ - if (t.size() != 60) + if (t.size() != 53) throw std::runtime_error("Encountered invalid state unpickling ProblemDefinition object!"); /* Create a new C++ instance */ @@ -1123,62 +1097,55 @@ PYBIND11_MODULE(rat_core, m) { p.dataPresent = t[3].cast>(); p.dataLimits = t[4].cast(); p.simulationLimits = t[5].cast(); - p.oilChiDataPresent = t[6].cast>(); - p.numberOfContrasts = t[7].cast(); - p.geometry = t[8].cast(); - p.useImaginary = t[9].cast(); - p.repeatLayers = t[10].cast(); - p.contrastBackgroundParams = t[11].cast(); - p.contrastBackgroundTypes = t[12].cast(); - p.contrastBackgroundActions = t[13].cast(); - p.contrastQzshifts = t[14].cast>(); - p.contrastScalefactors = t[15].cast>(); - p.contrastBulkIns = t[16].cast>(); - p.contrastBulkOuts = t[17].cast>(); - p.contrastResolutionParams = t[18].cast(); - p.contrastResolutionTypes = t[19].cast(); - p.backgroundParams = t[20].cast>(); - p.qzshifts = t[21].cast>(); - p.scalefactors = t[22].cast>(); - p.bulkIns = t[23].cast>(); - p.bulkOuts = t[24].cast>(); - p.resolutionParams = t[25].cast>(); - p.params = t[26].cast>(); - p.numberOfLayers = t[27].cast(); - p.contrastLayers = t[28].cast(); - p.layersDetails = t[29].cast(); - p.customFiles = t[30].cast(); - p.modelType = t[31].cast(); - p.contrastCustomFiles = t[32].cast>(); - p.contrastDomainRatios = t[33].cast>(); - p.domainRatios = t[34].cast>(); - p.numberOfDomainContrasts = t[35].cast(); - p.domainContrastLayers = t[36].cast(); - p.fitParams = t[37].cast>(); - p.otherParams = t[38].cast>(); - p.fitLimits = t[39].cast>(); - p.otherLimits = t[40].cast>(); - p.priorNames = t[41].cast(); - p.priorValues = t[42].cast>(); + p.numberOfContrasts = t[6].cast(); + p.geometry = t[7].cast(); + p.useImaginary = t[8].cast(); + p.repeatLayers = t[9].cast(); + p.contrastBackgroundParams = t[10].cast(); + p.contrastBackgroundTypes = t[11].cast(); + p.contrastBackgroundActions = t[12].cast(); + p.contrastScalefactors = t[13].cast>(); + p.contrastBulkIns = t[14].cast>(); + p.contrastBulkOuts = t[15].cast>(); + p.contrastResolutionParams = t[16].cast(); + p.contrastResolutionTypes = t[17].cast(); + p.backgroundParams = t[18].cast>(); + p.scalefactors = t[19].cast>(); + p.bulkIns = t[20].cast>(); + p.bulkOuts = t[21].cast>(); + p.resolutionParams = t[22].cast>(); + p.params = t[23].cast>(); + p.numberOfLayers = t[24].cast(); + p.contrastLayers = t[25].cast(); + p.layersDetails = t[26].cast(); + p.customFiles = t[27].cast(); + p.modelType = t[28].cast(); + p.contrastCustomFiles = t[29].cast>(); + p.contrastDomainRatios = t[30].cast>(); + p.domainRatios = t[31].cast>(); + p.numberOfDomainContrasts = t[32].cast(); + p.domainContrastLayers = t[33].cast(); + p.fitParams = t[34].cast>(); + p.fitLimits = t[35].cast>(); + p.priorNames = t[36].cast(); + p.priorValues = t[37].cast>(); - p.names.params = t[43].cast(); - p.names.backgroundParams = t[44].cast(); - p.names.scalefactors = t[45].cast(); - p.names.qzshifts = t[46].cast(); - p.names.bulkIns = t[47].cast(); - p.names.bulkOuts = t[48].cast(); - p.names.resolutionParams = t[49].cast(); - p.names.domainRatios = t[50].cast(); - p.names.contrasts = t[51].cast(); - - p.checks.params = t[52].cast>(); - p.checks.backgroundParams = t[53].cast>(); - p.checks.scalefactors = t[54].cast>(); - p.checks.qzshifts = t[55].cast>(); - p.checks.bulkIns = t[56].cast>(); - p.checks.bulkOuts = t[57].cast>(); - p.checks.resolutionParams = t[58].cast>(); - p.checks.domainRatios = t[59].cast>(); + p.names.params = t[38].cast(); + p.names.backgroundParams = t[39].cast(); + p.names.scalefactors = t[40].cast(); + p.names.bulkIns = t[41].cast(); + p.names.bulkOuts = t[42].cast(); + p.names.resolutionParams = t[43].cast(); + p.names.domainRatios = t[44].cast(); + p.names.contrasts = t[45].cast(); + + p.checks.params = t[46].cast>(); + p.checks.backgroundParams = t[47].cast>(); + p.checks.scalefactors = t[48].cast>(); + p.checks.bulkIns = t[49].cast>(); + p.checks.bulkOuts = t[50].cast>(); + p.checks.resolutionParams = t[51].cast>(); + p.checks.domainRatios = t[52].cast>(); return p; })); diff --git a/tests/test_inputs.py b/tests/test_inputs.py index f9bf3ae8..d931ff5f 100644 --- a/tests/test_inputs.py +++ b/tests/test_inputs.py @@ -118,7 +118,6 @@ def test_names(): names.params = ["Substrate Roughness", "Test Thickness", "Test SLD", "Test Roughness"] names.backgroundParams = ["Background Param 1"] names.scalefactors = ["Scalefactor 1"] - names.qzshifts = [] names.bulkIns = ["SLD Air"] names.bulkOuts = ["SLD D2O"] names.resolutionParams = ["Resolution Param 1"] @@ -135,7 +134,6 @@ def test_checks(): checks.params = np.array([1, 0, 0, 0]) checks.backgroundParams = np.array([0]) checks.scalefactors = np.array([0]) - checks.qzshifts = np.array([]) checks.bulkIns = np.array([0]) checks.bulkOuts = np.array([0]) checks.resolutionParams = np.array([0]) @@ -155,14 +153,12 @@ def standard_layers_problem(test_names, test_checks): problem.params = [3.0, 0.0, 0.0, 0.0] problem.bulkIns = [0.0] problem.bulkOuts = [6.35e-06] - problem.qzshifts = [] problem.scalefactors = [0.23] problem.domainRatios = [] problem.backgroundParams = [1e-06] problem.resolutionParams = [0.03] problem.contrastBulkIns = [1] problem.contrastBulkOuts = [1] - problem.contrastQzshifts = [] problem.contrastScalefactors = [1] problem.contrastBackgroundParams = [[1]] problem.contrastBackgroundActions = [BackgroundActions.Add] @@ -176,7 +172,6 @@ def standard_layers_problem(test_names, test_checks): problem.data = [np.array([[1.0, 1.0, 1.0, 0.0, 0.0, 0.0]])] problem.dataLimits = [[1.0, 1.0]] problem.simulationLimits = [[1.0, 1.0]] - problem.oilChiDataPresent = [0] problem.numberOfContrasts = 1 problem.numberOfLayers = 1 problem.repeatLayers = [[0, 1]] @@ -185,18 +180,7 @@ def standard_layers_problem(test_names, test_checks): problem.numberOfDomainContrasts = 0 problem.domainContrastLayers = [] problem.fitParams = [3.0] - problem.otherParams = [0.0, 0.0, 0.0, 1e-06, 0.23, 0.0, 6.35e-06, 0.03] problem.fitLimits = [[1.0, 5.0]] - problem.otherLimits = [ - [0.0, 0.0], - [0.0, 0.0], - [0.0, 0.0], - [1e-07, 1e-05], - [0.02, 0.25], - [0.0, 0.0], - [6.2e-06, 6.35e-06], - [0.01, 0.05], - ] problem.priorNames = [ "Substrate Roughness", "Test Thickness", @@ -237,14 +221,12 @@ def domains_problem(test_names, test_checks): problem.params = [3.0, 0.0, 0.0, 0.0] problem.bulkIns = [0.0] problem.bulkOuts = [6.35e-06] - problem.qzshifts = [] problem.scalefactors = [0.23] problem.domainRatios = [0.5] problem.backgroundParams = [1e-06] problem.resolutionParams = [0.03] problem.contrastBulkIns = [1] problem.contrastBulkOuts = [1] - problem.contrastQzshifts = [] problem.contrastScalefactors = [1] problem.contrastBackgroundParams = [[1]] problem.contrastBackgroundActions = [BackgroundActions.Add] @@ -258,7 +240,6 @@ def domains_problem(test_names, test_checks): problem.data = [np.array([[1.0, 1.0, 1.0, 0.0, 0.0, 0.0]])] problem.dataLimits = [[1.0, 1.0]] problem.simulationLimits = [[1.0, 1.0]] - problem.oilChiDataPresent = [0] problem.numberOfContrasts = 1 problem.numberOfLayers = 1 problem.repeatLayers = [[0, 1]] @@ -267,19 +248,7 @@ def domains_problem(test_names, test_checks): problem.numberOfDomainContrasts = 2 problem.domainContrastLayers = [[1], [1]] problem.fitParams = [3.0] - problem.otherParams = [0.0, 0.0, 0.0, 1e-06, 0.23, 0.0, 6.35e-06, 0.03, 0.5] problem.fitLimits = [[1.0, 5.0]] - problem.otherLimits = [ - [0.0, 0.0], - [0.0, 0.0], - [0.0, 0.0], - [1e-07, 1e-05], - [0.02, 0.25], - [0.0, 0.0], - [6.2e-06, 6.35e-06], - [0.01, 0.05], - [0.4, 0.6], - ] problem.priorNames = [ "Substrate Roughness", "Test Thickness", @@ -323,14 +292,12 @@ def custom_xy_problem(test_names, test_checks): problem.params = [3.0, 0.0, 0.0, 0.0] problem.bulkIns = [0.0] problem.bulkOuts = [6.35e-06] - problem.qzshifts = [] problem.scalefactors = [0.23] problem.domainRatios = [] problem.backgroundParams = [1e-06] problem.resolutionParams = [0.03] problem.contrastBulkIns = [1] problem.contrastBulkOuts = [1] - problem.contrastQzshifts = [] problem.contrastScalefactors = [1] problem.contrastBackgroundParams = [[1]] problem.contrastBackgroundActions = [BackgroundActions.Add] @@ -344,7 +311,6 @@ def custom_xy_problem(test_names, test_checks): problem.data = [np.empty([0, 6])] problem.dataLimits = [[0.0, 0.0]] problem.simulationLimits = [[0.005, 0.7]] - problem.oilChiDataPresent = [0] problem.repeatLayers = [[0, 1]] problem.layersDetails = [] problem.contrastLayers = [[]] @@ -353,18 +319,7 @@ def custom_xy_problem(test_names, test_checks): problem.numberOfDomainContrasts = 0 problem.domainContrastLayers = [] problem.fitParams = [3.0] - problem.otherParams = [0.0, 0.0, 0.0, 1e-06, 0.23, 0.0, 6.35e-06, 0.03] problem.fitLimits = [[1.0, 5.0]] - problem.otherLimits = [ - [0.0, 0.0], - [0.0, 0.0], - [0.0, 0.0], - [1e-07, 1e-05], - [0.02, 0.25], - [0.0, 0.0], - [6.2e-06, 6.35e-06], - [0.01, 0.05], - ] problem.priorNames = [ "Substrate Roughness", "Test Thickness", @@ -403,7 +358,6 @@ def normal_limits(): limits.params = [[1.0, 5.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0]] limits.backgroundParams = [[1e-7, 1e-5]] limits.scalefactors = [[0.02, 0.25]] - limits.qzshifts = [] limits.bulkIns = [[0.0, 0.0]] limits.bulkOuts = [[6.2e-6, 6.35e-6]] limits.resolutionParams = [[0.01, 0.05]] @@ -419,7 +373,6 @@ def domains_limits(): limits.params = [[1.0, 5.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0]] limits.backgroundParams = [[1e-7, 1e-5]] limits.scalefactors = [[0.02, 0.25]] - limits.qzshifts = [] limits.bulkIns = [[0.0, 0.0]] limits.bulkOuts = [[6.2e-6, 6.35e-6]] limits.resolutionParams = [[0.01, 0.05]] @@ -540,7 +493,6 @@ def test_make_input(test_project, test_problem, test_limits, test_controls, requ "params", "backgroundParams", "scalefactors", - "qzshifts", "bulkIns", "bulkOuts", "resolutionParams", @@ -723,7 +675,6 @@ def check_problem_equal(actual_problem, expected_problem) -> None: array_fields = [ "params", "backgroundParams", - "qzshifts", "scalefactors", "bulkIns", "bulkOuts", @@ -731,7 +682,6 @@ def check_problem_equal(actual_problem, expected_problem) -> None: "domainRatios", "contrastBackgroundParams", "contrastBackgroundActions", - "contrastQzshifts", "contrastScalefactors", "contrastBulkIns", "contrastBulkOuts", @@ -741,21 +691,17 @@ def check_problem_equal(actual_problem, expected_problem) -> None: "dataPresent", "dataLimits", "simulationLimits", - "oilChiDataPresent", "repeatLayers", "contrastLayers", "domainContrastLayers", "fitParams", - "otherParams", "fitLimits", - "otherLimits", "priorValues", ] checks_fields = [ "params", "backgroundParams", "scalefactors", - "qzshifts", "bulkIns", "bulkOuts", "resolutionParams", diff --git a/tests/test_run.py b/tests/test_run.py index 85f7b64c..f488de4a 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -56,14 +56,12 @@ def reflectivity_calculation_problem(): ) problem.bulkIns = np.array([2.073e-06]) problem.bulkOuts = np.array([5.98e-06, 2.21e-06]) - problem.qzshifts = np.array([0.0]) problem.scalefactors = np.array([0.1, 0.15]) problem.domainRatios = np.array([]) problem.backgroundParams = np.array([2.23e-06, 3.38e-06]) problem.resolutionParams = np.array([0.03]) problem.contrastBulkIns = np.array([1.0, 1.0]) problem.contrastBulkOuts = np.array([1.0, 2.0]) - problem.contrastQzshifts = np.array([0.0, 0.0]) problem.contrastScalefactors = np.array([1.0, 2.0]) problem.contrastBackgroundParams = [[1.0], [2.0]] problem.contrastBackgroundTypes = ["constant", "constant"] @@ -76,7 +74,6 @@ def reflectivity_calculation_problem(): problem.dataPresent = np.array([1.0, 1.0]) problem.dataLimits = [[0.011403, 0.59342], [0.011403, 0.59342]] problem.simulationLimits = [[0.011403, 0.59342], [0.011403, 0.59342]] - problem.oilChiDataPresent = np.array([0.0, 0.0]) problem.numberOfContrasts = 2.0 problem.numberOfLayers = 6.0 problem.repeatLayers = [[0.0, 1.0], [0.0, 1.0]] @@ -116,21 +113,6 @@ def reflectivity_calculation_problem(): 2.210e-06, ], ) - problem.otherParams = np.array( - [ - 3.390e-06, - -4.010e-07, - 0.000e00, - 1.750e-06, - 1.470e-06, - -4.610e-07, - 1.000e02, - 1.000e-01, - 1.500e-01, - 2.073e-06, - 3.000e-02, - ], - ) problem.fitLimits = np.array( [ [1.00e00, 1.00e01], @@ -153,21 +135,6 @@ def reflectivity_calculation_problem(): [1.00e-06, 4.99e-06], ], ) - problem.otherLimits = np.array( - [ - [3.39e-06, 3.41e-06], - [-5.00e-07, -3.00e-07], - [0.00e00, 1.00e-09], - [1.00e-07, 2.00e-06], - [5.00e-07, 1.50e-06], - [-5.00e-07, 0.00e00], - [9.99e01, 1.00e02], - [5.00e-02, 2.00e-01], - [5.00e-02, 2.00e-01], - [2.00e-06, 2.10e-06], - [1.00e-02, 5.00e-02], - ], - ) problem.names.params = [ "Substrate Roughness", "Oxide Thickness", @@ -193,7 +160,6 @@ def reflectivity_calculation_problem(): ] problem.names.backgroundParams = ["Background parameter D2O", "Background parameter SMW"] problem.names.scalefactors = ["Scalefactor 1", "Scalefactor 2"] - problem.names.qzshifts = [] problem.names.bulkIns = ["Silicon"] problem.names.bulkOuts = ["D2O", "SMW"] problem.names.resolutionParams = ["Resolution Param 1"] @@ -204,7 +170,6 @@ def reflectivity_calculation_problem(): ) problem.checks.backgroundParams = np.array([1.0, 1.0]) problem.checks.scalefactors = np.array([0.0, 0.0]) - problem.checks.qzshifts = np.array([]) problem.checks.bulkIns = np.array([0.0]) problem.checks.bulkOuts = np.array([1.0, 1.0]) problem.checks.resolutionParams = np.array([0.0]) @@ -252,14 +217,12 @@ def dream_problem(): ) problem.bulkIns = np.array([2.073e-06]) problem.bulkOuts = np.array([6.01489149e-06, 1.59371685e-06]) - problem.qzshifts = np.array([0.0]) problem.scalefactors = np.array([0.1, 0.15]) problem.domainRatios = np.array([]) problem.backgroundParams = np.array([2.37113128e-06, 1.99006694e-06]) problem.resolutionParams = np.array([0.03]) problem.contrastBulkIns = np.array([1.0, 1.0]) problem.contrastBulkOuts = np.array([1.0, 2.0]) - problem.contrastQzshifts = np.array([0.0, 0.0]) problem.contrastScalefactors = np.array([1.0, 2.0]) problem.contrastBackgroundParams = [[1.0], [2.0]] problem.contrastBackgroundTypes = ["constant", "constant"] @@ -272,7 +235,6 @@ def dream_problem(): problem.dataPresent = np.array([1.0, 1.0]) problem.dataLimits = [[0.011403, 0.59342], [0.011403, 0.59342]] problem.simulationLimits = [[0.011403, 0.59342], [0.011403, 0.59342]] - problem.oilChiDataPresent = np.array([0.0, 0.0]) problem.numberOfContrasts = 2.0 problem.numberOfLayers = 6.0 problem.repeatLayers = [[0.0, 1.0], [0.0, 1.0]] @@ -312,22 +274,6 @@ def dream_problem(): 1.59371685e-06, ], ) - problem.otherParams = np.array( - [ - 3.390e-06, - -4.010e-07, - 0.000e00, - 1.750e-06, - 1.470e-06, - -4.610e-07, - 1.000e02, - 1.000e-01, - 1.500e-01, - 2.073e-06, - 3.000e-02, - 0.000e00, - ], - ) problem.fitLimits = np.array( [ [1.00e00, 1.00e01], @@ -350,22 +296,6 @@ def dream_problem(): [1.00e-06, 4.99e-06], ], ) - problem.otherLimits = np.array( - [ - [3.39e-06, 3.41e-06], - [-5.00e-07, -3.00e-07], - [0.00e00, 1.00e-09], - [1.00e-07, 2.00e-06], - [5.00e-07, 1.50e-06], - [-5.00e-07, 0.00e00], - [9.99e01, 1.00e02], - [5.00e-02, 2.00e-01], - [5.00e-02, 2.00e-01], - [2.00e-06, 2.10e-06], - [1.00e-02, 5.00e-02], - [0.00e00, 0.00e00], - ], - ) problem.names.params = [ "Substrate Roughness", "Oxide Thickness", @@ -391,7 +321,6 @@ def dream_problem(): ] problem.names.backgroundParams = ["Background parameter D2O", "Background parameter SMW"] problem.names.scalefactors = ["Scalefactor 1", "Scalefactor 2"] - problem.names.qzshifts = [] problem.names.bulkIns = ["Silicon"] problem.names.bulkOuts = ["D2O", "SMW"] problem.names.resolutionParams = ["Resolution Param 1"] @@ -402,7 +331,6 @@ def dream_problem(): ) problem.checks.backgroundParams = np.array([1.0, 1.0]) problem.checks.scalefactors = np.array([0.0, 0.0]) - problem.checks.qzshifts = np.array([]) problem.checks.bulkIns = np.array([0.0]) problem.checks.bulkOuts = np.array([1.0, 1.0]) problem.checks.resolutionParams = np.array([0.0]) From 3148cfb76639c7a1a2016581e66b05c46553c82b Mon Sep 17 00:00:00 2001 From: Paul Sharp <44529197+DrPaulSharp@users.noreply.github.com> Date: Wed, 5 Mar 2025 15:31:31 +0000 Subject: [PATCH 08/20] Bumps python version to 3.10 and matlab engine version to 2023a (#138) * Bumps python version to 3.10 and matlab engine version to 2023a * Adds ability to use "set_fields" with parameter names * Addresses review comment --- .github/workflows/build_wheel.yml | 2 +- .github/workflows/run_tests.yml | 2 +- CONTRIBUTING.md | 2 +- RATapi/classlist.py | 6 +++++- setup.py | 10 ++++------ tests/test_classlist.py | 7 +++++-- 6 files changed, 17 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build_wheel.yml b/.github/workflows/build_wheel.yml index 6d6c896a..7f0205b5 100644 --- a/.github/workflows/build_wheel.yml +++ b/.github/workflows/build_wheel.yml @@ -15,7 +15,7 @@ jobs: env: CIBW_SKIP: 'pp*' CIBW_ARCHS: 'auto64' - CIBW_PROJECT_REQUIRES_PYTHON: '>=3.9' + CIBW_PROJECT_REQUIRES_PYTHON: '>=3.10' CIBW_TEST_REQUIRES: 'pytest' defaults: run: diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 99582ff8..e07c6f90 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -20,7 +20,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-13] - version: ["3.9", "3.13"] + version: ["3.10", "3.13"] defaults: run: shell: bash -l {0} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 157a515f..6980c2c2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,7 +5,7 @@ issue has not been reported already) or submitting a pull request. Create Developer Environment ---------------------------- -This project targets Python 3.9 or later. Install an appropriate version of Python and other dependencies +This project targets Python 3.10 or later. Install an appropriate version of Python and other dependencies Then create a fork of the python-RAT repo, and clone the fork diff --git a/RATapi/classlist.py b/RATapi/classlist.py index 23e85477..c3e1d04f 100644 --- a/RATapi/classlist.py +++ b/RATapi/classlist.py @@ -296,11 +296,15 @@ def extend(self, other: Sequence[T]) -> None: self._check_unique_name_fields(other) self.data.extend(other) - def set_fields(self, index: int, **kwargs) -> None: + def set_fields(self, index: Union[int, slice, str, T], **kwargs) -> None: """Assign the values of an existing object's attributes using keyword arguments.""" self._validate_name_field(kwargs) pydantic_object = False + # Find index if name or object is supplied + if isinstance(index, (str, self._class_handle)): + index = self.index(index) + if importlib.util.find_spec("pydantic"): # Pydantic is installed, so set up a context manager that will # suppress custom validation errors until all fields have been set. diff --git a/setup.py b/setup.py index 84e7c9ce..a3bb0ea4 100644 --- a/setup.py +++ b/setup.py @@ -165,7 +165,7 @@ def build_libraries(self, libraries): cmdclass={"build_clib": BuildClib, "build_ext": BuildExt}, libraries=[libevent], ext_modules=ext_modules, - python_requires=">=3.9", + python_requires=">=3.10", install_requires=[ "numpy >= 1.20", "prettytable >= 3.9.0", @@ -178,13 +178,11 @@ def build_libraries(self, libraries): ':python_version < "3.11"': ["StrEnum >= 0.4.15"], "Dev": ["pytest>=7.4.0", "pytest-cov>=4.1.0", "ruff>=0.4.10"], "Matlab_latest": ["matlabengine"], - "Matlab_2024a": ["matlabengine == 24.1.*"], + "Matlab_2025a": ["matlabengine == 25.1.*"], + "Matlab_2024b": ["matlabengine == 24.2.2"], + "Matlab_2024a": ["matlabengine == 24.1.4"], "Matlab_2023b": ["matlabengine == 23.2.3"], "Matlab_2023a": ["matlabengine == 9.14.3"], - "Matlab_2022b": ["matlabengine == 9.13.9"], - "Matlab_2022a": ["matlabengine == 9.12.19"], - "Matlab_2021b": ["matlabengine == 9.11.21"], - "Matlab_2021a": ["matlabengine == 9.10.3"], }, zip_safe=False, ) diff --git a/tests/test_classlist.py b/tests/test_classlist.py index 18b75151..3fa874ab 100644 --- a/tests/test_classlist.py +++ b/tests/test_classlist.py @@ -729,6 +729,7 @@ def test_extend_empty_classlist(extended_list: Sequence, one_name_class_list: Cl assert isinstance(extended_list[-1], class_list._class_handle) +@pytest.mark.parametrize("index", [0, "Alice", InputAttributes(name="Alice")]) @pytest.mark.parametrize( ["new_values", "expected_classlist"], [ @@ -739,10 +740,12 @@ def test_extend_empty_classlist(extended_list: Sequence, one_name_class_list: Cl ), ], ) -def test_set_fields(two_name_class_list: ClassList, new_values: dict[str, Any], expected_classlist: ClassList) -> None: +def test_set_fields( + two_name_class_list: ClassList, index: Union[int, str], new_values: dict[str, Any], expected_classlist: ClassList +) -> None: """We should be able to set field values in an element of a ClassList using keyword arguments.""" class_list = two_name_class_list - class_list.set_fields(0, **new_values) + class_list.set_fields(index, **new_values) assert class_list == expected_classlist From 8746e33930793190eb49da8cdff73d237bd3ae28 Mon Sep 17 00:00:00 2001 From: "Alex H. Room" <69592136+alexhroom@users.noreply.github.com> Date: Tue, 11 Mar 2025 09:45:15 +0000 Subject: [PATCH 09/20] adds Jeffreys prior to `Priors` enum and updates submodule (#139) * added jeffreys to enum * update submodule --- RATapi/utils/enums.py | 3 +++ cpp/RAT | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/RATapi/utils/enums.py b/RATapi/utils/enums.py index 06da0b1d..550b583f 100644 --- a/RATapi/utils/enums.py +++ b/RATapi/utils/enums.py @@ -172,6 +172,9 @@ class Priors(RATEnum): """A Gaussian distribution centred on the parameter value, with shape defined by ``mu`` and ``sigma`` for the parameter.""" + Jeffreys = "jeffreys" + """A Jeffreys' prior distribution over the parameter range.""" + # Project class Calculations(RATEnum): diff --git a/cpp/RAT b/cpp/RAT index 62421f6d..282d17be 160000 --- a/cpp/RAT +++ b/cpp/RAT @@ -1 +1 @@ -Subproject commit 62421f6df7e2d5bb16c628a944d5955aea609d37 +Subproject commit 282d17beb964af0855c1b2cc412e0b187b1a4140 From 8db92c2cec82b80f8f55c7f1a582ac4e1d5cc077 Mon Sep 17 00:00:00 2001 From: "Alex H. Room" <69592136+alexhroom@users.noreply.github.com> Date: Fri, 14 Mar 2025 11:37:45 +0000 Subject: [PATCH 10/20] Adds orsopy integration for `.ort` files and ORSO models (#131) * written ort file reader * made model parsing only if the model is there * fixed union method * added orso integration notebook * added tests * added pint to requirements and added orso extras install * backwards compatibility * removed to-project and updated notebook * fixed print formatting * review fixes * fixed repeat names * fix iterator * new data * updated example data and better name resolution * removed some data * review fixes --- .github/workflows/run_tests.yml | 2 +- RATapi/__init__.py | 4 +- RATapi/classlist.py | 13 + RATapi/examples/data/c_PLP0011859_q.ort | 441 ++++++++++++++++++ .../orso_integration/orso_integration.ipynb | 188 ++++++++ RATapi/utils/orso.py | 248 ++++++++++ requirements.txt | 2 + setup.py | 3 +- tests/test_data/bare_substrate.json | 1 + tests/test_data/bare_substrate.ort | 83 ++++ tests/test_data/prist.json | 1 + tests/test_data/prist5_10K_m_025.Rqz.ort | 124 +++++ tests/test_orso_utils.py | 119 +++++ 13 files changed, 1225 insertions(+), 4 deletions(-) create mode 100644 RATapi/examples/data/c_PLP0011859_q.ort create mode 100644 RATapi/examples/orso_integration/orso_integration.ipynb create mode 100644 RATapi/utils/orso.py create mode 100644 tests/test_data/bare_substrate.json create mode 100644 tests/test_data/bare_substrate.ort create mode 100644 tests/test_data/prist.json create mode 100644 tests/test_data/prist5_10K_m_025.Rqz.ort create mode 100644 tests/test_orso_utils.py diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index e07c6f90..8911dfdc 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -53,5 +53,5 @@ jobs: - name: Install and Test with pytest run: | export PATH="$pythonLocation:$PATH" - python -m pip install -e .[Dev] + python -m pip install -e .[Dev,Orso] pytest tests/ --cov=RATapi --cov-report=term diff --git a/RATapi/__init__.py b/RATapi/__init__.py index 8484ea45..1c0b820f 100644 --- a/RATapi/__init__.py +++ b/RATapi/__init__.py @@ -6,6 +6,6 @@ from RATapi.controls import Controls from RATapi.project import Project from RATapi.run import run -from RATapi.utils import convert, plotting +from RATapi.utils import convert, orso, plotting -__all__ = ["examples", "models", "events", "ClassList", "Controls", "Project", "run", "plotting", "convert"] +__all__ = ["examples", "models", "events", "ClassList", "Controls", "Project", "run", "plotting", "convert", "orso"] diff --git a/RATapi/classlist.py b/RATapi/classlist.py index c3e1d04f..b046f885 100644 --- a/RATapi/classlist.py +++ b/RATapi/classlist.py @@ -296,6 +296,19 @@ def extend(self, other: Sequence[T]) -> None: self._check_unique_name_fields(other) self.data.extend(other) + def union(self, other: Sequence[T]) -> None: + """Extend the ClassList by a sequence, ignoring input items with names that already exist.""" + if other and not (isinstance(other, Sequence) and not isinstance(other, str)): + other = [other] + + self.extend( + [ + item + for item in other + if hasattr(item, self.name_field) and getattr(item, self.name_field) not in self.get_names() + ] + ) + def set_fields(self, index: Union[int, slice, str, T], **kwargs) -> None: """Assign the values of an existing object's attributes using keyword arguments.""" self._validate_name_field(kwargs) diff --git a/RATapi/examples/data/c_PLP0011859_q.ort b/RATapi/examples/data/c_PLP0011859_q.ort new file mode 100644 index 00000000..7a7bf1fd --- /dev/null +++ b/RATapi/examples/data/c_PLP0011859_q.ort @@ -0,0 +1,441 @@ +# # ORSO reflectivity data file | 1.1 standard | YAML encoding | https://www.reflectometry.org/ +# # handwritten test file header created to test RAT orsopy integration! +# # example data from refnx: https://refnx.readthedocs.io/en/latest/getting_started.html#fitting-a-neutron-reflectometry-dataset +# data_source: +# owner: +# name: null +# affiliation: null +# measurement: +# instrument_settings: null +# data_files: null +# experiment: +# title: +# probe: neutron +# instrument: None +# start_date: 1970-01-01T00:00:00 +# sample: +# name: film on silicon +# model: +# stack: Si | SiO2 30 | film 250 | D2O +# materials: +# film: +# sld: 2.0e-6 +# roughness: 3 +# reduction: +# software: null +# timestamp: null +# data_set: 0 +# columns: +# - {name: Qz, unit: 1/angstrom, physical_quantity: normal momentum transfer} +# - {name: R, unit: '', physical_quantity: specular reflectivity} +# - {error_of: R, error_type: uncertainty, value_is: sigma} +# - {error_of: Qz, error_type: resolution, value_is: sigma} +# # Qz (1/angstrom) R () sR sQz +0.00806022 0.709581 0.0850676 0.000331422 +0.00813662 0.862281 0.11237 0.000334619 +0.00826375 0.908647 0.0790047 0.000339939 +0.00837067 0.773292 0.0792728 0.000344412 +0.00845033 1.05797 0.125959 0.000347744 +0.00853083 1.01566 0.113295 0.000351111 +0.00861217 0.734717 0.0611566 0.000354512 +0.00869437 0.769216 0.0617058 0.000357949 +0.00877743 1.11574 0.11273 0.000361421 +0.00886136 0.972303 0.089716 0.000364929 +0.00894616 0.751214 0.0549393 0.000368473 +0.00903185 0.797649 0.0567122 0.000372053 +0.00911844 0.922189 0.0685841 0.000375671 +0.00920593 0.975755 0.0729395 0.000379325 +0.00929432 0.819504 0.0521617 0.000383017 +0.00938364 0.78832 0.0479473 0.000386748 +0.00947389 0.794701 0.0460224 0.000390516 +0.00956508 0.8744 0.0515164 0.000394323 +0.00965721 0.839662 0.0474285 0.00039817 +0.0097503 0.800872 0.0439958 0.000402055 +0.00984436 1.1171 0.073733 0.000405981 +0.00993939 0.888411 0.049541 0.000409947 +0.0100354 0.779129 0.0389873 0.000413953 +0.0101324 0.799968 0.0389974 0.000418001 +0.0102304 0.843124 0.041598 0.00042209 +0.0103294 0.961332 0.0492536 0.00042622 +0.0104868 0.880544 0.0299083 0.000432781 +0.0106327 0.755735 0.0320851 0.000438868 +0.0107359 0.971231 0.0453485 0.00044317 +0.0108401 0.895549 0.0390542 0.000447516 +0.0109454 0.862589 0.0358072 0.000451906 +0.0110518 0.890992 0.0361257 0.000456342 +0.0111593 0.900348 0.0367924 0.000460822 +0.0112679 0.845927 0.0321881 0.000465348 +0.0113776 0.943152 0.0365533 0.000469921 +0.0114884 0.995631 0.0390116 0.00047454 +0.0116004 0.969594 0.0363623 0.000479205 +0.0117135 0.905181 0.032041 0.000483919 +0.0118278 0.893381 0.0306119 0.00048868 +0.0119433 0.919602 0.0314677 0.00049349 +0.0120599 0.918998 0.0306097 0.000498349 +0.0121777 0.781056 0.0235542 0.000503257 +0.0122968 0.864915 0.0272025 0.000508215 +0.012417 0.843516 0.0255015 0.000513223 +0.0125385 0.998418 0.0319722 0.000518283 +0.0126612 0.88126 0.0260273 0.000523393 +0.0127852 0.883569 0.0255862 0.000528556 +0.0129105 0.93767 0.0274651 0.000533771 +0.013037 1.0192 0.0305107 0.000539039 +0.0131648 0.845526 0.0226269 0.00054436 +0.0132939 0.873804 0.0232468 0.000549735 +0.0134243 0.865953 0.0224906 0.000555165 +0.0135561 0.877982 0.0224491 0.00056065 +0.0136892 0.947545 0.0248985 0.00056619 +0.0138237 0.888154 0.0220962 0.000571787 +0.0139595 0.891362 0.0218148 0.00057744 +0.0140967 0.888456 0.0215092 0.00058315 +0.0142353 0.913717 0.0222586 0.000588919 +0.0143753 0.810364 0.0188289 0.000594745 +0.0145168 0.738548 0.0168827 0.000600631 +0.0146597 0.68651 0.0159742 0.000606576 +0.014804 0.58224 0.0135021 0.000612582 +0.0149498 0.446855 0.0100626 0.000618648 +0.015097 0.392461 0.00915549 0.000624775 +0.0152458 0.320517 0.00731959 0.000630965 +0.0153961 0.281006 0.00639909 0.000637217 +0.0155479 0.2401 0.00544239 0.000643532 +0.0157012 0.220881 0.00502437 0.000649911 +0.0158561 0.192033 0.00431441 0.000656354 +0.0160126 0.179849 0.00405159 0.000662863 +0.0161707 0.160069 0.00356202 0.000669437 +0.0163303 0.153129 0.00346777 0.000676078 +0.0164916 0.13422 0.003016 0.000682786 +0.0166545 0.12833 0.00288853 0.000689562 +0.016819 0.124794 0.00286182 0.000696406 +0.0169853 0.109127 0.0024831 0.000703319 +0.0171532 0.104429 0.00235392 0.000710302 +0.0173228 0.094683 0.00209162 0.000717355 +0.0175881 0.0896911 0.00143938 0.000728391 +0.0178419 0.0809144 0.00187278 0.000738945 +0.0180185 0.0746544 0.00171422 0.000746287 +0.0181968 0.0703661 0.00163423 0.000753704 +0.018377 0.0690445 0.0016053 0.000761195 +0.0185589 0.0627055 0.00146146 0.000768762 +0.0187427 0.0593915 0.00140007 0.000776405 +0.0189284 0.0575477 0.00136095 0.000784125 +0.0191159 0.0513833 0.00122668 0.000791923 +0.0193054 0.0492267 0.00117824 0.0007998 +0.0194967 0.0452174 0.00107962 0.000807755 +0.01969 0.0424556 0.00102225 0.000815792 +0.0198852 0.0412613 0.000994781 0.000823909 +0.0200824 0.0352333 0.00087779 0.000832108 +0.0202815 0.0335271 0.000836747 0.000840389 +0.0204827 0.0332684 0.000833959 0.000848754 +0.0206859 0.0316644 0.000789886 0.000857204 +0.0208912 0.02916 0.000749613 0.000865738 +0.0210985 0.0265201 0.000706383 0.000874359 +0.021308 0.0251829 0.000671389 0.000883066 +0.0215195 0.0238757 0.000642693 0.000891861 +0.0217331 0.0228929 0.000633068 0.000900745 +0.021949 0.0208646 0.000593128 0.000909718 +0.0221669 0.0208771 0.00059201 0.000918781 +0.0223871 0.0182228 0.000529205 0.000927936 +0.0226095 0.0177346 0.000525152 0.000937183 +0.0228341 0.0158714 0.000481877 0.000946523 +0.023061 0.0143255 0.000451619 0.000955957 +0.0232902 0.0142776 0.000452249 0.000965486 +0.0235217 0.0126624 0.000419845 0.000975112 +0.0237555 0.0122128 0.000413786 0.000984834 +0.0239917 0.0104608 0.000374532 0.000994654 +0.0242302 0.0106133 0.000381052 0.00100457 +0.0244712 0.00987903 0.000364128 0.00101459 +0.0247145 0.00837203 0.000331393 0.00102471 +0.0249603 0.00767048 0.000307705 0.00103493 +0.0252086 0.00734489 0.000304978 0.00104526 +0.0254594 0.00679865 0.000289868 0.00105569 +0.0257127 0.0059163 0.000267178 0.00106622 +0.0259685 0.00534498 0.000251536 0.00107686 +0.0262269 0.00512265 0.000247412 0.0010876 +0.026488 0.00475031 0.000237953 0.00109846 +0.0267516 0.00430715 0.00022385 0.00110942 +0.0270179 0.00401817 0.000220051 0.0011205 +0.0272868 0.00353915 0.000204653 0.00113168 +0.0275585 0.00381819 0.000207944 0.00114298 +0.0278329 0.00286475 0.000181921 0.0011544 +0.02811 0.0027958 0.000176691 0.00116592 +0.02839 0.0026215 0.000175002 0.00117757 +0.0286727 0.00248477 0.000169456 0.00118933 +0.0289583 0.00242009 0.000170925 0.00120121 +0.0292467 0.00235926 0.00017006 0.0012132 +0.0295381 0.00197856 0.000160021 0.00122532 +0.0298323 0.0019472 0.000158282 0.00123757 +0.0301296 0.00173593 0.000152731 0.00124993 +0.0304298 0.00189459 0.000160305 0.00126242 +0.030733 0.00169668 0.000152574 0.00127504 +0.0310392 0.00179369 0.000159245 0.00128778 +0.0313486 0.00178686 0.000155202 0.00130065 +0.031661 0.00187201 0.000158395 0.00131365 +0.0319766 0.00168818 0.000149984 0.00132678 +0.0322953 0.00173237 0.000154325 0.00134004 +0.0326173 0.0015376 0.000145396 0.00135344 +0.0329424 0.00154134 0.000149756 0.00136697 +0.0332708 0.00170033 0.000157208 0.00138064 +0.0336026 0.00214224 0.000172887 0.00139445 +0.0339376 0.00194402 0.000168689 0.0014084 +0.0342205 0.001914 0.000107714 0.00141748 +0.0346645 0.00198641 0.000143828 0.00143727 +0.0350219 0.00184974 0.000127974 0.00145177 +0.035385 0.00193264 0.000122708 0.00146643 +0.0357162 0.00218899 0.000149598 0.00148083 +0.0360773 0.00231432 0.000150345 0.00149568 +0.0364677 0.00183177 0.000104159 0.00151093 +0.0368263 0.00179715 0.000101292 0.00152596 +0.0371616 0.00227874 0.000141733 0.0015409 +0.0375421 0.00218339 0.00012946 0.00155639 +0.0379404 0.001799 9.62498e-05 0.00157213 +0.0383128 0.00182808 9.73464e-05 0.00158778 +0.038684 0.00194985 0.00010826 0.00160356 +0.0390692 0.00204214 0.000112245 0.00161957 +0.0394734 0.00165946 8.39554e-05 0.00163584 +0.0398633 0.00162031 8.07338e-05 0.00165215 +0.0402658 0.00158862 7.61769e-05 0.00166867 +0.0406608 0.00158251 7.82912e-05 0.0016853 +0.0410701 0.0014299 7.03445e-05 0.00170214 +0.0414781 0.00133612 6.49882e-05 0.00171913 +0.0418744 0.0017219 9.20006e-05 0.00173625 +0.0423049 0.00140489 6.78817e-05 0.00175363 +0.0427317 0.00114648 5.31515e-05 0.00177115 +0.0431546 0.00103013 4.8728e-05 0.00178884 +0.0435835 0.00101864 4.85163e-05 0.0018067 +0.0440155 0.00106025 5.14888e-05 0.00182475 +0.0444549 0.000911561 4.51767e-05 0.00184298 +0.0448983 0.000779536 3.74676e-05 0.00186138 +0.045347 0.000595757 2.86778e-05 0.00187998 +0.0457947 0.000647645 3.28669e-05 0.00189877 +0.0462512 0.000526839 2.66621e-05 0.00191774 +0.0467124 0.000436546 2.24095e-05 0.00193691 +0.0471783 0.000369605 1.92965e-05 0.00195626 +0.0476469 0.000331564 1.80855e-05 0.00197583 +0.0481208 0.000254584 1.44062e-05 0.00199557 +0.0485994 0.00020908 1.31243e-05 0.00201553 +0.0490828 0.000192937 1.26159e-05 0.00203569 +0.049571 0.000130922 9.99222e-06 0.00205605 +0.050064 0.000109823 9.08051e-06 0.00207663 +0.0505619 9.56548e-05 8.37009e-06 0.00209741 +0.0510655 7.92572e-05 7.69913e-06 0.00211839 +0.0515734 7.95605e-05 6.89856e-06 0.0021396 +0.0520864 7.22495e-05 6.10273e-06 0.00216102 +0.0526045 7.82513e-05 6.39136e-06 0.00218266 +0.0531279 8.63712e-05 6.56186e-06 0.00220452 +0.0536565 0.000133162 8.78984e-06 0.0022266 +0.0541905 0.000143632 8.17884e-06 0.00224891 +0.0547298 0.000158682 8.56534e-06 0.00227144 +0.0552745 0.000190007 9.75524e-06 0.00229421 +0.0558247 0.00023542 1.14172e-05 0.00231721 +0.0563804 0.000233932 1.03452e-05 0.00234044 +0.0569417 0.00024339 1.05955e-05 0.0023639 +0.0575087 0.000270858 1.12513e-05 0.00238761 +0.0580813 0.000310966 1.23355e-05 0.00241156 +0.0586597 0.000348327 1.35786e-05 0.00243575 +0.0592439 0.000332957 1.27559e-05 0.0024602 +0.059834 0.000354659 1.32236e-05 0.00248489 +0.06043 0.000354467 1.30661e-05 0.00250983 +0.0610319 0.000385515 1.40893e-05 0.00253502 +0.06164 0.000332509 1.20176e-05 0.00256048 +0.0622541 0.000333087 1.19715e-05 0.00258619 +0.0628744 0.000330307 1.19723e-05 0.00261217 +0.0635009 0.000322469 1.16291e-05 0.00263842 +0.0641337 0.000274462 9.94879e-06 0.00266493 +0.0647728 0.000279243 1.02537e-05 0.00269171 +0.0654184 0.000238938 8.96085e-06 0.00271877 +0.0660704 0.000233028 8.72665e-06 0.00274611 +0.066729 0.000186343 7.23614e-06 0.00277372 +0.0673942 0.000164949 6.6195e-06 0.00280162 +0.068066 0.000133244 5.54157e-06 0.00282981 +0.0687446 0.000115376 4.97162e-06 0.00285828 +0.06943 8.25033e-05 3.95115e-06 0.00288705 +0.0701223 6.71923e-05 3.57018e-06 0.00291611 +0.0708215 5.03322e-05 2.92623e-06 0.00294547 +0.0715278 3.33612e-05 2.5097e-06 0.00297513 +0.0722411 2.29052e-05 2.11638e-06 0.0030051 +0.0729338 1.87944e-05 1.86053e-06 0.00303317 +0.0736683 1.55344e-05 1.79821e-06 0.00306431 +0.0744015 1.82452e-05 1.95184e-06 0.00309508 +0.0751405 2.23562e-05 1.801e-06 0.00312604 +0.0758721 2.77362e-05 1.89014e-06 0.00315615 +0.0766402 3.93101e-05 2.30095e-06 0.00318888 +0.0773964 4.59669e-05 2.3324e-06 0.00322041 +0.0781061 5.54131e-05 2.45926e-06 0.00324807 +0.0788682 6.72605e-05 2.72957e-06 0.00327954 +0.0797953 7.65064e-05 2.77309e-06 0.00331828 +0.0807289 7.79472e-05 2.85741e-06 0.00335684 +0.0815334 8.50542e-05 2.9904e-06 0.00339073 +0.0823318 9.68569e-05 3.2227e-06 0.00342487 +0.0831605 9.4624e-05 3.16621e-06 0.00345952 +0.0840252 8.58564e-05 2.82446e-06 0.00349469 +0.0848648 8.10144e-05 2.66254e-06 0.00353001 +0.0857342 7.65592e-05 2.54978e-06 0.00356583 +0.0865553 7.38543e-05 2.48584e-06 0.00360169 +0.0874372 6.24971e-05 2.19092e-06 0.00363822 +0.0883163 5.49543e-05 2.00127e-06 0.00367507 +0.0891108 5.69454e-05 2.08158e-06 0.00371178 +0.0900416 4.2292e-05 1.72971e-06 0.00374959 +0.0909546 3.36754e-05 1.45891e-06 0.00378764 +0.0918504 2.54564e-05 1.24963e-06 0.00382594 +0.0927435 2.09221e-05 1.15976e-06 0.00386459 +0.0936289 1.53112e-05 1.06014e-06 0.00390356 +0.0945527 1.26565e-05 9.57753e-07 0.0039431 +0.0955178 9.33124e-06 8.62139e-07 0.0039832 +0.0964771 8.90152e-06 7.82897e-07 0.00402366 +0.0974189 1.07915e-05 8.93847e-07 0.0040644 +0.0984259 1.21368e-05 9.11308e-07 0.00410584 +0.0994201 1.5408e-05 9.07777e-07 0.0041476 +0.100425 1.93803e-05 9.60253e-07 0.00418981 +0.101434 2.09116e-05 1.04089e-06 0.00423243 +0.102499 2.51925e-05 1.07837e-06 0.0042757 +0.103498 2.8188e-05 1.13217e-06 0.00431908 +0.104546 2.9981e-05 1.17853e-06 0.0043631 +0.105608 3.05628e-05 1.15049e-06 0.0044076 +0.106682 2.81903e-05 1.09907e-06 0.00445257 +0.107758 2.69263e-05 1.0374e-06 0.00449797 +0.108831 2.54192e-05 1.01256e-06 0.0045438 +0.109916 2.2827e-05 9.3671e-07 0.00459012 +0.11104 1.76917e-05 7.9045e-07 0.00463704 +0.112117 1.54751e-05 7.6254e-07 0.00468423 +0.113244 1.17258e-05 6.68805e-07 0.00473208 +0.114319 1.05027e-05 6.59805e-07 0.00478019 +0.115464 8.12153e-06 5.84703e-07 0.00482905 +0.116645 4.98884e-06 5.26719e-07 0.00487853 +0.117762 4.90999e-06 5.36096e-07 0.00492824 +0.118925 5.30434e-06 5.24084e-07 0.00497863 +0.120129 5.68905e-06 4.91234e-07 0.00502965 +0.121346 6.44471e-06 5.06107e-07 0.00508123 +0.122586 7.45619e-06 5.05803e-07 0.0051334 +0.123823 8.24842e-06 5.0256e-07 0.00518608 +0.125091 1.06749e-05 5.84539e-07 0.00523939 +0.126361 1.13162e-05 5.72228e-07 0.00529323 +0.127643 1.1044e-05 5.76255e-07 0.00534765 +0.128932 1.06258e-05 5.65534e-07 0.00540265 +0.13021 9.33766e-06 5.25472e-07 0.00545814 +0.131547 8.9228e-06 5.00068e-07 0.00551441 +0.132854 6.43611e-06 4.44688e-07 0.00557113 +0.134162 6.18113e-06 4.55625e-07 0.00562844 +0.135515 4.68568e-06 4.03478e-07 0.00568647 +0.136824 4.66754e-06 3.80929e-07 0.00574496 +0.138204 4.2625e-06 3.76301e-07 0.00580426 +0.139589 3.8843e-06 3.57158e-07 0.00586419 +0.140989 3.69948e-06 3.79662e-07 0.00592478 +0.14243 3.65993e-06 3.54338e-07 0.00598611 +0.143851 4.55939e-06 3.66569e-07 0.00604802 +0.145263 4.58088e-06 3.47062e-07 0.00611055 +0.14675 4.82929e-06 3.50392e-07 0.00617393 +0.148225 4.93092e-06 3.5333e-07 0.00623794 +0.14971 4.781e-06 3.61256e-07 0.00630265 +0.151225 4.64484e-06 3.39711e-07 0.00636812 +0.152736 4.36508e-06 3.32459e-07 0.00643425 +0.154261 3.80706e-06 3.25378e-07 0.00650113 +0.155825 3.54415e-06 3.05388e-07 0.00656881 +0.157366 2.63269e-06 2.77355e-07 0.00663715 +0.158941 2.12968e-06 2.57061e-07 0.0067063 +0.160511 2.50908e-06 2.68417e-07 0.00677617 +0.162137 2.60626e-06 2.68354e-07 0.00684694 +0.163755 2.46796e-06 2.61811e-07 0.00691843 +0.165404 2.43769e-06 2.45708e-07 0.00699078 +0.167035 2.81846e-06 2.62635e-07 0.00706385 +0.168736 3.20191e-06 2.67722e-07 0.00713789 +0.17041 3.02096e-06 2.54866e-07 0.00721266 +0.172119 2.39855e-06 2.32464e-07 0.00728832 +0.173839 2.45824e-06 2.38546e-07 0.00736483 +0.175574 2.03366e-06 2.23196e-07 0.00744222 +0.177336 1.68404e-06 2.04828e-07 0.00752051 +0.179087 1.29383e-06 2.00765e-07 0.00759963 +0.180907 1.43967e-06 1.88207e-07 0.00767979 +0.182715 1.5402e-06 1.99998e-07 0.0077608 +0.184535 1.33849e-06 1.86482e-07 0.00784273 +0.186383 1.61599e-06 1.86369e-07 0.00792563 +0.188247 1.49347e-06 1.88386e-07 0.00800949 +0.190126 2.05968e-06 1.9207e-07 0.00809433 +0.192038 1.69015e-06 1.7677e-07 0.00818019 +0.193954 1.48837e-06 1.79274e-07 0.00826701 +0.1959 1.40382e-06 1.74728e-07 0.00835488 +0.19786 1.34689e-06 1.70569e-07 0.00844376 +0.199845 9.4771e-07 1.61779e-07 0.00853372 +0.201833 1.08955e-06 1.73502e-07 0.00862469 +0.203863 1.24635e-06 1.65466e-07 0.0087168 +0.205904 1.22737e-06 1.6449e-07 0.00880998 +0.207959 1.16896e-06 1.58456e-07 0.00890425 +0.210042 1.16172e-06 1.52198e-07 0.00899967 +0.21214 1.21689e-06 1.58854e-07 0.00909622 +0.214257 1.31757e-06 1.54607e-07 0.00919392 +0.216399 1.03279e-06 1.47292e-07 0.00929281 +0.21857 1.03066e-06 1.51473e-07 0.00939292 +0.220781 5.94535e-07 1.51172e-07 0.00949429 +0.222988 7.27996e-07 1.42928e-07 0.00959682 +0.225216 7.80631e-07 1.48525e-07 0.0097006 +0.227467 1.06339e-06 1.4993e-07 0.00980565 +0.229741 6.52697e-07 1.29668e-07 0.00991199 +0.232037 1.07438e-06 1.48453e-07 0.0100196 +0.234356 8.68763e-07 1.46862e-07 0.0101286 +0.236698 9.21868e-07 1.45082e-07 0.0102389 +0.239064 6.47149e-07 1.36809e-07 0.0103506 +0.241454 5.36393e-07 1.35688e-07 0.0104637 +0.243867 6.33717e-07 1.30403e-07 0.0105782 +0.246304 6.2927e-07 1.36557e-07 0.0106942 +0.248766 6.33292e-07 1.22453e-07 0.0108116 +0.251253 1.03705e-06 1.35409e-07 0.0109304 +0.253764 8.25286e-07 1.43161e-07 0.0110508 +0.256301 6.26825e-07 1.19257e-07 0.0111727 +0.258863 5.25259e-07 1.22741e-07 0.0112962 +0.26145 5.21832e-07 1.30918e-07 0.0114212 +0.264064 3.91659e-07 1.2137e-07 0.0115479 +0.266703 4.72439e-07 1.39973e-07 0.0116762 +0.269369 5.59536e-07 1.39151e-07 0.0118061 +0.272062 6.6407e-07 1.44867e-07 0.0119377 +0.274782 4.59378e-07 1.51466e-07 0.012071 +0.277529 3.66961e-07 1.43546e-07 0.0122061 +0.280303 5.31531e-07 1.48308e-07 0.012343 +0.283105 4.28914e-07 1.36145e-07 0.0124816 +0.285935 5.52006e-07 1.41983e-07 0.0126221 +0.288793 5.70264e-07 1.529e-07 0.0127644 +0.29168 5.04731e-07 1.36429e-07 0.0129086 +0.294596 6.61923e-07 1.40793e-07 0.0130548 +0.297541 7.60132e-07 1.60721e-07 0.0132029 +0.300516 4.68527e-07 1.34887e-07 0.0133529 +0.30352 4.4286e-07 1.47134e-07 0.0135051 +0.306554 4.89979e-07 1.4043e-07 0.0136592 +0.309619 3.60163e-07 1.33728e-07 0.0138155 +0.312714 2.90563e-07 1.48062e-07 0.0139739 +0.315841 5.19963e-07 1.51562e-07 0.0141344 +0.318998 4.66665e-07 1.56256e-07 0.0142972 +0.322187 3.88883e-07 1.57011e-07 0.0144622 +0.325408 4.46778e-07 1.55512e-07 0.0146295 +0.328662 3.23885e-07 1.52586e-07 0.0147991 +0.331947 3.78736e-07 1.5818e-07 0.0149711 +0.335266 3.88199e-07 1.53911e-07 0.0151455 +0.338618 4.42877e-07 1.49098e-07 0.0153224 +0.342003 2.47787e-07 1.46321e-07 0.0155017 +0.345423 2.85769e-07 1.35135e-07 0.0156836 +0.348876 4.75964e-07 1.53664e-07 0.0158681 +0.352364 4.49639e-07 1.53956e-07 0.0160552 +0.355887 2.47147e-07 1.44322e-07 0.0162449 +0.359445 2.16764e-07 1.49181e-07 0.0164375 +0.363039 5.20585e-07 1.57369e-07 0.0166327 +0.366669 5.24697e-07 1.54541e-07 0.0168309 +0.370335 3.64403e-07 1.55024e-07 0.0170319 +0.374037 4.59752e-07 1.65308e-07 0.0172358 +0.377777 5.35922e-07 1.82043e-07 0.0174427 +0.381554 4.26807e-07 1.77003e-07 0.0176527 +0.385369 4.03666e-07 1.8111e-07 0.0178658 +0.389222 2.55454e-07 1.59729e-07 0.018082 +0.393113 9.26972e-08 1.65402e-07 0.0183015 +0.397044 1.10672e-07 1.80888e-07 0.0185242 +0.401014 4.52163e-07 1.73191e-07 0.0187503 +0.405023 3.78066e-07 1.51597e-07 0.0189798 +0.409073 3.09136e-07 1.57195e-07 0.0192128 +0.413163 3.41774e-07 1.44818e-07 0.0194493 +0.417294 3.44924e-07 1.59581e-07 0.0196894 +0.421466 2.5184e-07 1.59757e-07 0.0199332 +0.42568 4.01737e-07 1.53814e-07 0.0201807 +0.429936 3.17279e-07 1.6913e-07 0.0204321 +0.434235 5.50631e-07 1.61142e-07 0.0206873 +0.438577 5.0851e-07 1.6499e-07 0.0209465 +0.442962 6.02593e-07 1.73835e-07 0.0212097 +0.447391 4.38454e-07 1.6535e-07 0.0214771 +0.451865 3.38757e-07 1.87639e-07 0.0217487 +0.456383 4.35846e-07 1.97826e-07 0.0220245 +0.460946 3.85579e-07 1.76143e-07 0.0223047 +0.465555 3.83415e-07 1.88454e-07 0.0225894 diff --git a/RATapi/examples/orso_integration/orso_integration.ipynb b/RATapi/examples/orso_integration/orso_integration.ipynb new file mode 100644 index 00000000..aa6f0797 --- /dev/null +++ b/RATapi/examples/orso_integration/orso_integration.ipynb @@ -0,0 +1,188 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# ``orsopy`` Integration" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "``python-RAT`` contains some integration with ``orsopy``, allowing for convenient interaction with the ``.ort`` file format. This integration is available through the `RATapi.utils.orso` submodule." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import RATapi.utils.orso" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Creating models from the ORSO model description language\n", + "\n", + "The [ORSO model description format](https://www.reflectometry.org/advanced_and_expert_level/file_format/simple_model) allows the description of a standard slab model as a one-line string, provided that all the layer materials are defined [in the ORSO SLD database](https://slddb.esss.dk/slddb/).\n", + "\n", + "The function `RATapi.utils.orso.orso_model_to_rat` function can read a model and return an `ORSOSample` dataclass, which gives bulk in and bulk out parameters for the model, a list of all layers defined in the model, and all the parameters needed to define those layers as RAT models. \n", + "\n", + "**Note:** the ORSO format gives the thicknesses of materials in *nanometres*. When we convert them to RAT parameters, the units will be converted to Angstroms.\n", + "\n", + "For example, the string `air | Ni 100 | SiO2 0.5 | Si` describes a 1000 angstrom nickel film backed by a 5 angstrom silicon oxide layer. The bulk-in and bulk-out are air and silicon respectively. The roughnesses and SLDs will be calculated or taken from the ORSO SLD database." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# create the RAT parameters and layers from this model\n", + "sample = RATapi.utils.orso.orso_model_to_rat(\"air | Ni 100 | SiO2 0.5 | Si\")\n", + "print(sample)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can also set `absorption=True` and the model will account for absorption. For example if we change the nickel film for a boron carbide film and want to account for its relatively high absorption, we can add it to the output:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sample = RATapi.utils.orso.orso_model_to_rat(\"vacuum | B4C 100 | SiO2 0.5 | Si\", absorption=True)\n", + "print(sample)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, ORSO supports defining repeated layers using parentheses. For example, if we had a polarising multilayer of 5 repetitions of 70 angstrom silicon and 70 angstrom iron, we could represent it as `air | 5 ( Si 7 | Fe 7 ) | Si`.\n", + "\n", + "RAT will only create the number of layers and parameters necessary, but the `ORSOSample` object's `model` attribute will give a list of layer names with the structure of the model preserved, which can be given as the layer model for a Contrast." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sample = RATapi.utils.orso.orso_model_to_rat(\"air | 5 ( Si 7 | Fe 7 ) | Si\")\n", + "print(sample)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Reading in data and models from .ort files" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "RAT can also load both data and model information from an .ort file. This is done through the `ORSOProject` object, which takes a file path and can also optionally account for absorption.\n", + "\n", + "The example data file we use here is example data for an unknown film on deposited on silicon." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import pathlib\n", + "data_path = pathlib.Path(\"../data\")\n", + "\n", + "orso_data = RATapi.utils.orso.ORSOProject(data_path / \"c_PLP0011859_q.ort\")\n", + "print(orso_data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `ORSOProject` object contains two lists: `ORSOProject.data` and `ORSOProject.samples`. The former is a list of Data objects with each dataset defined in the file, and the latter is a list of `ORSOSample` objects (like above) with model information. Note that if the .ort file does not define a model for a dataset, that index of `ORSOProject.samples` will be None.\n", + "\n", + "It's then easy to access this data to create a RAT `Project` that represents our data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from RATapi.models import Background, Contrast, Parameter, Resolution\n", + "\n", + "dataset = orso_data.data[0]\n", + "sample = orso_data.samples[0]\n", + "\n", + "project = RATapi.Project(\n", + " name = \"Example Project\",\n", + " geometry = \"substrate/liquid\",\n", + " parameters = sample.parameters,\n", + " bulk_in = [sample.bulk_in],\n", + " bulk_out = [sample.bulk_out],\n", + " scalefactors = [Parameter(name=\"Scalefactor\", min=0, value=0.34, max=1.5)],\n", + " background_parameters = [Parameter(name=\"Background Parameter\", min=0, value=2e-6, max=1)],\n", + " backgrounds = [Background(name=\"Background\", type=\"constant\", source=\"Background Parameter\")],\n", + " resolutions = [Resolution(name=\"Data Resolution\", type=\"data\")],\n", + " data = [dataset],\n", + " layers = sample.layers,\n", + " contrasts = [Contrast(\n", + " name = \"prist4\",\n", + " data = dataset.name,\n", + " background = \"Background\",\n", + " bulk_in = sample.bulk_in.name,\n", + " bulk_out = sample.bulk_out.name,\n", + " scalefactor = \"Scalefactor\",\n", + " resolution = \"Data Resolution\",\n", + " model = sample.model,\n", + " )]\n", + ")\n", + "\n", + "controls = RATapi.Controls()\n", + "project, results = RATapi.run(project, controls)\n", + "RATapi.plotting.plot_ref_sld(project, results)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/RATapi/utils/orso.py b/RATapi/utils/orso.py new file mode 100644 index 00000000..50550619 --- /dev/null +++ b/RATapi/utils/orso.py @@ -0,0 +1,248 @@ +"""Readers from file formats.""" + +from dataclasses import dataclass +from itertools import count +from pathlib import Path +from textwrap import shorten +from typing import Union + +import orsopy +import prettytable +from orsopy.fileio import load_orso + +from RATapi import ClassList +from RATapi.models import AbsorptionLayer, Data, Layer, Parameter + + +class ORSOProject: + """A class to encapsulate model information and data from an .ort file. + + Parameters + ---------- + filepath : str or Path + The path to the .ort file. + absorption : bool, default None + Whether to account for absorption in the model data. + + """ + + def __init__(self, filepath: Union[str, Path], absorption: bool = False): + ort_data = load_orso(filepath) + datasets = [Data(name=dataset.info.data_source.sample.name, data=dataset.data) for dataset in ort_data] + # orso datasets in the same file can have repeated names! + # but classlists do not allow this + # use this dict to keep track of counts for repeated names + name_counts = {d.name: count(1) for d in datasets} + names = [d.name for d in datasets] + if len(names) > len(list(set(names))): + for i, data in enumerate(datasets): + if data.name in names[:i]: + data.name += f"-{next(name_counts[data.name])}" + self.data = ClassList(datasets) + self.samples = [ + orso_model_to_rat(dataset.info.data_source.sample.model, absorption=absorption) for dataset in ort_data + ] + + def __str__(self): + data_str = f"Data:\n{str(self.data)}\n\n" + if len(self.samples) == 1: + samples_str = f"Sample:\n{str(self.samples[0])}" + else: + table = prettytable.PrettyTable() + table.field_names = ["index", "bulk in", "bulk out", "parameters", "layers", "model"] + for index, sample in enumerate(self.samples): + if sample is None: + row = [index, "", "", "", "", ""] + else: + row = [ + index, + sample.bulk_in.name, + sample.bulk_out.name, + shorten(", ".join([p.name for p in sample.parameters]), width=20, placeholder="..."), + shorten(", ".join([layer.name for layer in sample.layers]), width=20, placeholder="..."), + shorten(str(sample.model), width=20, placeholder="..."), + ] + table.add_row(row) + samples_str = table.get_string() + + return data_str + samples_str + + +@dataclass +class ORSOSample: + """The stack data from an ORSO SampleModel, in RAT models.""" + + bulk_in: Parameter + bulk_out: Parameter + parameters: ClassList[Parameter] + layers: Union[ClassList[Layer], ClassList[AbsorptionLayer]] + model: list[str] + + def __str__(self): + return ( + "Bulk in:\n" + f"{str(self.bulk_in)}\n\n" + "Bulk out:\n" + f"{str(self.bulk_out)}\n\n" + "Parameters:\n" + f"{str(self.parameters)}\n\n" + "Layers:\n" + f"{str(self.layers)}\n\n" + "Model:\n" + f"{self.model}" + ) + + +def orso_model_to_rat( + model: Union[orsopy.fileio.model_language.SampleModel, str], absorption: bool = False +) -> Union[ORSOSample, None]: + """Get information from an ORSO SampleModel object. + + Parameters + ---------- + model : orsopy.fileio.model_language.SampleModel or str + The sample model to turn into a RAT data. If given as a string, + the string is interpreted as a layer stack in ORSO model language. + absorption : bool, default False + Whether to account for absorption in the model. + + Returns + ------- + ORSOSample + A dataclass containing the sample data, or None if the model is None. + + """ + if model is None: + return None + + if isinstance(model, str): + model = orsopy.fileio.model_language.SampleModel(stack=model) + + stack = model.resolve_to_layers() + # if bulk in or out is air, it has SLD predefined + # else we need to grab it from SLDDB + if bulk_in_sld := stack[0].material.sld is None: + bulk_in_sld = stack[0].material.get_sld() + + # resolve_to_layers loses the name of bulk in and out + bulk_in_name = model.stack.split("|")[0].strip() + bulk_in = Parameter( + name=f"{bulk_in_name} SLD", + min=bulk_in_sld.real, + value=bulk_in_sld.real, + max=bulk_in_sld.real, + fit=False, + ) + + if bulk_out_sld := stack[-1].material.sld is None: + bulk_out_sld = stack[-1].material.get_sld() + + bulk_out_name = model.stack.split("|")[-1].strip() + bulk_out = Parameter( + name=f"{bulk_out_name} SLD", + min=bulk_out_sld.real, + value=bulk_out_sld.real, + max=bulk_out_sld.real, + fit=False, + ) + + parameters = ClassList() + layers = ClassList() + contrast_model = [] + + for orso_layer in stack[1:-1]: + name = get_material_name(orso_layer.material, model) + contrast_model.append(name) + layer_params, layer = orso_layer_to_rat_layer(orso_layer, name, absorption) + parameters.union(layer_params) + layers.union(layer) + + return ORSOSample( + bulk_in=bulk_in, + bulk_out=bulk_out, + parameters=parameters, + layers=layers, + model=contrast_model, + ) + + +def orso_layer_to_rat_layer( + layer: orsopy.fileio.model_language.Layer, name: str, absorption: bool = False +) -> tuple[ClassList[Parameter], Layer]: + """Convert an ``orsopy`` layer to a RAT layer. + + Parameters + ---------- + layer : orsopy.fileio.model_language.Layer + An ``orsopy`` Layer. + name : str + The name of the material in the layer. + absorption : bool, default True + Whether absorption should be accounted for in the layer. + + Returns + ------- + ClassList[Parameter], Layer + The parameters required for the RAT layer and the layer itself. + + """ + thickness = layer.thickness.as_unit("angstrom") + roughness = layer.roughness.as_unit("angstrom") + sld = layer.material.get_sld() + + params = ClassList( + [ + Parameter(name=f"{name} Thickness", min=thickness, value=thickness, max=thickness, fit=False), + Parameter(name=f"{name} Roughness", min=roughness, value=roughness, max=roughness, fit=False), + Parameter(name=f"{name} SLD", min=sld.real, value=sld.real, max=sld.real, fit=False), + ] + ) + if absorption: + params.append(Parameter(name=f"{name} SLD imaginary", min=sld.imag, value=sld.imag, max=sld.imag, fit=False)) + layer = AbsorptionLayer( + name=name, + thickness=f"{name} Thickness", + roughness=f"{name} Roughness", + SLD_real=f"{name} SLD", + SLD_imaginary=f"{name} SLD imaginary", + ) + else: + layer = Layer( + name=name, + thickness=f"{name} Thickness", + roughness=f"{name} Roughness", + SLD=f"{name} SLD", + ) + + return params, layer + + +def get_material_name( + material: orsopy.fileio.model_language.Material, model: orsopy.fileio.model_language.SampleModel +) -> str: + """Get the name of a material in the model. + + Layers with custom property definitions may not have a formula, so this adjusts the name for that. + + Parameters + ---------- + material : Material + The material to get the name of. + model : SampleModel + The sample model from which the material came. + + Returns + ------- + str + The name of the material. + + """ + if material.formula is not None: + return material.formula + else: + matching_materials = [k for k, v in model.materials.items() if v == material] + if matching_materials: + return matching_materials[0] + else: + # orsopy should catch that this is the case before we get here, but just in case... + raise ValueError("ORSO model contains layers with undefined materials!") diff --git a/requirements.txt b/requirements.txt index 47a268b5..cee9a790 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,5 @@ StrEnum >= 0.4.15; python_version < '3.11' ruff >= 0.4.10 scipy >= 1.13.1 tqdm >= 4.66.5 +orsopy >= 1.2.1 +pint >= 0.24.4 diff --git a/setup.py b/setup.py index a3bb0ea4..d41a6ce8 100644 --- a/setup.py +++ b/setup.py @@ -172,11 +172,12 @@ def build_libraries(self, libraries): "pydantic >= 2.7.2", "matplotlib >= 3.8.3", "scipy >= 1.13.1", - "tqdm>=4.66.5", + "tqdm >= 4.66.5", ], extras_require={ ':python_version < "3.11"': ["StrEnum >= 0.4.15"], "Dev": ["pytest>=7.4.0", "pytest-cov>=4.1.0", "ruff>=0.4.10"], + "Orso": ["orsopy>=1.2.1", "pint>=0.24.4"], "Matlab_latest": ["matlabengine"], "Matlab_2025a": ["matlabengine == 25.1.*"], "Matlab_2024b": ["matlabengine == 24.2.2"], diff --git a/tests/test_data/bare_substrate.json b/tests/test_data/bare_substrate.json new file mode 100644 index 00000000..65e405c2 --- /dev/null +++ b/tests/test_data/bare_substrate.json @@ -0,0 +1 @@ +{"name": "substrate", "calculation": "normal", "model": "standard layers", "geometry": "air/substrate", "absorption": false, "parameters": [{"name": "Substrate Roughness", "min": 1.0, "value": 3.0, "max": 5.0, "fit": true, "prior_type": "uniform", "mu": 0.0, "sigma": Infinity, "show_priors": false}], "bulk_in": [{"name": "air SLD", "min": 4.42927130586151e-09, "value": 4.42927130586151e-09, "max": 4.42927130586151e-09, "fit": false, "prior_type": "uniform", "mu": 0.0, "sigma": Infinity, "show_priors": false}], "bulk_out": [{"name": "D2O SLD", "min": 6.360408603667384e-06, "value": 6.360408603667384e-06, "max": 6.360408603667384e-06, "fit": false, "prior_type": "uniform", "mu": 0.0, "sigma": Infinity, "show_priors": false}], "scalefactors": [{"name": "Scalefactor 1", "min": 0.02, "value": 0.23, "max": 0.25, "fit": false, "prior_type": "uniform", "mu": 0.0, "sigma": Infinity, "show_priors": false}], "domain_ratios": [], "background_parameters": [{"name": "Background Param 1", "min": 1e-07, "value": 1e-06, "max": 1e-05, "fit": false, "prior_type": "uniform", "mu": 0.0, "sigma": Infinity, "show_priors": false}], "backgrounds": [{"name": "Background 1", "type": "constant", "source": "Background Param 1", "value_1": "", "value_2": "", "value_3": "", "value_4": "", "value_5": ""}], "resolution_parameters": [{"name": "Resolution Param 1", "min": 0.01, "value": 0.03, "max": 0.05, "fit": false, "prior_type": "uniform", "mu": 0.0, "sigma": Infinity, "show_priors": false}], "resolutions": [{"name": "Resolution 1", "type": "constant", "source": "Resolution Param 1", "value_1": "", "value_2": "", "value_3": "", "value_4": "", "value_5": ""}], "custom_files": [], "data": [{"name": "Simulation", "data": [], "data_range": [], "simulation_range": [0.005, 0.7]}, {"name": "D2O substrate", "data": [[0.048866, 0.00012343, 1.3213e-06, 0.03], [0.051309, 0.00010063, 1.0803e-06, 0.03], [0.053874, 8.2165e-05, 8.8779e-07, 0.03], [0.056568, 6.4993e-05, 7.2018e-07, 0.03], [0.059396, 5.3958e-05, 6.0015e-07, 0.03], [0.062366, 4.359e-05, 5.0129e-07, 0.03], [0.065485, 3.578e-05, 4.1957e-07, 0.03], [0.068759, 2.913e-05, 3.5171e-07, 0.03], [0.072197, 2.3481e-05, 3.0586e-07, 0.03], [0.075807, 1.8906e-05, 2.6344e-07, 0.03], [0.079597, 1.4642e-05, 2.2314e-07, 0.03], [0.083577, 1.1589e-05, 1.8938e-07, 0.03], [0.087756, 9.5418e-06, 1.622e-07, 0.03], [0.092143, 7.5694e-06, 1.3809e-07, 0.03], [0.096751, 6.3831e-06, 1.2097e-07, 0.03], [0.10159, 5.0708e-06, 1.0333e-07, 0.03], [0.10667, 4.1041e-06, 8.9548e-08, 0.03], [0.112, 3.4253e-06, 7.983e-08, 0.03], [0.1176, 2.8116e-06, 7.1554e-08, 0.03], [0.12348, 2.3767e-06, 6.3738e-08, 0.03], [0.12966, 1.9241e-06, 5.6586e-08, 0.03], [0.13614, 1.5642e-06, 5.2778e-08, 0.03], [0.14294, 1.2922e-06, 4.973e-08, 0.03], [0.15009, 1.1694e-06, 5.1175e-08, 0.03], [0.1576, 9.7837e-07, 5.0755e-08, 0.03], [0.16548, 8.9138e-07, 5.3542e-08, 0.03], [0.17375, 7.942e-07, 5.4857e-08, 0.03], [0.18244, 7.9131e-07, 5.8067e-08, 0.03], [0.19156, 6.5358e-07, 5.7717e-08, 0.03], [0.20114, 6.297e-07, 5.7951e-08, 0.03], [0.21119, 5.013e-07, 5.5262e-08, 0.03], [0.22175, 5.0218e-07, 5.6461e-08, 0.03], [0.23284, 3.9299e-07, 5.0685e-08, 0.03], [0.24448, 3.5324e-07, 5.0194e-08, 0.03], [0.25671, 4.4475e-07, 5.6485e-08, 0.03], [0.26954, 5.1338e-07, 6.2247e-08, 0.03], [0.28302, 3.4918e-07, 4.9745e-08, 0.03], [0.29717, 4.3037e-07, 5.5488e-08, 0.03], [0.31203, 4.0099e-07, 5.3591e-08, 0.03], [0.32763, 3.8397e-07, 5.1303e-08, 0.03], [0.34401, 3.0995e-07, 4.5965e-08, 0.03], [0.36121, 3.9357e-07, 5.0135e-08, 0.03], [0.37927, 3.0997e-07, 4.368e-08, 0.03], [0.39824, 2.9656e-07, 4.2432e-08, 0.03], [0.41815, 2.1909e-07, 3.6117e-08, 0.03], [0.43906, 2.3153e-07, 3.6307e-08, 0.03], [0.46101, 3.3428e-07, 4.3874e-08, 0.03], [0.48406, 2.3441e-07, 3.7488e-08, 0.03], [0.50826, 1.5496e-07, 3.0585e-08, 0.03], [0.53368, 2.4708e-07, 3.9376e-08, 0.03], [0.56036, 2.2157e-07, 3.8258e-08, 0.03], [0.58838, 2.2798e-07, 4.6976e-08, 0.03], [0.61169, 6.0272e-07, 2.3239e-07, 0.03]], "data_range": [0.048866, 0.61169], "simulation_range": [0.048866, 0.61169]}], "layers": [], "domain_contrasts": [], "contrasts": []} \ No newline at end of file diff --git a/tests/test_data/bare_substrate.ort b/tests/test_data/bare_substrate.ort new file mode 100644 index 00000000..7e4ffb52 --- /dev/null +++ b/tests/test_data/bare_substrate.ort @@ -0,0 +1,83 @@ +# # ORSO reflectivity data file | 1.1 standard | YAML encoding | https://www.reflectometry.org/ +# # handwritten test file header created to test RAT orsopy integration! +# data_source: +# owner: +# name: null +# affiliation: null +# measurement: +# instrument_settings: null +# data_files: null +# experiment: +# title: Bare D2O substrate +# probe: neutron +# instrument: None +# start_date: 1970-01-01T00:00:00 +# sample: +# name: D2O substrate +# model: +# stack: air | D2O +# reduction: +# software: null +# timestamp: null +# data_set: 0 +# columns: +# - {name: Qz, unit: 1/angstrom, physical_quantity: normal momentum transfer} +# - {name: R, unit: '', physical_quantity: specular reflectivity} +# - {error_of: R, error_type: uncertainty, value_is: sigma} +# - {error_of: Qz, error_type: resolution, value_is: sigma} +# # Qz (1/angstrom) R () sR sQz +4.8866e-02 1.2343e-04 1.3213e-06 0.03 +5.1309e-02 1.0063e-04 1.0803e-06 0.03 +5.3874e-02 8.2165e-05 8.8779e-07 0.03 +5.6568e-02 6.4993e-05 7.2018e-07 0.03 +5.9396e-02 5.3958e-05 6.0015e-07 0.03 +6.2366e-02 4.3590e-05 5.0129e-07 0.03 +6.5485e-02 3.5780e-05 4.1957e-07 0.03 +6.8759e-02 2.9130e-05 3.5171e-07 0.03 +7.2197e-02 2.3481e-05 3.0586e-07 0.03 +7.5807e-02 1.8906e-05 2.6344e-07 0.03 +7.9597e-02 1.4642e-05 2.2314e-07 0.03 +8.3577e-02 1.1589e-05 1.8938e-07 0.03 +8.7756e-02 9.5418e-06 1.6220e-07 0.03 +9.2143e-02 7.5694e-06 1.3809e-07 0.03 +9.6751e-02 6.3831e-06 1.2097e-07 0.03 +1.0159e-01 5.0708e-06 1.0333e-07 0.03 +1.0667e-01 4.1041e-06 8.9548e-08 0.03 +1.1200e-01 3.4253e-06 7.9830e-08 0.03 +1.1760e-01 2.8116e-06 7.1554e-08 0.03 +1.2348e-01 2.3767e-06 6.3738e-08 0.03 +1.2966e-01 1.9241e-06 5.6586e-08 0.03 +1.3614e-01 1.5642e-06 5.2778e-08 0.03 +1.4294e-01 1.2922e-06 4.9730e-08 0.03 +1.5009e-01 1.1694e-06 5.1175e-08 0.03 +1.5760e-01 9.7837e-07 5.0755e-08 0.03 +1.6548e-01 8.9138e-07 5.3542e-08 0.03 +1.7375e-01 7.9420e-07 5.4857e-08 0.03 +1.8244e-01 7.9131e-07 5.8067e-08 0.03 +1.9156e-01 6.5358e-07 5.7717e-08 0.03 +2.0114e-01 6.2970e-07 5.7951e-08 0.03 +2.1119e-01 5.0130e-07 5.5262e-08 0.03 +2.2175e-01 5.0218e-07 5.6461e-08 0.03 +2.3284e-01 3.9299e-07 5.0685e-08 0.03 +2.4448e-01 3.5324e-07 5.0194e-08 0.03 +2.5671e-01 4.4475e-07 5.6485e-08 0.03 +2.6954e-01 5.1338e-07 6.2247e-08 0.03 +2.8302e-01 3.4918e-07 4.9745e-08 0.03 +2.9717e-01 4.3037e-07 5.5488e-08 0.03 +3.1203e-01 4.0099e-07 5.3591e-08 0.03 +3.2763e-01 3.8397e-07 5.1303e-08 0.03 +3.4401e-01 3.0995e-07 4.5965e-08 0.03 +3.6121e-01 3.9357e-07 5.0135e-08 0.03 +3.7927e-01 3.0997e-07 4.3680e-08 0.03 +3.9824e-01 2.9656e-07 4.2432e-08 0.03 +4.1815e-01 2.1909e-07 3.6117e-08 0.03 +4.3906e-01 2.3153e-07 3.6307e-08 0.03 +4.6101e-01 3.3428e-07 4.3874e-08 0.03 +4.8406e-01 2.3441e-07 3.7488e-08 0.03 +5.0826e-01 1.5496e-07 3.0585e-08 0.03 +5.3368e-01 2.4708e-07 3.9376e-08 0.03 +5.6036e-01 2.2157e-07 3.8258e-08 0.03 +5.8838e-01 2.2798e-07 4.6976e-08 0.03 +6.1169e-01 6.0272e-07 2.3239e-07 0.03 + + diff --git a/tests/test_data/prist.json b/tests/test_data/prist.json new file mode 100644 index 00000000..3b36893e --- /dev/null +++ b/tests/test_data/prist.json @@ -0,0 +1 @@ +{"name": "Example Project", "calculation": "normal", "model": "standard layers", "geometry": "air/substrate", "absorption": false, "parameters": [{"name": "Substrate Roughness", "min": 1.0, "value": 3.0, "max": 5.0, "fit": true, "prior_type": "uniform", "mu": 0.0, "sigma": Infinity, "show_priors": false}, {"name": "Al2O3 Thickness", "min": 20.0, "value": 20.0, "max": 20.0, "fit": false, "prior_type": "uniform", "mu": 0.0, "sigma": Infinity, "show_priors": false}, {"name": "Al2O3 Roughness", "min": 3.0, "value": 3.0, "max": 3.0, "fit": false, "prior_type": "uniform", "mu": 0.0, "sigma": Infinity, "show_priors": false}, {"name": "Al2O3 SLD", "min": 5.715275837467511e-06, "value": 5.715275837467511e-06, "max": 5.715275837467511e-06, "fit": false, "prior_type": "uniform", "mu": 0.0, "sigma": Infinity, "show_priors": false}, {"name": "Ti0.27Co0.73 Thickness", "min": 93.0, "value": 93.0, "max": 93.0, "fit": false, "prior_type": "uniform", "mu": 0.0, "sigma": Infinity, "show_priors": false}, {"name": "Ti0.27Co0.73 Roughness", "min": 3.0, "value": 3.0, "max": 3.0, "fit": false, "prior_type": "uniform", "mu": 0.0, "sigma": Infinity, "show_priors": false}, {"name": "Ti0.27Co0.73 SLD", "min": 6.720584953375607e-07, "value": 6.720584953375607e-07, "max": 6.720584953375607e-07, "fit": false, "prior_type": "uniform", "mu": 0.0, "sigma": Infinity, "show_priors": false}], "bulk_in": [{"name": "vacuum SLD", "min": 0.0, "value": 0.0, "max": 0.0, "fit": false, "prior_type": "uniform", "mu": 0.0, "sigma": Infinity, "show_priors": false}], "bulk_out": [{"name": "Si SLD", "min": 2.07370547828562e-06, "value": 2.07370547828562e-06, "max": 2.07370547828562e-06, "fit": false, "prior_type": "uniform", "mu": 0.0, "sigma": Infinity, "show_priors": false}], "scalefactors": [{"name": "Scalefactor", "min": 0.0, "value": 1.0, "max": 1.5, "fit": true, "prior_type": "uniform", "mu": 0.0, "sigma": Infinity, "show_priors": false}], "domain_ratios": [], "background_parameters": [{"name": "Background Parameter", "min": 0.0, "value": 0.025, "max": 1.0, "fit": true, "prior_type": "uniform", "mu": 0.0, "sigma": Infinity, "show_priors": false}], "backgrounds": [{"name": "Background", "type": "constant", "source": "Background Parameter", "value_1": "", "value_2": "", "value_3": "", "value_4": "", "value_5": ""}], "resolution_parameters": [{"name": "Resolution Param 1", "min": 0.01, "value": 0.03, "max": 0.05, "fit": false, "prior_type": "uniform", "mu": 0.0, "sigma": Infinity, "show_priors": false}], "resolutions": [{"name": "Data Resolution", "type": "data", "source": "", "value_1": "", "value_2": "", "value_3": "", "value_4": "", "value_5": ""}], "custom_files": [], "data": [{"name": "Simulation", "data": [], "data_range": [], "simulation_range": [0.005, 0.7]}, {"name": "prist4", "data": [[0.0021999999999999997, 0.004724119389721505, 0.004724119389721505, 0.00019999999999999987], [0.0026, 0.0, 0.0, 0.0002000000000000001], [0.003, 0.0049441079014990305, 0.0028632759811688313, 0.0002000000000000001], [0.0034000000000000002, 0.00539571466421478, 0.0027033018099334193, 0.00019999999999999987], [0.0038, 0.011948517768746966, 0.0037903477990210636, 0.0002000000000000001], [0.004200000000000001, 0.016302332669940297, 0.003972317564845323, 0.0002000000000000001], [0.0046, 0.02265216487165058, 0.0045763672802481776, 0.0002000000000000001], [0.005, 0.023020814468949415, 0.00425372711337262, 0.00019999999999999966], [0.0054, 0.027305557750665344, 0.004428700971127359, 0.0002000000000000001], [0.0058, 0.019457945835850306, 0.0037817426451169783, 0.0002000000000000001], [0.006200000000000001, 0.027645506109865266, 0.004267993991409073, 0.0002000000000000001], [0.0066, 0.024223034347324195, 0.0037655052701117517, 0.0002000000000000001], [0.007, 0.028408308819418325, 0.0037377941755602716, 0.00019999999999999966], [0.0074, 0.057460495308053604, 0.0052154049057105455, 0.0002000000000000001], [0.0078, 0.1489883351950982, 0.00754789840356179, 0.0002000000000000001], [0.008199999999999999, 0.3520480929443021, 0.011146891694516669, 0.00019999999999999966], [0.0086, 0.5317657760789548, 0.01351725179531962, 0.00019999999999999966], [0.009, 0.7914500646501588, 0.016276277463585134, 0.00020000000000000052], [0.009399999999999999, 0.9680331047846104, 0.01782748799306378, 0.00019999999999999966], [0.0098, 1.0324643310179094, 0.018104900945963596, 0.00020000000000000052], [0.010204081632653062, 0.6287661683321234, 0.013873373556929145, 0.00020408163265306107], [0.010620574760516453, 0.3506955915110479, 0.010136613757789808, 0.00021241149521033023], [0.011054067607884473, 0.23477751708069897, 0.008109962639928676, 0.000221081352157689], [0.011505254040859348, 0.18964302712880826, 0.007180573014831645, 0.00023010508081718756], [0.011974856246608712, 0.14285208497806512, 0.006016224926747713, 0.00023949712493217482], [0.012463625889327434, 0.12188457497836602, 0.005435279585343197, 0.00024927251778654805], [0.012972345313381616, 0.10530945504618572, 0.005027517945960155, 0.00025944690626763297], [0.013501828795560456, 0.08038795087657985, 0.004197556276825506, 0.0002700365759112089], [0.014052923848440476, 0.07313672279549625, 0.003958200398418697, 0.00028105847696880976], [0.01462651257694825, 0.06129841978151465, 0.0034783001152817426, 0.0002925302515389659], [0.01522351309029308, 0.06240905936586468, 0.003479800062460076, 0.000304470261805862], [0.015844880971529533, 0.05651280096554304, 0.0032223775371921573, 0.00031689761943059103], [0.016491610807102167, 0.05044073793552524, 0.00290210672478378, 0.0003298322161420439], [0.017164737778820625, 0.0502775250441711, 0.002825286449304508, 0.00034329475557641313], [0.0178653393208133, 0.05168970509722787, 0.002811814341605395, 0.0003573067864162654], [0.018594536844111803, 0.0530076187705834, 0.0027685413804410695, 0.00037189073688223724], [0.019353497531626573, 0.0490707566771454, 0.0025677593751218504, 0.00038706995063253133], [0.020143436206386842, 0.04687883170966378, 0.0024119133732668076, 0.0004028687241277376], [0.020965617276035284, 0.04919955377214455, 0.0024276503561561644, 0.0004193123455207056], [0.021821356756689787, 0.05204111270310913, 0.00245561515567379, 0.00043642713513379616], [0.022712024379411822, 0.04925178172610669, 0.0023335505076022252, 0.00045424048758823873], [0.02363904578265312, 0.04317260894774357, 0.00211385396560445, 0.0004727809156530611], [0.024603904794189984, 0.0383043276355005, 0.0019158755178575817, 0.0004920780958838009], [0.02560814580619774, 0.04444220010645997, 0.002018641932709427, 0.0005121629161239545], [0.026653376247267036, 0.039587001147536376, 0.0018488058581743547, 0.0005330675249453423], [0.027741269155318757, 0.036677858215866437, 0.0017473224966445143, 0.0005548253831063766], [0.028873565855535847, 0.037663512267048754, 0.0017682821318945074, 0.0005774713171107174], [0.030052078747598535, 0.036755569252809664, 0.0017350440273063527, 0.0006010415749519698], [0.03127869420668419, 0.03854639441776167, 0.0017965558626918197, 0.0006255738841336837], [0.032555375602875386, 0.032866785139061716, 0.0016217721152892931, 0.000651107512057509], [0.03388416644380907, 0.0315131968037535, 0.0016061232808323794, 0.0006776833288761816], [0.0352671936455972, 0.030280345918169513, 0.0015885947028228238, 0.0007053438729119448], [0.03670667093725423, 0.025036982048446406, 0.0014176844815086535, 0.0007341334187450851], [0.038204902404080934, 0.02083180456328542, 0.001280336546816776, 0.0007640980480816198], [0.03976428617567608, 0.019376976329704945, 0.0012712203272554497, 0.0007952857235135231], [0.04138731826447918, 0.016729675515039004, 0.0011715497338181137, 0.000827746365289584], [0.04307659656098854, 0.013996303090353096, 0.0010654383375290077, 0.0008615319312197701], [0.0448348249920493, 0.010621480384334052, 0.0009246807782300395, 0.0008966964998409904], [0.04666481784886764, 0.007820332463525126, 0.0008093102454836752, 0.000933296356977354], [0.04856950429167857, 0.0060428022894521575, 0.0007183835413384203, 0.0009713900858335685], [0.050551933038277694, 0.004095892971484393, 0.0005841670446995313, 0.0010110386607655557], [0.05261527724392169, 0.005111525161941947, 0.0006658003030884043, 0.0010523055448784409], [0.05476283958040829, 0.0020207116612746484, 0.0004143866059231477, 0.0010952567916081637], [0.05699805752246577, 0.001711568931414378, 0.00038529038925611664, 0.0011399611504493146], [0.05932450884991336, 0.0008496305883131249, 0.00027095330976641694, 0.001186490176998272], [0.06174591737439962, 0.0008142116035177235, 0.00027251395576638693, 0.0012349183474879948], [0.06426615889988532, 8.92042382623096e-05, 8.920423826322552e-05, 0.0012853231779977048], [0.06688926742641126, 9.339736346561163e-05, 9.339736346489978e-05, 0.0013377853485282282], [0.06961944160708111, 0.00040599169078880156, 0.0002030320824188672, 0.0013923888321416208], [0.07246105146859462, 0.0002183257352075823, 0.00015439324988622013, 0.0014492210293718943], [0.07541864540608828, 0.00012684339163879, 0.00012684339163797725, 0.001508372908121773], [0.07849695746347965, 0.0, 0.0, 0.001569939149269589], [0.08170091491096862, 0.0, 0.0, 0.0016340182982193738], [0.08503564613182449, 0.0004165748322677754, 0.00024090333523406435, 0.001700712922636495], [0.08850648883108263, 0.0, 0.0, 0.0017701297766216512], [0.09211899857929008, 0.0, 0.0, 0.001842379971585803], [0.0958789577049754, 0.0, 0.0, 0.0019175791540995135], [0.09979238455007644, 0.0, 0.0, 0.001995847691001529], [0.10386554310314079, 0.0, 0.0, 0.0020773108620628228]], "data_range": [0.0021999999999999997, 0.10386554310314079], "simulation_range": [0.0021999999999999997, 0.10386554310314079]}], "layers": [{"name": "Al2O3", "thickness": "Al2O3 Thickness", "SLD": "Al2O3 SLD", "roughness": "Al2O3 Roughness", "hydration": "", "hydrate_with": "bulk out"}, {"name": "Ti0.27Co0.73", "thickness": "Ti0.27Co0.73 Thickness", "SLD": "Ti0.27Co0.73 SLD", "roughness": "Ti0.27Co0.73 Roughness", "hydration": "", "hydrate_with": "bulk out"}], "domain_contrasts": [], "contrasts": [{"name": "prist4", "data": "prist4", "background": "Background", "background_action": "add", "bulk_in": "vacuum SLD", "bulk_out": "Si SLD", "scalefactor": "Scalefactor", "resolution": "Data Resolution", "resample": false, "model": ["Al2O3", "Ti0.27Co0.73"]}]} \ No newline at end of file diff --git a/tests/test_data/prist5_10K_m_025.Rqz.ort b/tests/test_data/prist5_10K_m_025.Rqz.ort new file mode 100644 index 00000000..5b60b8fc --- /dev/null +++ b/tests/test_data/prist5_10K_m_025.Rqz.ort @@ -0,0 +1,124 @@ +# # ORSO reflectivity data file | 1.0 standard | YAML encoding | https://www.reflectometry.org/ +# data_source: +# owner: +# name: Artur Glavic +# affiliation: null +# contact: b'' +# experiment: +# title: Structural evolution of the CO2/Water interface +# instrument: Amor +# start_date: 2023-11-29T10:12:45 +# probe: neutron +# facility: SINQ@PSI +# proposalID: '20230368' +# sample: +# name: prist4 +# sample_parameters: +# tempMean: {magnitude: -9999.0} +# model: +# stack: vacuum | Al2O3 2 | Ti0.27Co0.73 9.3 | Si +# measurement: +# instrument_settings: +# incident_angle: {min: 0.0950000000000002, max: 1.495, unit: deg} +# wavelength: {min: 3.0, max: 12.0, unit: angstrom} +# mu: {magnitude: 0.25, unit: deg, comment: sample angle to referece direction} +# nu: {magnitude: 1.0450000000000002, unit: deg, comment: detector angle to referece +# direction} +# data_files: +# - file: raw/amor2023n000848.hdf +# timestamp: 2023-11-29T10:12:45 +# amor_monitor: 1792.443302905 +# scheme: angle- and energy-dispersive +# references: [] +# amor_monitor: 1792.443302905 +# reduction: +# software: {name: eos, version: '2.0'} +# timestamp: 2023-11-29T10:57:48.480339 +# computer: amor.psi.ch +# call: eos.py -a 0.04 -F 0.0093,0.0101 -n 848 prist5_10K_m_025 +# data_set: 0 +# columns: +# - {name: Qz, unit: 1/angstrom, physical_quantity: normal momentum transfer} +# - {name: R, unit: '', physical_quantity: specular reflectivity} +# - {error_of: R, error_type: uncertainty, value_is: sigma} +# - {error_of: Qz, error_type: resolution, value_is: sigma} +# # Qz (1/angstrom) R () sR sQz +2.1999999999999997e-03 4.7241193897215048e-03 4.7241193897215048e-03 1.9999999999999987e-04 +2.5999999999999999e-03 0.0000000000000000e+00 0.0000000000000000e+00 2.0000000000000009e-04 +3.0000000000000001e-03 4.9441079014990305e-03 2.8632759811688313e-03 2.0000000000000009e-04 +3.4000000000000002e-03 5.3957146642147798e-03 2.7033018099334193e-03 1.9999999999999987e-04 +3.8000000000000000e-03 1.1948517768746966e-02 3.7903477990210636e-03 2.0000000000000009e-04 +4.2000000000000006e-03 1.6302332669940297e-02 3.9723175648453228e-03 2.0000000000000009e-04 +4.5999999999999999e-03 2.2652164871650581e-02 4.5763672802481776e-03 2.0000000000000009e-04 +5.0000000000000001e-03 2.3020814468949415e-02 4.2537271133726200e-03 1.9999999999999966e-04 +5.4000000000000003e-03 2.7305557750665344e-02 4.4287009711273589e-03 2.0000000000000009e-04 +5.7999999999999996e-03 1.9457945835850306e-02 3.7817426451169783e-03 2.0000000000000009e-04 +6.2000000000000006e-03 2.7645506109865266e-02 4.2679939914090732e-03 2.0000000000000009e-04 +6.6000000000000000e-03 2.4223034347324195e-02 3.7655052701117517e-03 2.0000000000000009e-04 +7.0000000000000001e-03 2.8408308819418325e-02 3.7377941755602716e-03 1.9999999999999966e-04 +7.4000000000000003e-03 5.7460495308053604e-02 5.2154049057105455e-03 2.0000000000000009e-04 +7.7999999999999996e-03 1.4898833519509819e-01 7.5478984035617898e-03 2.0000000000000009e-04 +8.1999999999999990e-03 3.5204809294430212e-01 1.1146891694516669e-02 1.9999999999999966e-04 +8.6000000000000000e-03 5.3176577607895481e-01 1.3517251795319620e-02 1.9999999999999966e-04 +8.9999999999999993e-03 7.9145006465015877e-01 1.6276277463585134e-02 2.0000000000000052e-04 +9.3999999999999986e-03 9.6803310478461035e-01 1.7827487993063780e-02 1.9999999999999966e-04 +9.7999999999999997e-03 1.0324643310179094e+00 1.8104900945963596e-02 2.0000000000000052e-04 +1.0204081632653062e-02 6.2876616833212340e-01 1.3873373556929145e-02 2.0408163265306107e-04 +1.0620574760516453e-02 3.5069559151104790e-01 1.0136613757789808e-02 2.1241149521033023e-04 +1.1054067607884473e-02 2.3477751708069897e-01 8.1099626399286761e-03 2.2108135215768900e-04 +1.1505254040859348e-02 1.8964302712880826e-01 7.1805730148316446e-03 2.3010508081718756e-04 +1.1974856246608712e-02 1.4285208497806512e-01 6.0162249267477130e-03 2.3949712493217482e-04 +1.2463625889327434e-02 1.2188457497836602e-01 5.4352795853431970e-03 2.4927251778654805e-04 +1.2972345313381616e-02 1.0530945504618572e-01 5.0275179459601553e-03 2.5944690626763297e-04 +1.3501828795560456e-02 8.0387950876579850e-02 4.1975562768255060e-03 2.7003657591120889e-04 +1.4052923848440476e-02 7.3136722795496253e-02 3.9582003984186967e-03 2.8105847696880976e-04 +1.4626512576948251e-02 6.1298419781514651e-02 3.4783001152817426e-03 2.9253025153896592e-04 +1.5223513090293080e-02 6.2409059365864682e-02 3.4798000624600761e-03 3.0447026180586197e-04 +1.5844880971529533e-02 5.6512800965543043e-02 3.2223775371921573e-03 3.1689761943059103e-04 +1.6491610807102167e-02 5.0440737935525240e-02 2.9021067247837801e-03 3.2983221614204389e-04 +1.7164737778820625e-02 5.0277525044171101e-02 2.8252864493045079e-03 3.4329475557641313e-04 +1.7865339320813300e-02 5.1689705097227867e-02 2.8118143416053952e-03 3.5730678641626538e-04 +1.8594536844111803e-02 5.3007618770583402e-02 2.7685413804410695e-03 3.7189073688223724e-04 +1.9353497531626573e-02 4.9070756677145402e-02 2.5677593751218504e-03 3.8706995063253133e-04 +2.0143436206386842e-02 4.6878831709663782e-02 2.4119133732668076e-03 4.0286872412773761e-04 +2.0965617276035284e-02 4.9199553772144550e-02 2.4276503561561644e-03 4.1931234552070561e-04 +2.1821356756689787e-02 5.2041112703109127e-02 2.4556151556737898e-03 4.3642713513379616e-04 +2.2712024379411822e-02 4.9251781726106690e-02 2.3335505076022252e-03 4.5424048758823873e-04 +2.3639045782653120e-02 4.3172608947743572e-02 2.1138539656044499e-03 4.7278091565306109e-04 +2.4603904794189984e-02 3.8304327635500499e-02 1.9158755178575817e-03 4.9207809588380086e-04 +2.5608145806197739e-02 4.4442200106459967e-02 2.0186419327094269e-03 5.1216291612395451e-04 +2.6653376247267036e-02 3.9587001147536376e-02 1.8488058581743547e-03 5.3306752494534232e-04 +2.7741269155318757e-02 3.6677858215866437e-02 1.7473224966445143e-03 5.5482538310637659e-04 +2.8873565855535847e-02 3.7663512267048754e-02 1.7682821318945074e-03 5.7747131711071743e-04 +3.0052078747598535e-02 3.6755569252809664e-02 1.7350440273063527e-03 6.0104157495196979e-04 +3.1278694206684193e-02 3.8546394417761670e-02 1.7965558626918197e-03 6.2557388413368373e-04 +3.2555375602875386e-02 3.2866785139061716e-02 1.6217721152892931e-03 6.5110751205750897e-04 +3.3884166443809073e-02 3.1513196803753502e-02 1.6061232808323794e-03 6.7768332887618160e-04 +3.5267193645597203e-02 3.0280345918169513e-02 1.5885947028228238e-03 7.0534387291194475e-04 +3.6706670937254229e-02 2.5036982048446406e-02 1.4176844815086535e-03 7.3413341874508514e-04 +3.8204902404080934e-02 2.0831804563285421e-02 1.2803365468167761e-03 7.6409804808161980e-04 +3.9764286175676081e-02 1.9376976329704945e-02 1.2712203272554497e-03 7.9528572351352314e-04 +4.1387318264479181e-02 1.6729675515039004e-02 1.1715497338181137e-03 8.2774636528958404e-04 +4.3076596560988542e-02 1.3996303090353096e-02 1.0654383375290077e-03 8.6153193121977015e-04 +4.4834824992049299e-02 1.0621480384334052e-02 9.2468077823003954e-04 8.9669649984099042e-04 +4.6664817848867640e-02 7.8203324635251256e-03 8.0931024548367521e-04 9.3329635697735405e-04 +4.8569504291678570e-02 6.0428022894521575e-03 7.1838354133842033e-04 9.7139008583356848e-04 +5.0551933038277694e-02 4.0958929714843932e-03 5.8416704469953131e-04 1.0110386607655557e-03 +5.2615277243921690e-02 5.1115251619419473e-03 6.6580030308840434e-04 1.0523055448784409e-03 +5.4762839580408292e-02 2.0207116612746484e-03 4.1438660592314769e-04 1.0952567916081637e-03 +5.6998057522465770e-02 1.7115689314143780e-03 3.8529038925611664e-04 1.1399611504493146e-03 +5.9324508849913360e-02 8.4963058831312487e-04 2.7095330976641694e-04 1.1864901769982721e-03 +6.1745917374399620e-02 8.1421160351772355e-04 2.7251395576638693e-04 1.2349183474879948e-03 +6.4266158899885323e-02 8.9204238262309603e-05 8.9204238263225523e-05 1.2853231779977048e-03 +6.6889267426411256e-02 9.3397363465611626e-05 9.3397363464899779e-05 1.3377853485282282e-03 +6.9619441607081112e-02 4.0599169078880156e-04 2.0303208241886721e-04 1.3923888321416208e-03 +7.2461051468594620e-02 2.1832573520758229e-04 1.5439324988622013e-04 1.4492210293718943e-03 +7.5418645406088280e-02 1.2684339163878999e-04 1.2684339163797725e-04 1.5083729081217731e-03 +7.8496957463479650e-02 0.0000000000000000e+00 0.0000000000000000e+00 1.5699391492695891e-03 +8.1700914910968619e-02 0.0000000000000000e+00 0.0000000000000000e+00 1.6340182982193738e-03 +8.5035646131824488e-02 4.1657483226777538e-04 2.4090333523406435e-04 1.7007129226364950e-03 +8.8506488831082628e-02 0.0000000000000000e+00 0.0000000000000000e+00 1.7701297766216512e-03 +9.2118998579290082e-02 0.0000000000000000e+00 0.0000000000000000e+00 1.8423799715858030e-03 +9.5878957704975398e-02 0.0000000000000000e+00 0.0000000000000000e+00 1.9175791540995135e-03 +9.9792384550076441e-02 0.0000000000000000e+00 0.0000000000000000e+00 1.9958476910015288e-03 +1.0386554310314079e-01 0.0000000000000000e+00 0.0000000000000000e+00 2.0773108620628228e-03 diff --git a/tests/test_orso_utils.py b/tests/test_orso_utils.py new file mode 100644 index 00000000..958abd88 --- /dev/null +++ b/tests/test_orso_utils.py @@ -0,0 +1,119 @@ +"""Tests for the RATapi.utils.orso module.""" + +import os +from io import StringIO +from pathlib import Path + +import numpy as np +import pytest +from orsopy.fileio.model_language import SampleModel + +from RATapi.examples.bayes_benchmark.bayes_benchmark import get_project +from RATapi.project import Project +from RATapi.utils.orso import ORSOProject, orso_model_to_rat + +TEST_DIR_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data") + + +@pytest.fixture +def bare_subs(): + """The bare substrate project from the Bayes Benchmark example.""" + return get_project() + + +@pytest.fixture +def prist(): + """The project from the model data from prist5_10K_m_025.Rqz.ort""" + return Project.load(Path(TEST_DIR_PATH, "prist.json")) + + +@pytest.mark.parametrize( + "model", + [ + "air | FeO2 0.75 | Fe 10 | Si", + "vacuum | 5 (O3 3 | He2 4) | SiO2 0.75 | Si", + "Si | 5 (O2 2 | 3 (D2O 1 | H2O 1)) | air", + ], +) +@pytest.mark.parametrize("absorption", [True, False]) +def test_orso_model_to_rat(model, absorption): + """Test that orso_model_to_rat gives the expected parameters, layers and model.""" + + expected = SampleModel(model).resolve_to_layers()[1:-1] + expected_layers = [layer.material.formula for layer in expected] + expected_thicknesses = {layer.material.formula: layer.thickness for layer in expected} + expected_roughnesses = {layer.material.formula: layer.roughness for layer in expected} + actual = orso_model_to_rat(model, absorption=absorption) + + assert actual.model == expected_layers + + for layer in actual.layers: + assert layer.name in expected_layers + for layer in expected_layers: + assert layer in [actual_layer.name for actual_layer in actual.layers] + + expected_parameters = [] + # get set of parameters + for layer in set(expected_layers): + expected_parameters.extend([f"{layer} Thickness", f"{layer} Roughness", f"{layer} SLD"]) + if absorption: + expected_parameters.append(f"{layer} SLD imaginary") + + assert actual.parameters[f"{layer} Thickness"].value == expected_thicknesses[layer].as_unit("angstrom") + assert actual.parameters[f"{layer} Roughness"].value == expected_roughnesses[layer].as_unit("angstrom") + + assert set(p.name for p in actual.parameters) == set(expected_parameters) + + +@pytest.mark.parametrize( + "test_data", + [ + "bare_substrate.ort", + "prist5_10K_m_025.Rqz.ort", + ], +) +def test_load_ort_data(test_data): + """Test that .ort data is loaded correctly.""" + # manually get the test data for comparison + data_strings = [""] + parsing_data = False + with Path(TEST_DIR_PATH, test_data).open() as file: + for line in file: + if line[0] == "#": + if parsing_data: + parsing_data = False + data_strings.append("") + else: + continue + else: + parsing_data = True + data_strings[-1] += line + + expected_data = list(map(lambda s: np.loadtxt(StringIO(s)), data_strings)) + actual_data = ORSOProject(Path(TEST_DIR_PATH, test_data)).data + + assert len(actual_data) == len(expected_data) + for actual_dataset, expected_dataset in zip(actual_data, expected_data): + np.testing.assert_array_equal(actual_dataset.data, expected_dataset) + + +@pytest.mark.parametrize( + "test_data, expected_data", + [ + ["bare_substrate.ort", "bare_substrate.json"], + ["prist5_10K_m_025.Rqz.ort", "prist.json"], + ], +) +def test_load_ort_project(test_data, expected_data): + """Test that a project with model data is loaded correctly.""" + ort_data = ORSOProject(Path(TEST_DIR_PATH, test_data)) + sample = ort_data.samples[0] + exp_project = Project.load(Path(TEST_DIR_PATH, expected_data)) + + for class_list in ["bulk_in", "bulk_out"]: + assert getattr(sample, class_list) == getattr(exp_project, class_list)[0] + assert sample.parameters == exp_project.parameters[1:] + assert sample.layers == exp_project.layers + + for data, exp_data in zip(ort_data.data, exp_project.data[1:]): + np.testing.assert_array_equal(data.data, exp_data.data) From 7538af46a7b711795fec8ca4ca1184e752ed25c1 Mon Sep 17 00:00:00 2001 From: "Alex H. Room" <69592136+alexhroom@users.noreply.github.com> Date: Fri, 14 Mar 2025 15:52:03 +0000 Subject: [PATCH 11/20] makes orsopy actually optional (#140) --- RATapi/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/RATapi/__init__.py b/RATapi/__init__.py index 1c0b820f..3d74927d 100644 --- a/RATapi/__init__.py +++ b/RATapi/__init__.py @@ -1,11 +1,16 @@ """RATapi is a Python package for modelling, fitting and optimising reflectivity problems.""" +from contextlib import suppress + import RATapi.examples as examples from RATapi import events, models from RATapi.classlist import ClassList from RATapi.controls import Controls from RATapi.project import Project from RATapi.run import run -from RATapi.utils import convert, orso, plotting +from RATapi.utils import convert, plotting + +with suppress(ImportError): # orsopy is an optional dependency + from RATapi.utils import orso as orso -__all__ = ["examples", "models", "events", "ClassList", "Controls", "Project", "run", "plotting", "convert", "orso"] +__all__ = ["examples", "models", "events", "ClassList", "Controls", "Project", "run", "plotting", "convert"] From 0b9816bcb9fa2309063449fc00726c0f1d13beb5 Mon Sep 17 00:00:00 2001 From: "Alex H. Room" <69592136+alexhroom@users.noreply.github.com> Date: Mon, 24 Mar 2025 09:23:14 +0000 Subject: [PATCH 12/20] Ensure nChains is greater than 1 (#141) * Change nChains to be greater than 1 * fixed tests --- RATapi/controls.py | 2 +- tests/test_controls.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/RATapi/controls.py b/RATapi/controls.py index 50f18c99..28b52d8a 100644 --- a/RATapi/controls.py +++ b/RATapi/controls.py @@ -118,7 +118,7 @@ class Controls(BaseModel, validate_assignment=True, extra="forbid", use_attribut nSamples: int = Field(20000, ge=0) """[DREAM] The number of samples in the initial population for each chain.""" - nChains: int = Field(10, gt=0) + nChains: int = Field(10, gt=1) """[DREAM] The number of Markov chains to use in the algorithm.""" jumpProbability: float = Field(0.5, gt=0.0, lt=1.0) diff --git a/tests/test_controls.py b/tests/test_controls.py index 73b776bd..5c4400a3 100644 --- a/tests/test_controls.py +++ b/tests/test_controls.py @@ -859,7 +859,7 @@ def test_dream_nSamples_error(self, value: int) -> None: @pytest.mark.parametrize("value", [-5, 0]) def test_dream_nChains_error(self, value: int) -> None: """Tests the nChains setter error in Dream class.""" - with pytest.raises(pydantic.ValidationError, match="Input should be greater than 0"): + with pytest.raises(pydantic.ValidationError, match="Input should be greater than 1"): self.dream.nChains = value def test_control_class_dream_str(self, table_str) -> None: From 5a26412e693d21ef87669105a4a4bf13c62124ee Mon Sep 17 00:00:00 2001 From: "Alex H. Room" <69592136+alexhroom@users.noreply.github.com> Date: Tue, 25 Mar 2025 10:01:34 +0000 Subject: [PATCH 13/20] Fixes the `nSamples` docstring (#142) --- RATapi/controls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RATapi/controls.py b/RATapi/controls.py index 28b52d8a..9a36fdee 100644 --- a/RATapi/controls.py +++ b/RATapi/controls.py @@ -116,7 +116,7 @@ class Controls(BaseModel, validate_assignment=True, extra="forbid", use_attribut # Dream nSamples: int = Field(20000, ge=0) - """[DREAM] The number of samples in the initial population for each chain.""" + """[DREAM] The total number of function evaluations (number of algorithm generations times number of chains).""" nChains: int = Field(10, gt=1) """[DREAM] The number of Markov chains to use in the algorithm.""" From b89eb00e16c0d67c2722060a8538cc1ea32f0476 Mon Sep 17 00:00:00 2001 From: Paul Sharp <44529197+DrPaulSharp@users.noreply.github.com> Date: Thu, 27 Mar 2025 16:12:29 +0000 Subject: [PATCH 14/20] Updates inputs, outputs, and build process (#143) * Removes limits struct from input to RAT * Renames "layerSlds" as "layers" * Updates submodule and build * Updates submodule * Updates defines.h --- RATapi/inputs.py | 20 +--- RATapi/outputs.py | 17 ++- RATapi/run.py | 3 +- cpp/RAT | 2 +- cpp/includes/defines.h | 42 +------ cpp/rat.cpp | 77 ++----------- tests/conftest.py | 242 ++++++++++++++++++++--------------------- tests/test_inputs.py | 59 +--------- tests/test_outputs.py | 5 +- tests/utils.py | 4 +- 10 files changed, 158 insertions(+), 313 deletions(-) diff --git a/RATapi/inputs.py b/RATapi/inputs.py index 0bc00284..3798cb0f 100644 --- a/RATapi/inputs.py +++ b/RATapi/inputs.py @@ -10,7 +10,7 @@ import RATapi import RATapi.controls import RATapi.wrappers -from RATapi.rat_core import Checks, Control, Limits, NameStore, ProblemDefinition +from RATapi.rat_core import Checks, Control, NameStore, ProblemDefinition from RATapi.utils.enums import Calculations, Languages, LayerModels, TypeOptions parameter_field = { @@ -113,7 +113,7 @@ def __len__(self): return len(self.files) -def make_input(project: RATapi.Project, controls: RATapi.Controls) -> tuple[ProblemDefinition, Limits, Control]: +def make_input(project: RATapi.Project, controls: RATapi.Controls) -> tuple[ProblemDefinition, Control]: """Construct the inputs required for the compiled RAT code using the data defined in the input project and controls. Parameters @@ -127,28 +127,14 @@ def make_input(project: RATapi.Project, controls: RATapi.Controls) -> tuple[Prob ------- problem : RAT.rat_core.ProblemDefinition The problem input used in the compiled RAT code. - limits : RAT.rat_core.Limits - A list of min/max values for each parameter defined in the project. cpp_controls : RAT.rat_core.Control The controls object used in the compiled RAT code. """ - limits = Limits() - - for class_list in RATapi.project.parameter_class_lists: - setattr( - limits, - parameter_field[class_list], - [[element.min, element.max] for element in getattr(project, class_list)], - ) - - if project.model == LayerModels.CustomXY: - controls.calcSldDuringFit = True - problem = make_problem(project) cpp_controls = make_controls(controls) - return problem, limits, cpp_controls + return problem, cpp_controls def make_problem(project: RATapi.Project) -> ProblemDefinition: diff --git a/RATapi/outputs.py b/RATapi/outputs.py index 84b3222d..150ebc2d 100644 --- a/RATapi/outputs.py +++ b/RATapi/outputs.py @@ -141,12 +141,13 @@ class Results: The background for each contrast defined over the simulation range. resolutions : list The resolution for each contrast defined over the simulation range. - layerSlds : list - The array of layer parameter values for each contrast. sldProfiles : list The SLD profiles for each contrast. + layers : list + The array of layer parameter values for each contrast. resampledLayers : list - If resampling is used, the SLD for each contrast after resampling has been performed. + If resampling is used, the array of layer parameter values for each contrast after resampling has been + performed. calculationResults : CalculationResults The chi-squared fit results from the final calculation and fit. contrastParams : ContrastParams @@ -164,8 +165,8 @@ class Results: shiftedData: list backgrounds: list resolutions: list - layerSlds: list sldProfiles: list + layers: list resampledLayers: list calculationResults: CalculationResults contrastParams: ContrastParams @@ -323,8 +324,6 @@ class DreamOutput(RATResult): The runtime of the DREAM algorithm in seconds. iteration : float The number of iterations performed. - modelOutput : float - Unused. Will always be 0. AR : np.ndarray A two-column array where ``DreamOutput.AR[i, 0]`` is an iteration number and ``DreamOutput.AR[i, 1]`` is the average acceptance rate of chain step @@ -344,7 +343,6 @@ class DreamOutput(RATResult): outlierChains: np.ndarray runtime: float iteration: float - modelOutput: float AR: np.ndarray R_stat: np.ndarray CR: np.ndarray @@ -483,7 +481,6 @@ def make_results( outlierChains=bayes_results.dreamOutput.outlierChains, runtime=bayes_results.dreamOutput.runtime, iteration=bayes_results.dreamOutput.iteration, - modelOutput=bayes_results.dreamOutput.modelOutput, AR=bayes_results.dreamOutput.AR, R_stat=bayes_results.dreamOutput.R_stat, CR=bayes_results.dreamOutput.CR, @@ -502,8 +499,8 @@ def make_results( shiftedData=output_results.shiftedData, backgrounds=output_results.backgrounds, resolutions=output_results.resolutions, - layerSlds=output_results.layerSlds, sldProfiles=output_results.sldProfiles, + layers=output_results.layers, resampledLayers=output_results.resampledLayers, calculationResults=calculation_results, contrastParams=contrast_params, @@ -524,8 +521,8 @@ def make_results( shiftedData=output_results.shiftedData, backgrounds=output_results.backgrounds, resolutions=output_results.resolutions, - layerSlds=output_results.layerSlds, sldProfiles=output_results.sldProfiles, + layers=output_results.layers, resampledLayers=output_results.resampledLayers, calculationResults=calculation_results, contrastParams=contrast_params, diff --git a/RATapi/run.py b/RATapi/run.py index 17f47ecc..476daa39 100644 --- a/RATapi/run.py +++ b/RATapi/run.py @@ -109,7 +109,7 @@ def run(project, controls): horizontal_line = "\u2500" * 107 + "\n" display_on = controls.display != Display.Off - problem_definition, limits, cpp_controls = make_input(project, controls) + problem_definition, cpp_controls = make_input(project, controls) if display_on: print("Starting RAT " + horizontal_line) @@ -118,7 +118,6 @@ def run(project, controls): with ProgressBar(display=display_on), TextOutput(display=display_on): problem_definition, output_results, bayes_results = RATapi.rat_core.RATMain( problem_definition, - limits, cpp_controls, ) end = time.time() diff --git a/cpp/RAT b/cpp/RAT index 282d17be..df626113 160000 --- a/cpp/RAT +++ b/cpp/RAT @@ -1 +1 @@ -Subproject commit 282d17beb964af0855c1b2cc412e0b187b1a4140 +Subproject commit df626113d7a930cfa23c1ab47fdf9b1111b52123 diff --git a/cpp/includes/defines.h b/cpp/includes/defines.h index ffecaec3..67b0fda7 100644 --- a/cpp/includes/defines.h +++ b/cpp/includes/defines.h @@ -237,8 +237,6 @@ runtime : float The runtime of the DREAM algorithm in seconds. iteration : float The number of iterations performed. -modelOutput : float - Unused. Will always be 0. AR : np.ndarray[np.float] A two-column array where ``DreamOutput.AR[i, 0]`` is an iteration number and ``DreamOutput.AR[i, 1]`` is the average acceptance rate of chain step @@ -259,7 +257,6 @@ struct DreamOutput py::array_t outlierChains; real_T runtime; real_T iteration; - real_T modelOutput; py::array_t AR; py::array_t R_stat; py::array_t CR; @@ -354,12 +351,12 @@ backgrounds : list The background for each contrast defined over the simulation range. resolutions : list The resolution for each contrast defined over the simulation range. -layerSlds : list - The array of layer parameter values for each contrast. sldProfiles : list The SLD profiles for each contrast. +layers : list + The array of layer parameter values for each contrast. resampledLayers : list - If resampling is used, the SLD for each contrast after resampling has been performed. + If resampling is used, the array of layer parameter values for each contrast after resampling has been performed. calculationResults : RATapi.rat_core.Calculation The chi-squared fit results from the final calculation and fit. contrastParams : RATapi.rat_core.ContrastParams @@ -377,8 +374,8 @@ struct OutputResult { py::list shiftedData; py::list backgrounds; py::list resolutions; - py::list layerSlds; py::list sldProfiles; + py::list layers; py::list resampledLayers; Calculation calculationResults {}; ContrastParams contrastParams {}; @@ -386,37 +383,6 @@ struct OutputResult { py::list fitNames; }; -const std::string docsLimits = R"(The Python binding for the C++ limit struct which contains -Min and max values for each parameter defined in the project. - -Parameters ----------- -params : np.ndarray[np.float] - Limits for params in the problem definition. -backgroundParams : np.ndarray[np.float] - Limits for backgroundParams in the problem definition. -scalefactors : np.ndarray[np.float] - Limits for scalefactors in the problem definition. -bulkIns : np.ndarray[np.float] - Limits for bulkIns in the problem definition. -bulkOuts : np.ndarray[np.float] - Limits for bulkOuts in the problem definition. -resolutionParams : np.ndarray[np.float] - Limits for resolutionParams in the problem definition. -domainRatios : np.ndarray[np.float] - Limits for domainRatios in the problem definition. -)"; - -struct Limits { - py::array_t params; - py::array_t backgroundParams; - py::array_t scalefactors; - py::array_t bulkIns; - py::array_t bulkOuts; - py::array_t resolutionParams; - py::array_t domainRatios; -}; - const std::string docsNameStore = R"(The Python binding for the C++ names struct which contains names of all parameters in the project. diff --git a/cpp/rat.cpp b/cpp/rat.cpp index d8d48d68..a92dc49d 100644 --- a/cpp/rat.cpp +++ b/cpp/rat.cpp @@ -301,21 +301,6 @@ RAT::b_ProblemDefinition createProblemDefinitionStruct(const ProblemDefinition& } -RAT::ProblemLimits createProblemLimitsStruct(const Limits& limits) -{ - RAT::ProblemLimits limits_struct; - limits_struct.params = customCaller("Limits.params", pyArrayToRatArray2d, limits.params); - limits_struct.backgroundParams = customCaller("Limits.backgroundParams", pyArrayToRatArray2d, limits.backgroundParams); - limits_struct.scalefactors = customCaller("Limits.scalefactors", pyArrayToRatArray2d, limits.scalefactors); - limits_struct.bulkIns = customCaller("Limits.bulkIns", pyArrayToRatArray2d, limits.bulkIns); - limits_struct.bulkOuts = customCaller("Limits.bulkOuts", pyArrayToRatArray2d, limits.bulkOuts); - limits_struct.resolutionParams = customCaller("Limits.resolutionParams", pyArrayToRatArray2d, limits.resolutionParams); - limits_struct.domainRatios = customCaller("Limits.domainRatios", pyArrayToRatArray2d, limits.domainRatios); - - return limits_struct; -} - - RAT::Controls createControlsStruct(const Control& control) { RAT::Controls control_struct; @@ -391,26 +376,26 @@ OutputResult OutputResultFromStruct(const RAT::Results result) output_result.resolutions.append(array); } - for (int32_T idx0{0}; idx0 < result.layerSlds.size(0); idx0++) { + for (int32_T idx0{0}; idx0 < result.sldProfiles.size(0); idx0++) { py::list inner_list; - for (int32_T idx1{0}; idx1 < result.layerSlds.size(1); idx1++) { - auto tmp = result.layerSlds[idx0 + result.layerSlds.size(0) * idx1]; + for (int32_T idx1{0}; idx1 < result.sldProfiles.size(1); idx1++) { + auto tmp = result.sldProfiles[idx0 + result.sldProfiles.size(0) * idx1]; auto array = py::array_t({tmp.f1.size(0), tmp.f1.size(1)}); std::memcpy(array.request().ptr, tmp.f1.data(), array.nbytes()); inner_list.append(array); } - output_result.layerSlds.append(inner_list); + output_result.sldProfiles.append(inner_list); } - for (int32_T idx0{0}; idx0 < result.sldProfiles.size(0); idx0++) { + for (int32_T idx0{0}; idx0 < result.layers.size(0); idx0++) { py::list inner_list; - for (int32_T idx1{0}; idx1 < result.sldProfiles.size(1); idx1++) { - auto tmp = result.sldProfiles[idx0 + result.sldProfiles.size(0) * idx1]; + for (int32_T idx1{0}; idx1 < result.layers.size(1); idx1++) { + auto tmp = result.layers[idx0 + result.layers.size(0) * idx1]; auto array = py::array_t({tmp.f1.size(0), tmp.f1.size(1)}); std::memcpy(array.request().ptr, tmp.f1.data(), array.nbytes()); inner_list.append(array); } - output_result.sldProfiles.append(inner_list); + output_result.layers.append(inner_list); } for (int32_T idx0{0}; idx0 < result.resampledLayers.size(0); idx0++) { @@ -548,7 +533,6 @@ BayesResults bayesResultsFromStruct(const RAT::BayesResults results) bayesResults.dreamOutput.outlierChains = pyArray2dFromBoundedArray>(results.dreamOutput.outlierChains); bayesResults.dreamOutput.runtime = results.dreamOutput.runtime; bayesResults.dreamOutput.iteration = results.dreamOutput.iteration; - bayesResults.dreamOutput.modelOutput = results.dreamOutput.modelOutput; bayesResults.dreamOutput.R_stat = pyArrayFromRatArray2d(results.dreamOutput.R_stat); bayesResults.dreamOutput.CR = pyArrayFromRatArray2d(results.dreamOutput.CR); bayesResults.dreamOutput.AR = pyArray2dFromBoundedArray>(results.dreamOutput.AR); @@ -582,8 +566,6 @@ Parameters ---------- problem_def : Rat.rat_core.ProblemDefinition The project input for the RAT calculation. -limits : RATapi.rat_core.Limits - Min and max values for each parameter defined in the problem definition. control : RATapi.rat_core.Control The controls object for the RAT calculation. @@ -597,16 +579,15 @@ bayes_result : Rat.rat_core.BayesResults The extra results if RAT calculation is Bayesian. )"; -py::tuple RATMain(const ProblemDefinition& problem_def, const Limits& limits, const Control& control) +py::tuple RATMain(const ProblemDefinition& problem_def, const Control& control) { RAT::b_ProblemDefinition problem_def_struct = createProblemDefinitionStruct(problem_def); - RAT::ProblemLimits limits_struct = createProblemLimitsStruct(limits); RAT::Controls control_struct = createControlsStruct(control); // Output RAT::Results results; RAT::BayesResults bayesResults; // Call the entry-point - RAT::RATMain(&problem_def_struct, &limits_struct, &control_struct, &results, &bayesResults); + RAT::RATMain(&problem_def_struct, &control_struct, &results, &bayesResults); // Copy result to output auto out_problem_def = problemDefinitionFromStruct(problem_def_struct); out_problem_def.customFiles = problem_def.customFiles.attr("copy")(); @@ -802,7 +783,6 @@ PYBIND11_MODULE(rat_core, m) { .def_readwrite("outlierChains", &DreamOutput::outlierChains) .def_readwrite("runtime", &DreamOutput::runtime) .def_readwrite("iteration", &DreamOutput::iteration) - .def_readwrite("modelOutput", &DreamOutput::modelOutput) .def_readwrite("AR", &DreamOutput::AR) .def_readwrite("R_stat", &DreamOutput::R_stat) .def_readwrite("CR", &DreamOutput::CR); @@ -836,8 +816,8 @@ PYBIND11_MODULE(rat_core, m) { .def_readwrite("shiftedData", &OutputResult::shiftedData) .def_readwrite("backgrounds", &OutputResult::backgrounds) .def_readwrite("resolutions", &OutputResult::resolutions) - .def_readwrite("layerSlds", &OutputResult::layerSlds) .def_readwrite("sldProfiles", &OutputResult::sldProfiles) + .def_readwrite("layers", &OutputResult::layers) .def_readwrite("resampledLayers", &OutputResult::resampledLayers) .def_readwrite("calculationResults", &OutputResult::calculationResults) .def_readwrite("contrastParams", &OutputResult::contrastParams) @@ -912,39 +892,6 @@ PYBIND11_MODULE(rat_core, m) { return chk; })); - py::class_(m, "Limits", docsLimits.c_str()) - .def(py::init<>()) - .def_readwrite("params", &Limits::params) - .def_readwrite("backgroundParams", &Limits::backgroundParams) - .def_readwrite("scalefactors", &Limits::scalefactors) - .def_readwrite("bulkIns", &Limits::bulkIns) - .def_readwrite("bulkOuts", &Limits::bulkOuts) - .def_readwrite("resolutionParams", &Limits::resolutionParams) - .def_readwrite("domainRatios", &Limits::domainRatios) - .def(py::pickle( - [](const Limits &lim) { // __getstate__ - /* Return a tuple that fully encodes the state of the object */ - return py::make_tuple(lim.params, lim.backgroundParams, lim.scalefactors, lim.bulkIns, lim.bulkOuts, - lim.resolutionParams, lim.domainRatios); - }, - [](py::tuple t) { // __setstate__ - if (t.size() != 7) - throw std::runtime_error("Encountered invalid state unpickling Limits object!"); - - /* Create a new C++ instance */ - Limits lim; - - lim.params = t[0].cast>(); - lim.backgroundParams = t[1].cast>(); - lim.scalefactors = t[2].cast>(); - lim.bulkIns = t[3].cast>(); - lim.bulkOuts = t[4].cast>(); - lim.resolutionParams = t[5].cast>(); - lim.domainRatios = t[6].cast>(); - - return lim; - })); - py::class_(m, "Control", docsControl.c_str()) .def(py::init<>()) .def_readwrite("parallel", &Control::parallel) @@ -1150,7 +1097,7 @@ PYBIND11_MODULE(rat_core, m) { return p; })); - m.def("RATMain", &RATMain, docsRATMain.c_str(), py::arg("problem_def"), py::arg("limits"), py::arg("control")); + m.def("RATMain", &RATMain, docsRATMain.c_str(), py::arg("problem_def"), py::arg("control")); m.def("makeSLDProfileXY", &makeSLDProfileXY, docsMakeSLDProfileXY.c_str(), py::arg("bulk_in"), py::arg("bulk_out"), py::arg("ssub"), py::arg("layers"), py::arg("number_of_repeats") = DEFAULT_NREPEATS); diff --git a/tests/conftest.py b/tests/conftest.py index ce580c15..50fbc11c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -774,36 +774,6 @@ def reflectivity_calculation_output_results(): ] ), ] - results.layerSlds = [ - [ - np.array( - [ - [1.954000e01, 4.001499e-06, 3.000000e00], - [2.266000e01, -6.586988e-08, 3.000000e00], - [8.560000e00, 3.672535e-06, 5.640000e00], - [1.712000e01, 5.980000e-06, 5.640000e00], - [1.070000e01, 3.100365e-06, 6.014000e00], - [1.782000e01, 6.751924e-07, 6.014000e00], - [1.782000e01, 6.751924e-07, 6.014000e00], - [1.070000e01, 3.100365e-06, 6.014000e00], - ], - ), - ], - [ - np.array( - [ - [1.9540000e01, 3.1114020e-06, 3.0000000e00], - [2.2660000e01, -2.6387028e-07, 3.0000000e00], - [8.5600000e00, 1.9590700e-06, 5.6400000e00], - [1.7120000e01, 2.2100000e-06, 5.6400000e00], - [1.0700000e01, 1.7375100e-06, 6.0140000e00], - [1.7820000e01, 1.0164400e-08, 6.0140000e00], - [1.7820000e01, 1.0164400e-08, 6.0140000e00], - [1.0700000e01, 1.7375100e-06, 6.0140000e00], - ], - ), - ], - ] results.sldProfiles = [ [ np.array( @@ -866,6 +836,36 @@ def reflectivity_calculation_output_results(): ), ], ] + results.layers = [ + [ + np.array( + [ + [1.954000e01, 4.001499e-06, 3.000000e00], + [2.266000e01, -6.586988e-08, 3.000000e00], + [8.560000e00, 3.672535e-06, 5.640000e00], + [1.712000e01, 5.980000e-06, 5.640000e00], + [1.070000e01, 3.100365e-06, 6.014000e00], + [1.782000e01, 6.751924e-07, 6.014000e00], + [1.782000e01, 6.751924e-07, 6.014000e00], + [1.070000e01, 3.100365e-06, 6.014000e00], + ], + ), + ], + [ + np.array( + [ + [1.9540000e01, 3.1114020e-06, 3.0000000e00], + [2.2660000e01, -2.6387028e-07, 3.0000000e00], + [8.5600000e00, 1.9590700e-06, 5.6400000e00], + [1.7120000e01, 2.2100000e-06, 5.6400000e00], + [1.0700000e01, 1.7375100e-06, 6.0140000e00], + [1.7820000e01, 1.0164400e-08, 6.0140000e00], + [1.7820000e01, 1.0164400e-08, 6.0140000e00], + [1.0700000e01, 1.7375100e-06, 6.0140000e00], + ], + ), + ], + ] results.resampledLayers = [[np.array([[0.0, 0.0, 0.0]])], [np.array([[0.0, 0.0, 0.0]])]] results.calculationResults = RATapi.rat_core.Calculation() results.calculationResults.chiValues = np.array([202.83057377, 1641.4024969]) @@ -1430,36 +1430,6 @@ def reflectivity_calculation_results(): ] ), ], - layerSlds=[ - [ - np.array( - [ - [1.954000e01, 4.001499e-06, 3.000000e00], - [2.266000e01, -6.586988e-08, 3.000000e00], - [8.560000e00, 3.672535e-06, 5.640000e00], - [1.712000e01, 5.980000e-06, 5.640000e00], - [1.070000e01, 3.100365e-06, 6.014000e00], - [1.782000e01, 6.751924e-07, 6.014000e00], - [1.782000e01, 6.751924e-07, 6.014000e00], - [1.070000e01, 3.100365e-06, 6.014000e00], - ], - ), - ], - [ - np.array( - [ - [1.9540000e01, 3.1114020e-06, 3.0000000e00], - [2.2660000e01, -2.6387028e-07, 3.0000000e00], - [8.5600000e00, 1.9590700e-06, 5.6400000e00], - [1.7120000e01, 2.2100000e-06, 5.6400000e00], - [1.0700000e01, 1.7375100e-06, 6.0140000e00], - [1.7820000e01, 1.0164400e-08, 6.0140000e00], - [1.7820000e01, 1.0164400e-08, 6.0140000e00], - [1.0700000e01, 1.7375100e-06, 6.0140000e00], - ], - ), - ], - ], sldProfiles=[ [ np.array( @@ -1522,6 +1492,36 @@ def reflectivity_calculation_results(): ), ], ], + layers=[ + [ + np.array( + [ + [1.954000e01, 4.001499e-06, 3.000000e00], + [2.266000e01, -6.586988e-08, 3.000000e00], + [8.560000e00, 3.672535e-06, 5.640000e00], + [1.712000e01, 5.980000e-06, 5.640000e00], + [1.070000e01, 3.100365e-06, 6.014000e00], + [1.782000e01, 6.751924e-07, 6.014000e00], + [1.782000e01, 6.751924e-07, 6.014000e00], + [1.070000e01, 3.100365e-06, 6.014000e00], + ], + ), + ], + [ + np.array( + [ + [1.9540000e01, 3.1114020e-06, 3.0000000e00], + [2.2660000e01, -2.6387028e-07, 3.0000000e00], + [8.5600000e00, 1.9590700e-06, 5.6400000e00], + [1.7120000e01, 2.2100000e-06, 5.6400000e00], + [1.0700000e01, 1.7375100e-06, 6.0140000e00], + [1.7820000e01, 1.0164400e-08, 6.0140000e00], + [1.7820000e01, 1.0164400e-08, 6.0140000e00], + [1.0700000e01, 1.7375100e-06, 6.0140000e00], + ], + ), + ], + ], resampledLayers=[[np.array([[0.0, 0.0, 0.0]])], [np.array([[0.0, 0.0, 0.0]])]], calculationResults=RATapi.outputs.CalculationResults( chiValues=np.array([202.83057377, 1641.4024969]), @@ -2091,36 +2091,6 @@ def dream_output_results(): ] ), ] - results.layerSlds = [ - [ - np.array( - [ - [3.15755349e01, 3.35278238e-06, 4.16625659e00], - [3.61791464e01, 7.68327921e-07, 4.16625659e00], - [1.00488530e01, 2.06044530e-06, 2.78042232e01], - [1.08043784e01, 3.29384190e-06, 2.78042232e01], - [2.42251646e01, 2.35556998e-06, 1.55593097e01], - [1.49022278e01, 7.42138004e-07, 1.55593097e01], - [1.49022278e01, 7.42138004e-07, 1.55593097e01], - [2.42251646e01, 2.35556998e-06, 1.55593097e01], - ], - ), - ], - [ - np.array( - [ - [3.15755349e01, 4.11636356e-06, 4.16625659e00], - [3.61791464e01, 1.39268494e-06, 4.16625659e00], - [1.00488530e01, 2.45715680e-06, 2.78042232e01], - [1.08043784e01, 5.26668495e-06, 2.78042232e01], - [2.42251646e01, 3.31348777e-06, 1.55593097e01], - [1.49022278e01, 1.37428245e-06, 1.55593097e01], - [1.49022278e01, 1.37428245e-06, 1.55593097e01], - [2.42251646e01, 3.31348777e-06, 1.55593097e01], - ], - ), - ], - ] results.sldProfiles = [ [ np.array( @@ -2191,6 +2161,36 @@ def dream_output_results(): ), ], ] + results.layers = [ + [ + np.array( + [ + [3.15755349e01, 3.35278238e-06, 4.16625659e00], + [3.61791464e01, 7.68327921e-07, 4.16625659e00], + [1.00488530e01, 2.06044530e-06, 2.78042232e01], + [1.08043784e01, 3.29384190e-06, 2.78042232e01], + [2.42251646e01, 2.35556998e-06, 1.55593097e01], + [1.49022278e01, 7.42138004e-07, 1.55593097e01], + [1.49022278e01, 7.42138004e-07, 1.55593097e01], + [2.42251646e01, 2.35556998e-06, 1.55593097e01], + ], + ), + ], + [ + np.array( + [ + [3.15755349e01, 4.11636356e-06, 4.16625659e00], + [3.61791464e01, 1.39268494e-06, 4.16625659e00], + [1.00488530e01, 2.45715680e-06, 2.78042232e01], + [1.08043784e01, 5.26668495e-06, 2.78042232e01], + [2.42251646e01, 3.31348777e-06, 1.55593097e01], + [1.49022278e01, 1.37428245e-06, 1.55593097e01], + [1.49022278e01, 1.37428245e-06, 1.55593097e01], + [2.42251646e01, 3.31348777e-06, 1.55593097e01], + ], + ), + ], + ] results.resampledLayers = [[np.array([[0.0, 0.0, 0.0]])], [np.array([[0.0, 0.0, 0.0]])]] results.calculationResults = RATapi.rat_core.Calculation() results.calculationResults.chiValues = (np.array([4.6077885, 7.00028098]),) @@ -3984,7 +3984,6 @@ def dream_bayes(): bayes.dreamOutput.outlierChains = np.array([[0.0, 0.0]]) bayes.dreamOutput.runtime = 2.6e-06 bayes.dreamOutput.iteration = 2.0 - bayes.dreamOutput.modelOutput = 0.0 bayes.dreamOutput.AR = np.array([[1.0, np.nan]]) bayes.dreamOutput.R_stat = np.array( [ @@ -4576,36 +4575,6 @@ def dream_results(): ] ), ], - layerSlds=[ - [ - np.array( - [ - [3.15755349e01, 3.35278238e-06, 4.16625659e00], - [3.61791464e01, 7.68327921e-07, 4.16625659e00], - [1.00488530e01, 2.06044530e-06, 2.78042232e01], - [1.08043784e01, 3.29384190e-06, 2.78042232e01], - [2.42251646e01, 2.35556998e-06, 1.55593097e01], - [1.49022278e01, 7.42138004e-07, 1.55593097e01], - [1.49022278e01, 7.42138004e-07, 1.55593097e01], - [2.42251646e01, 2.35556998e-06, 1.55593097e01], - ], - ), - ], - [ - np.array( - [ - [3.15755349e01, 4.11636356e-06, 4.16625659e00], - [3.61791464e01, 1.39268494e-06, 4.16625659e00], - [1.00488530e01, 2.45715680e-06, 2.78042232e01], - [1.08043784e01, 5.26668495e-06, 2.78042232e01], - [2.42251646e01, 3.31348777e-06, 1.55593097e01], - [1.49022278e01, 1.37428245e-06, 1.55593097e01], - [1.49022278e01, 1.37428245e-06, 1.55593097e01], - [2.42251646e01, 3.31348777e-06, 1.55593097e01], - ], - ), - ], - ], sldProfiles=[ [ np.array( @@ -4676,6 +4645,36 @@ def dream_results(): ), ], ], + layers=[ + [ + np.array( + [ + [3.15755349e01, 3.35278238e-06, 4.16625659e00], + [3.61791464e01, 7.68327921e-07, 4.16625659e00], + [1.00488530e01, 2.06044530e-06, 2.78042232e01], + [1.08043784e01, 3.29384190e-06, 2.78042232e01], + [2.42251646e01, 2.35556998e-06, 1.55593097e01], + [1.49022278e01, 7.42138004e-07, 1.55593097e01], + [1.49022278e01, 7.42138004e-07, 1.55593097e01], + [2.42251646e01, 2.35556998e-06, 1.55593097e01], + ], + ), + ], + [ + np.array( + [ + [3.15755349e01, 4.11636356e-06, 4.16625659e00], + [3.61791464e01, 1.39268494e-06, 4.16625659e00], + [1.00488530e01, 2.45715680e-06, 2.78042232e01], + [1.08043784e01, 5.26668495e-06, 2.78042232e01], + [2.42251646e01, 3.31348777e-06, 1.55593097e01], + [1.49022278e01, 1.37428245e-06, 1.55593097e01], + [1.49022278e01, 1.37428245e-06, 1.55593097e01], + [2.42251646e01, 3.31348777e-06, 1.55593097e01], + ], + ), + ], + ], resampledLayers=[[np.array([[0.0, 0.0, 0.0]])], [np.array([[0.0, 0.0, 0.0]])]], calculationResults=RATapi.outputs.CalculationResults( chiValues=np.array([4.6077885, 7.00028098]), @@ -6463,7 +6462,6 @@ def dream_results(): outlierChains=np.array([[0.0, 0.0]]), runtime=2.6e-06, iteration=2.0, - modelOutput=0.0, AR=np.array([[1.0, np.nan]]), R_stat=np.array( [ diff --git a/tests/test_inputs.py b/tests/test_inputs.py index d931ff5f..2874a888 100644 --- a/tests/test_inputs.py +++ b/tests/test_inputs.py @@ -9,7 +9,7 @@ import RATapi import RATapi.wrappers from RATapi.inputs import FileHandles, check_indices, make_controls, make_input, make_problem -from RATapi.rat_core import Checks, Control, Limits, NameStore, ProblemDefinition +from RATapi.rat_core import Checks, Control, NameStore, ProblemDefinition from RATapi.utils.enums import ( BackgroundActions, BoundHandling, @@ -351,36 +351,6 @@ def custom_xy_problem(test_names, test_checks): return problem -@pytest.fixture -def normal_limits(): - """The expected limits object from "standard_layers_project" and "custom_xy_project".""" - limits = Limits() - limits.params = [[1.0, 5.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0]] - limits.backgroundParams = [[1e-7, 1e-5]] - limits.scalefactors = [[0.02, 0.25]] - limits.bulkIns = [[0.0, 0.0]] - limits.bulkOuts = [[6.2e-6, 6.35e-6]] - limits.resolutionParams = [[0.01, 0.05]] - limits.domainRatios = [] - - return limits - - -@pytest.fixture -def domains_limits(): - """The expected limits object from "domains_project".""" - limits = Limits() - limits.params = [[1.0, 5.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0]] - limits.backgroundParams = [[1e-7, 1e-5]] - limits.scalefactors = [[0.02, 0.25]] - limits.bulkIns = [[0.0, 0.0]] - limits.bulkOuts = [[6.2e-6, 6.35e-6]] - limits.resolutionParams = [[0.01, 0.05]] - limits.domainRatios = [[0.4, 0.6]] - - return limits - - @pytest.fixture def standard_layers_controls(): """The expected controls object for input to the compiled RAT code given the default inputs and @@ -427,7 +397,7 @@ def custom_xy_controls(): controls = Control() controls.procedure = Procedures.Calculate controls.parallel = Parallel.Single - controls.calcSldDuringFit = True + controls.calcSldDuringFit = False controls.resampleMinAngle = 0.9 controls.resampleNPoints = 50.0 controls.display = Display.Iter @@ -458,55 +428,38 @@ def custom_xy_controls(): @pytest.mark.parametrize( - ["test_project", "test_problem", "test_limits", "test_controls"], + ["test_project", "test_problem", "test_controls"], [ ( "standard_layers_project", "standard_layers_problem", - "normal_limits", "standard_layers_controls", ), ( "custom_xy_project", "custom_xy_problem", - "normal_limits", "custom_xy_controls", ), ( "domains_project", "domains_problem", - "domains_limits", "standard_layers_controls", ), ], ) -def test_make_input(test_project, test_problem, test_limits, test_controls, request) -> None: - """When converting the "project" and "controls", we should obtain the five input objects required for the compiled +def test_make_input(test_project, test_problem, test_controls, request) -> None: + """When converting the "project" and "controls", we should obtain the two input objects required for the compiled RAT code. """ test_project = request.getfixturevalue(test_project) test_problem = request.getfixturevalue(test_problem) - test_limits = request.getfixturevalue(test_limits) test_controls = request.getfixturevalue(test_controls) - parameter_fields = [ - "params", - "backgroundParams", - "scalefactors", - "bulkIns", - "bulkOuts", - "resolutionParams", - "domainRatios", - ] + problem, controls = make_input(test_project, RATapi.Controls()) - problem, limits, controls = make_input(test_project, RATapi.Controls()) problem = pickle.loads(pickle.dumps(problem)) check_problem_equal(problem, test_problem) - limits = pickle.loads(pickle.dumps(limits)) - for limit_field in parameter_fields: - assert np.all(getattr(limits, limit_field) == getattr(test_limits, limit_field)) - controls = pickle.loads(pickle.dumps(controls)) check_controls_equal(controls, test_controls) diff --git a/tests/test_outputs.py b/tests/test_outputs.py index 7d9a0afd..37a4c631 100644 --- a/tests/test_outputs.py +++ b/tests/test_outputs.py @@ -24,8 +24,8 @@ def reflectivity_calculation_str(): "shiftedData = [Data array: [21 x 3], Data array: [21 x 3]],\n" "backgrounds = [Data array: [82 x 3], Data array: [82 x 3]],\n" "resolutions = [Data array: [82 x 2], Data array: [82 x 2]],\n" - "layerSlds = [[Data array: [8 x 3]], [Data array: [8 x 3]]],\n" "sldProfiles = [[Data array: [25 x 2], Data array: [25 x 2]]],\n" + "layers = [[Data array: [8 x 3]], [Data array: [8 x 3]]],\n" "resampledLayers = [[Data array: [1 x 3]], [Data array: [1 x 3]]],\n" "calculationResults = CalculationResults(\n" "\tchiValues = [ 202.83057377 1641.4024969 ],\n" @@ -59,8 +59,8 @@ def dream_str(): "shiftedData = [Data array: [21 x 3], Data array: [21 x 3]],\n" "backgrounds = [Data array: [82 x 3], Data array: [82 x 3]],\n" "resolutions = [Data array: [82 x 2], Data array: [82 x 2]],\n" - "layerSlds = [[Data array: [8 x 3]], [Data array: [8 x 3]]],\n" "sldProfiles = [[Data array: [29 x 2], Data array: [29 x 2]]],\n" + "layers = [[Data array: [8 x 3]], [Data array: [8 x 3]]],\n" "resampledLayers = [[Data array: [1 x 3]], [Data array: [1 x 3]]],\n" "calculationResults = CalculationResults(\n" "\tchiValues = [4.6077885 7.00028098],\n" @@ -119,7 +119,6 @@ def dream_str(): "\toutlierChains = Data array: [1 x 2],\n" "\truntime = 2.6e-06,\n" "\titeration = 2.0,\n" - "\tmodelOutput = 0.0,\n" "\tAR = Data array: [1 x 2],\n" "\tR_stat = Data array: [1 x 19],\n" "\tCR = Data array: [1 x 4],\n" diff --git a/tests/utils.py b/tests/utils.py index a9411aac..40524fd0 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -31,7 +31,7 @@ def check_results_equal(actual_results, expected_results) -> None: "CalculationResults" and "ContrastParams". """ list_fields = ["reflectivity", "simulation", "shiftedData", "backgrounds", "resolutions"] - double_list_fields = ["layerSlds", "sldProfiles", "resampledLayers"] + double_list_fields = ["sldProfiles", "layers", "resampledLayers"] contrast_param_fields = [ "scalefactors", "bulkIn", @@ -109,7 +109,7 @@ def check_bayes_fields_equal(actual_results, expected_results) -> None: "IO", "storeOutput", ], - "dreamOutput": ["runtime", "iteration", "modelOutput"], + "dreamOutput": ["runtime", "iteration"], "nestedSamplerOutput": ["logZ"], } From b92118a70748ceef790139ed7dbe040933c15afb Mon Sep 17 00:00:00 2001 From: Paul Sharp <44529197+DrPaulSharp@users.noreply.github.com> Date: Thu, 27 Mar 2025 17:20:57 +0000 Subject: [PATCH 15/20] Fixes bugs in model validation when using "set_fields" (#144) * Changes model warnings to errors * Adds clause to "set_fields" to address bug * Updates tests --- RATapi/classlist.py | 5 +++++ RATapi/models.py | 22 +++++++++---------- tests/test_models.py | 46 +++++++++++++++++++++++++++++++-------- tests/test_project.py | 50 ------------------------------------------- 4 files changed, 52 insertions(+), 71 deletions(-) diff --git a/RATapi/classlist.py b/RATapi/classlist.py index b046f885..f0a61d3b 100644 --- a/RATapi/classlist.py +++ b/RATapi/classlist.py @@ -318,6 +318,11 @@ def set_fields(self, index: Union[int, slice, str, T], **kwargs) -> None: if isinstance(index, (str, self._class_handle)): index = self.index(index) + # Prioritise changing language to avoid CustomFile validator bug + value = kwargs.pop("language", None) + if value is not None: + kwargs = {"language": value, **kwargs} + if importlib.util.find_spec("pydantic"): # Pydantic is installed, so set up a context manager that will # suppress custom validation errors until all fields have been set. diff --git a/RATapi/models.py b/RATapi/models.py index b43112d8..e8e207ae 100644 --- a/RATapi/models.py +++ b/RATapi/models.py @@ -119,8 +119,8 @@ class Background(Signal): name: str = Field(default_factory=lambda: f"New Background {next(background_number)}", min_length=1) @model_validator(mode="after") - def warn_parameters(self): - """Raise a warning if the parameters given are not expected for the given type.""" + def check_unsupported_parameters(self): + """Raise an error if the parameters given are not supported for the given type.""" if self.type == TypeOptions.Constant: expected_empty_fields = ["value_1", "value_2", "value_3", "value_4", "value_5"] elif self.type == TypeOptions.Data: @@ -130,10 +130,9 @@ def warn_parameters(self): non_empty_fields = [v for v in expected_empty_fields if getattr(self, v) != ""] if non_empty_fields: - warnings.warn( - "The following values are not recognised by this background type and will be ignored: " - f"{', '.join(non_empty_fields)}", - stacklevel=2, + raise ValueError( + f'The following values are not supported by the "{self.type}" Background type: ' + f"{', '.join(non_empty_fields)}" ) return self @@ -630,8 +629,8 @@ def validate_unimplemented_resolutions(cls, type: TypeOptions): return type @model_validator(mode="after") - def warn_parameters(self): - """Raise a warning if the parameters given are not expected for the given type.""" + def check_unsupported_parameters(self): + """Raise an error if the parameters given are not supported for the given type.""" if self.type == TypeOptions.Constant: expected_empty_fields = ["value_1", "value_2", "value_3", "value_4", "value_5"] elif self.type == TypeOptions.Data: @@ -641,10 +640,9 @@ def warn_parameters(self): non_empty_fields = [v for v in expected_empty_fields if getattr(self, v) != ""] if non_empty_fields: - warnings.warn( - "The following values are not recognised by this resolution type and will be ignored: " - f"{', '.join(non_empty_fields)}", - stacklevel=2, + raise ValueError( + f'The following values are not supported by the "{self.type}" Resolution type: ' + f"{', '.join(non_empty_fields)}" ) return self diff --git a/tests/test_models.py b/tests/test_models.py index a387dede..703e1f38 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -334,22 +334,50 @@ def test_contrast_bad_ratio(): RATapi.models.Contrast(name="My Contrast", domain_ratio="bad ratio") -@pytest.mark.parametrize("model", [RATapi.models.Background, RATapi.models.Resolution]) -@pytest.mark.filterwarnings("ignore:The following values are not recognised by this*:UserWarning") -def test_type_change_clear(model): +@pytest.mark.parametrize( + ["model", "type", "values"], + [ + (RATapi.models.Background, "function", ["val1", "val2", "val3", "val4", "val5"]), + (RATapi.models.Resolution, "constant", ["", "", "", "", ""]), + ], +) +def test_type_change_clear(model, type, values): """If the type of a background or resolution is changed, it should wipe the other fields and warn the user.""" model_instance = model( name="Test", - type="constant", + type=type, source="src", - value_1="val1", - value_2="val2", - value_3="val3", - value_4="val4", - value_5="val5", + value_1=values[0], + value_2=values[1], + value_3=values[2], + value_4=values[3], + value_5=values[4], ) with pytest.warns(UserWarning, match="Changing the type of Test clears its source and value fields."): model_instance.type = "data" for attr in ["source", "value_1", "value_2", "value_3", "value_4", "value_5"]: assert getattr(model_instance, attr) == "" + + +@pytest.mark.parametrize( + ["model", "signal_type", "values"], + [ + (RATapi.models.Background, "constant", ["value_1", "value_2", "value_3", "value_4", "value_5"]), + (RATapi.models.Background, "data", ["value_2", "value_3", "value_4", "value_5"]), + (RATapi.models.Resolution, "constant", ["value_1", "value_2", "value_3", "value_4", "value_5"]), + (RATapi.models.Resolution, "data", ["value_1", "value_2", "value_3", "value_4", "value_5"]), + ], +) +def test_unsupported_parameters_error(model, signal_type, values): + """If a value is inputted for an unsupported field for a particular type of background or resolution then we should + raise an error.""" + for value in values: + with pytest.raises( + pydantic.ValidationError, + match=( + f"1 validation error for {model.__name__}\n Value error, The following values are not supported" + f' by the "{signal_type}" {model.__name__} type: {value}' + ), + ): + model(**{"type": signal_type, value: "unsupported"}) diff --git a/tests/test_project.py b/tests/test_project.py index eb2b2d8f..82fc6388 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -1137,17 +1137,7 @@ def test_write_script_wrong_extension(test_project, extension: str) -> None: ["class_list", "model_type", "field"], [ ("backgrounds", "constant", "source"), - ("backgrounds", "", "value_1"), - ("backgrounds", "", "value_2"), - ("backgrounds", "", "value_3"), - ("backgrounds", "", "value_4"), - ("backgrounds", "", "value_5"), ("resolutions", "constant", "source"), - ("resolutions", "", "value_1"), - ("resolutions", "", "value_2"), - ("resolutions", "", "value_3"), - ("resolutions", "", "value_4"), - ("resolutions", "", "value_5"), ("layers", "", "thickness"), ("layers", "", "SLD"), ("layers", "", "roughness"), @@ -1216,17 +1206,7 @@ def test_wrap_del(test_project, class_list: str, parameter: str, field: str) -> ["class_list", "model_type", "field", "model_params"], [ ("backgrounds", "constant", "source", {}), - ("backgrounds", "", "value_1", {}), - ("backgrounds", "", "value_2", {}), - ("backgrounds", "", "value_3", {}), - ("backgrounds", "", "value_4", {}), - ("backgrounds", "", "value_5", {}), ("resolutions", "constant", "source", {}), - ("resolutions", "", "value_1", {}), - ("resolutions", "", "value_2", {}), - ("resolutions", "", "value_3", {}), - ("resolutions", "", "value_4", {}), - ("resolutions", "", "value_5", {}), ("layers", "", "thickness", layer_params), ("layers", "", "SLD", layer_params), ("layers", "", "roughness", layer_params), @@ -1263,17 +1243,7 @@ def test_wrap_iadd(test_project, class_list: str, model_type: str, field: str, m ["class_list", "model_type", "field", "model_params"], [ ("backgrounds", "constant", "source", {}), - ("backgrounds", "", "value_1", {}), - ("backgrounds", "", "value_2", {}), - ("backgrounds", "", "value_3", {}), - ("backgrounds", "", "value_4", {}), - ("backgrounds", "", "value_5", {}), ("resolutions", "constant", "source", {}), - ("resolutions", "", "value_1", {}), - ("resolutions", "", "value_2", {}), - ("resolutions", "", "value_3", {}), - ("resolutions", "", "value_4", {}), - ("resolutions", "", "value_5", {}), ("layers", "", "thickness", layer_params), ("layers", "", "SLD", layer_params), ("layers", "", "roughness", layer_params), @@ -1311,17 +1281,7 @@ def test_wrap_append(test_project, class_list: str, model_type: str, field: str, ["class_list", "model_type", "field", "model_params"], [ ("backgrounds", "constant", "source", {}), - ("backgrounds", "", "value_1", {}), - ("backgrounds", "", "value_2", {}), - ("backgrounds", "", "value_3", {}), - ("backgrounds", "", "value_4", {}), - ("backgrounds", "", "value_5", {}), ("resolutions", "constant", "source", {}), - ("resolutions", "", "value_1", {}), - ("resolutions", "", "value_2", {}), - ("resolutions", "", "value_3", {}), - ("resolutions", "", "value_4", {}), - ("resolutions", "", "value_5", {}), ("layers", "", "thickness", layer_params), ("layers", "", "SLD", layer_params), ("layers", "", "roughness", layer_params), @@ -1492,17 +1452,7 @@ def test_wrap_clear(test_project, class_list: str, parameter: str, field: str) - ["class_list", "model_type", "field", "model_params"], [ ("backgrounds", "constant", "source", {}), - ("backgrounds", "", "value_1", {}), - ("backgrounds", "", "value_2", {}), - ("backgrounds", "", "value_3", {}), - ("backgrounds", "", "value_4", {}), - ("backgrounds", "", "value_5", {}), ("resolutions", "constant", "source", {}), - ("resolutions", "", "value_1", {}), - ("resolutions", "", "value_2", {}), - ("resolutions", "", "value_3", {}), - ("resolutions", "", "value_4", {}), - ("resolutions", "", "value_5", {}), ("layers", "", "thickness", layer_params), ("layers", "", "SLD", layer_params), ("layers", "", "roughness", layer_params), From d295afe3ecfd26dcc5aace2f91484f7df0ff2ae4 Mon Sep 17 00:00:00 2001 From: Paul Sharp <44529197+DrPaulSharp@users.noreply.github.com> Date: Thu, 17 Apr 2025 09:24:13 +0100 Subject: [PATCH 16/20] Updates submodule (#146) * Renames "BayesResults" as "OutputBayesResult" in "rat_core" * Updates submodule * Fixes matlab import error * Refactors matlab save in "project_to_r1" --- .../convert_rascal_project/convert_rascal.py | 6 +- RATapi/inputs.py | 2 +- RATapi/outputs.py | 4 +- RATapi/utils/convert.py | 10 +-- RATapi/utils/plotting.py | 6 +- cpp/RAT | 2 +- cpp/includes/defines.h | 6 +- cpp/rat.cpp | 64 +++++++++---------- tests/conftest.py | 2 +- tests/test_convert.py | 16 ++--- tests/test_inputs.py | 6 +- tests/test_plotting.py | 18 +++--- tests/test_run.py | 4 +- 13 files changed, 72 insertions(+), 74 deletions(-) diff --git a/RATapi/examples/convert_rascal_project/convert_rascal.py b/RATapi/examples/convert_rascal_project/convert_rascal.py index b0b945f7..2a41b61f 100644 --- a/RATapi/examples/convert_rascal_project/convert_rascal.py +++ b/RATapi/examples/convert_rascal_project/convert_rascal.py @@ -26,7 +26,7 @@ def convert_rascal(mat_filename="lipid_bilayer.mat"): """ project_path = pathlib.Path(__file__).parent / "R1monolayerVolumeModel.mat" - project = RAT.utils.convert.r1_to_project_class(project_path) + project = RAT.utils.convert.r1_to_project(project_path) # change values if you like, including ones not supported by R1 project.parameters["Head Thickness"].prior_type = "gaussian" @@ -35,10 +35,10 @@ def convert_rascal(mat_filename="lipid_bilayer.mat"): # convert DSPC standard layers example to a struct and save as file lipid_bilayer_project = RAT.examples.DSPC_standard_layers()[0] - RAT.utils.convert.project_class_to_r1(lipid_bilayer_project, filename=mat_filename) + RAT.utils.convert.project_to_r1(lipid_bilayer_project, filename=mat_filename) # convert and return as a Python dictionary - struct = RAT.utils.convert.project_class_to_r1(lipid_bilayer_project, return_struct=True) + struct = RAT.utils.convert.project_to_r1(lipid_bilayer_project, return_struct=True) return project, struct diff --git a/RATapi/inputs.py b/RATapi/inputs.py index 3798cb0f..06d9f82c 100644 --- a/RATapi/inputs.py +++ b/RATapi/inputs.py @@ -284,7 +284,7 @@ def make_problem(project: RATapi.Project) -> ProblemDefinition: problem.numberOfContrasts = len(project.contrasts) problem.geometry = project.geometry problem.useImaginary = project.absorption - problem.repeatLayers = [[0, 1]] * len(project.contrasts) # This is marked as "to do" in RAT + problem.repeatLayers = [1] * len(project.contrasts) problem.contrastBackgroundParams = contrast_background_params problem.contrastBackgroundTypes = contrast_background_types problem.contrastBackgroundActions = [contrast.background_action for contrast in project.contrasts] diff --git a/RATapi/outputs.py b/RATapi/outputs.py index 150ebc2d..925db7c5 100644 --- a/RATapi/outputs.py +++ b/RATapi/outputs.py @@ -409,7 +409,7 @@ class BayesResults(Results): def make_results( procedure: Procedures, output_results: RATapi.rat_core.OutputResult, - bayes_results: Optional[RATapi.rat_core.BayesResults] = None, + bayes_results: Optional[RATapi.rat_core.OutputBayesResult] = None, ) -> Union[Results, BayesResults]: """Initialise a python Results or BayesResults object using the outputs from a RAT calculation. @@ -419,7 +419,7 @@ def make_results( The procedure used by the calculation. output_results : RATapi.rat_core.OutputResult The C++ output results from the calculation. - bayes_results : Optional[RATapi.rat_core.BayesResults] + bayes_results : Optional[RATapi.rat_core.OutputBayesResult] The optional extra C++ Bayesian output results from a Bayesian calculation. Returns diff --git a/RATapi/utils/convert.py b/RATapi/utils/convert.py index 35bc216b..7c94ab50 100644 --- a/RATapi/utils/convert.py +++ b/RATapi/utils/convert.py @@ -15,7 +15,7 @@ from RATapi.utils.enums import Geometries, Languages, LayerModels -def r1_to_project_class(filename: Union[str, PathLike]) -> Project: +def r1_to_project(filename: Union[str, PathLike]) -> Project: """Read a RasCAL1 project struct as a Python `Project`. Parameters @@ -318,7 +318,7 @@ def fix_invalid_constraints(name: str, constrs: tuple[float, float], value: floa return project -def project_class_to_r1( +def project_to_r1( project: Project, filename: Union[str, PathLike] = "RAT_project", return_struct: bool = False ) -> Union[dict, None]: """Convert a RAT Project to a RasCAL1 project struct. @@ -549,10 +549,10 @@ def convert_parameters( # scipy.io.savemat doesn't do cells properly: # https://github.com/scipy/scipy/issues/3756 # rather than fiddling we just use matlab - eng = wrappers.start_matlab().result() - if eng is None: + loader = wrappers.MatlabWrapper.loader + if loader is None: raise ImportError("matlabengine is not installed.") + eng = loader.result() eng.workspace["problem"] = r1 eng.save(str(filename), "problem", nargout=0) - eng.exit() return None diff --git a/RATapi/utils/plotting.py b/RATapi/utils/plotting.py index 19596198..f9abcee5 100644 --- a/RATapi/utils/plotting.py +++ b/RATapi/utils/plotting.py @@ -17,7 +17,7 @@ import RATapi import RATapi.inputs import RATapi.outputs -from RATapi.rat_core import PlotEventData, makeSLDProfileXY +from RATapi.rat_core import PlotEventData, makeSLDProfile def plot_errorbars(ax: Axes, x: np.ndarray, y: np.ndarray, err: np.ndarray, one_sided: bool, color: str): @@ -154,11 +154,11 @@ def plot_ref_sld_helper( layer = data.resampledLayers[i][j] if layers.shape[1] == 4: layer = np.delete(layer, 2, 1) - new_profile = makeSLDProfileXY( + new_profile = makeSLDProfile( layers[0, 1], # Bulk In layers[-1, 1], # Bulk Out - data.subRoughs[i], # roughness layer, + data.subRoughs[i], # roughness 1, ) diff --git a/cpp/RAT b/cpp/RAT index df626113..589c9871 160000 --- a/cpp/RAT +++ b/cpp/RAT @@ -1 +1 @@ -Subproject commit df626113d7a930cfa23c1ab47fdf9b1111b52123 +Subproject commit 589c987180f33f1e265537044a6497ef51f4ef09 diff --git a/cpp/includes/defines.h b/cpp/includes/defines.h index 67b0fda7..02b5fa97 100644 --- a/cpp/includes/defines.h +++ b/cpp/includes/defines.h @@ -262,12 +262,12 @@ struct DreamOutput py::array_t CR; }; -const std::string docsBayesResults = R"(The Python binding for the C++ bayesResults struct. +const std::string docsOutputBayesResult = R"(The Python binding for the C++ bayesResults struct. The results of a Bayesian RAT calculation. Parameters ---------- -predictionIntervals : RATapi.rat_core.orePredictionIntervals +predictionIntervals : RATapi.rat_core.PredictionIntervals The prediction intervals. confidenceIntervals : RATapi.rat_core.ConfidenceIntervals The 65% and 95% confidence intervals for the best fit results. @@ -282,7 +282,7 @@ chain : np.ndarray The ``i``'th column of the array contains the chain for parameter ``fitNames[i]``. )"; -struct BayesResults +struct OutputBayesResult { PredictionIntervals predictionIntervals; ConfidenceIntervals confidenceIntervals; diff --git a/cpp/rat.cpp b/cpp/rat.cpp index a92dc49d..3335e09a 100644 --- a/cpp/rat.cpp +++ b/cpp/rat.cpp @@ -16,7 +16,7 @@ setup_pybind11(cfg) #include "RAT/RATMain_initialize.h" #include "RAT/RATMain_terminate.h" #include "RAT/RATMain_types.h" -#include "RAT/makeSLDProfileXY.h" +#include "RAT/makeSLDProfile.h" #include "RAT/dylib.hpp" #include "RAT/events/eventManager.h" #include "includes/defines.h" @@ -263,7 +263,7 @@ RAT::b_ProblemDefinition createProblemDefinitionStruct(const ProblemDefinition& problem_struct.numberOfContrasts = problem.numberOfContrasts; stringToRatBoundedArray(problem.geometry, problem_struct.geometry.data, problem_struct.geometry.size); problem_struct.useImaginary = problem.useImaginary; - problem_struct.repeatLayers = customCaller("Problem.repeatLayers", pyListToRatCellWrap2, problem.repeatLayers); + problem_struct.repeatLayers = customCaller("Problem.repeatLayers", pyArrayToRatRowArray1d, problem.repeatLayers); problem_struct.contrastBackgroundParams = customCaller("Problem.contrastBackgroundParams", pyListToRatCellWrap3, problem.contrastBackgroundParams); problem_struct.contrastBackgroundTypes = customCaller("Problem.contrastBackgroundTypes", pyListToRatCellWrap02d, problem.contrastBackgroundTypes); problem_struct.contrastBackgroundActions = customCaller("Problem.contrastBackgroundActions", pyListToRatCellWrap02d, problem.contrastBackgroundActions); @@ -460,7 +460,7 @@ ProblemDefinition problemDefinitionFromStruct(const RAT::b_ProblemDefinition pro problem_def.numberOfContrasts = problem.numberOfContrasts; stringFromRatBoundedArray(problem.geometry.data, problem.geometry.size, problem_def.geometry); problem_def.useImaginary = problem.useImaginary; - problem_def.repeatLayers = pyListFromRatCellWrap2(problem.repeatLayers); + problem_def.repeatLayers = pyArrayFromRatArray1d>(problem.repeatLayers); problem_def.contrastBackgroundParams = pyListFromBoundedCellWrap>(problem.contrastBackgroundParams); problem_def.contrastBackgroundTypes = pyListFromRatCellWrap02d(problem.contrastBackgroundTypes); problem_def.contrastBackgroundActions = pyListFromRatCellWrap02d(problem.contrastBackgroundActions); @@ -510,9 +510,9 @@ ProblemDefinition problemDefinitionFromStruct(const RAT::b_ProblemDefinition pro return problem_def; } -BayesResults bayesResultsFromStruct(const RAT::BayesResults results) +OutputBayesResult OutputBayesResultsFromStruct(const RAT::BayesResults results) { - BayesResults bayesResults; + OutputBayesResult bayesResults; bayesResults.chain = pyArrayFromRatArray2d(results.chain); @@ -575,7 +575,7 @@ out_problem_def : Rat.rat_core.ProblemDefinition The project input with the updated fit values. results : Rat.rat_core.OutputResult The results from a RAT calculation. -bayes_result : Rat.rat_core.BayesResults +bayes_result : Rat.rat_core.OutputBayesResult The extra results if RAT calculation is Bayesian. )"; @@ -593,10 +593,10 @@ py::tuple RATMain(const ProblemDefinition& problem_def, const Control& control) out_problem_def.customFiles = problem_def.customFiles.attr("copy")(); return py::make_tuple(out_problem_def, OutputResultFromStruct(results), - bayesResultsFromStruct(bayesResults)); + OutputBayesResultsFromStruct(bayesResults)); } -const std::string docsMakeSLDProfileXY = R"(Creates the profiles for the SLD plots +const std::string docsMakeSLDProfile = R"(Creates the profiles for the SLD plots Parameters ---------- @@ -604,10 +604,10 @@ bulk_in : float Bulk in value for contrast. bulk_out : float Bulk out value for contrast. -ssub : float - Substrate roughness. layers : np.ndarray[np.float] Array of parameters for each layer in the contrast. +ssub : float + Substrate roughness. number_of_repeats : int, default: 1 Number of times the layers are repeated. @@ -617,22 +617,20 @@ sld_profile : np.ndarray[np.float] Computed SLD profile )"; -py::array_t makeSLDProfileXY(real_T bulk_in, - real_T bulk_out, - real_T ssub, - const py::array_t &layers, - int number_of_repeats=DEFAULT_NREPEATS) +py::array_t makeSLDProfile(real_T bulk_in, + real_T bulk_out, + const py::array_t &layers, + real_T ssub, + int number_of_repeats=DEFAULT_NREPEATS) { coder::array out; coder::array layers_array = pyArrayToRatArray2d(layers); - py::buffer_info buffer_info = layers.request(); - RAT::makeSLDProfileXY(bulk_in, - bulk_out, - ssub, - layers_array, - buffer_info.shape[0], - number_of_repeats, - out); + RAT::makeSLDProfile(bulk_in, + bulk_out, + layers_array, + ssub, + number_of_repeats, + out); return pyArrayFromRatArray2d(out); @@ -787,14 +785,14 @@ PYBIND11_MODULE(rat_core, m) { .def_readwrite("R_stat", &DreamOutput::R_stat) .def_readwrite("CR", &DreamOutput::CR); - py::class_(m, "BayesResults", docsBayesResults.c_str()) + py::class_(m, "OutputBayesResult", docsOutputBayesResult.c_str()) .def(py::init<>()) - .def_readwrite("predictionIntervals", &BayesResults::predictionIntervals) - .def_readwrite("confidenceIntervals", &BayesResults::confidenceIntervals) - .def_readwrite("dreamParams", &BayesResults::dreamParams) - .def_readwrite("dreamOutput", &BayesResults::dreamOutput) - .def_readwrite("nestedSamplerOutput", &BayesResults::nestedSamplerOutput) - .def_readwrite("chain", &BayesResults::chain); + .def_readwrite("predictionIntervals", &OutputBayesResult::predictionIntervals) + .def_readwrite("confidenceIntervals", &OutputBayesResult::confidenceIntervals) + .def_readwrite("dreamParams", &OutputBayesResult::dreamParams) + .def_readwrite("dreamOutput", &OutputBayesResult::dreamOutput) + .def_readwrite("nestedSamplerOutput", &OutputBayesResult::nestedSamplerOutput) + .def_readwrite("chain", &OutputBayesResult::chain); py::class_(m, "Calculation", docsCalculation.c_str()) .def(py::init<>()) @@ -1047,7 +1045,7 @@ PYBIND11_MODULE(rat_core, m) { p.numberOfContrasts = t[6].cast(); p.geometry = t[7].cast(); p.useImaginary = t[8].cast(); - p.repeatLayers = t[9].cast(); + p.repeatLayers = t[9].cast>(); p.contrastBackgroundParams = t[10].cast(); p.contrastBackgroundTypes = t[11].cast(); p.contrastBackgroundActions = t[12].cast(); @@ -1099,6 +1097,6 @@ PYBIND11_MODULE(rat_core, m) { m.def("RATMain", &RATMain, docsRATMain.c_str(), py::arg("problem_def"), py::arg("control")); - m.def("makeSLDProfileXY", &makeSLDProfileXY, docsMakeSLDProfileXY.c_str(), - py::arg("bulk_in"), py::arg("bulk_out"), py::arg("ssub"), py::arg("layers"), py::arg("number_of_repeats") = DEFAULT_NREPEATS); + m.def("makeSLDProfile", &makeSLDProfile, docsMakeSLDProfile.c_str(), + py::arg("bulk_in"), py::arg("bulk_out"), py::arg("layers"), py::arg("ssub"), py::arg("number_of_repeats") = DEFAULT_NREPEATS); } diff --git a/tests/conftest.py b/tests/conftest.py index 50fbc11c..c109c92d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2253,7 +2253,7 @@ def dream_bayes(): This optimisation used the parameters: nSamples=1, nChains=1. """ - bayes = RATapi.rat_core.BayesResults() + bayes = RATapi.rat_core.OutputBayesResult() bayes.predictionIntervals = RATapi.rat_core.PredictionIntervals() bayes.predictionIntervals.reflectivity = [ np.array( diff --git a/tests/test_convert.py b/tests/test_convert.py index e636fc9a..8dc3ecdb 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -8,7 +8,7 @@ import pytest import RATapi -from RATapi.utils.convert import project_class_to_r1, r1_to_project_class +from RATapi.utils.convert import project_to_r1, r1_to_project TEST_DIR_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data") @@ -58,9 +58,9 @@ def dspc_bilayer(): ], ) @pytest.mark.parametrize("path_type", [os.path.join, pathlib.Path]) -def test_r1_to_project_class(file, project, path_type, request): +def test_r1_to_project(file, project, path_type, request): """Test that R1 to Project class conversion returns the expected Project.""" - output_project = r1_to_project_class(path_type(TEST_DIR_PATH, file)) + output_project = r1_to_project(path_type(TEST_DIR_PATH, file)) expected_project = request.getfixturevalue(project) # assert statements have to be more careful due to R1 missing features @@ -83,7 +83,7 @@ def test_r1_to_project_class(file, project, path_type, request): def test_r1_involution(project, request, monkeypatch): """Test that converting a Project to an R1 struct and back returns the same project.""" original_project = request.getfixturevalue(project) - r1_struct = project_class_to_r1(original_project, return_struct=True) + r1_struct = project_to_r1(original_project, return_struct=True) # rather than writing the struct to a file and reading the file, just directly # hand the struct over @@ -93,7 +93,7 @@ def mock_load(ignored_filename, **ignored_settings): monkeypatch.setattr("RATapi.utils.convert.loadmat", mock_load, raising=True) - converted_project = r1_to_project_class(project) + converted_project = r1_to_project(project) for class_list in RATapi.project.class_lists: assert getattr(converted_project, class_list) == getattr(original_project, class_list) @@ -105,7 +105,7 @@ def test_invalid_constraints(): match=r"The parameter (.+) has invalid constraints," " these have been adjusted to satisfy the current value of the parameter." ): - output_project = r1_to_project_class(pathlib.Path(TEST_DIR_PATH, "R1DoubleBilayerVolumeModel.mat")) + output_project = r1_to_project(pathlib.Path(TEST_DIR_PATH, "R1DoubleBilayerVolumeModel.mat")) assert output_project.background_parameters[0].min == output_project.background_parameters[0].value @@ -117,7 +117,7 @@ def test_matlab_save(path_type, request): project = request.getfixturevalue("r1_default_project") with tempfile.TemporaryDirectory() as temp: matfile = path_type(temp, "testfile.mat") - project_class_to_r1(project, filename=matfile) - converted_project = r1_to_project_class(matfile) + project_to_r1(project, filename=matfile) + converted_project = r1_to_project(matfile) assert project == converted_project diff --git a/tests/test_inputs.py b/tests/test_inputs.py index 2874a888..e7efd7b4 100644 --- a/tests/test_inputs.py +++ b/tests/test_inputs.py @@ -174,7 +174,7 @@ def standard_layers_problem(test_names, test_checks): problem.simulationLimits = [[1.0, 1.0]] problem.numberOfContrasts = 1 problem.numberOfLayers = 1 - problem.repeatLayers = [[0, 1]] + problem.repeatLayers = [1] problem.layersDetails = [[2, 3, 4, float("NaN"), 2]] problem.contrastLayers = [[1]] problem.numberOfDomainContrasts = 0 @@ -242,7 +242,7 @@ def domains_problem(test_names, test_checks): problem.simulationLimits = [[1.0, 1.0]] problem.numberOfContrasts = 1 problem.numberOfLayers = 1 - problem.repeatLayers = [[0, 1]] + problem.repeatLayers = [1] problem.layersDetails = [[2, 3, 4, float("NaN"), 2]] problem.contrastLayers = [[2, 1]] problem.numberOfDomainContrasts = 2 @@ -311,7 +311,7 @@ def custom_xy_problem(test_names, test_checks): problem.data = [np.empty([0, 6])] problem.dataLimits = [[0.0, 0.0]] problem.simulationLimits = [[0.005, 0.7]] - problem.repeatLayers = [[0, 1]] + problem.repeatLayers = [1] problem.layersDetails = [] problem.contrastLayers = [[]] problem.numberOfContrasts = 1 diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 78ddd08c..d907ba16 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -140,9 +140,9 @@ def test_ref_sld_bayes(fig, bayes_fig, bayes): assert any(isinstance(comp, PolyCollection) for comp in components) -@patch("RATapi.utils.plotting.makeSLDProfileXY") +@patch("RATapi.utils.plotting.makeSLDProfile") def test_sld_profile_function_call(mock: MagicMock) -> None: - """Tests the makeSLDProfileXY function called with + """Tests the makeSLDProfile function called with correct args. """ RATplot.plot_ref_sld_helper(data()) @@ -150,21 +150,21 @@ def test_sld_profile_function_call(mock: MagicMock) -> None: assert mock.call_count == 3 assert mock.call_args_list[0].args[0] == 2.07e-06 assert mock.call_args_list[0].args[1] == 6.28e-06 - assert mock.call_args_list[0].args[2] == 0.0 + assert mock.call_args_list[0].args[3] == 0.0 assert mock.call_args_list[0].args[4] == 1 assert mock.call_args_list[1].args[0] == 2.07e-06 assert mock.call_args_list[1].args[1] == 1.83e-06 - assert mock.call_args_list[1].args[2] == 0.0 + assert mock.call_args_list[1].args[3] == 0.0 assert mock.call_args_list[1].args[4] == 1 assert mock.call_args_list[2].args[0] == 2.07e-06 assert mock.call_args_list[2].args[1] == -5.87e-07 - assert mock.call_args_list[2].args[2] == 0.0 + assert mock.call_args_list[2].args[3] == 0.0 assert mock.call_args_list[2].args[4] == 1 -@patch("RATapi.utils.plotting.makeSLDProfileXY") +@patch("RATapi.utils.plotting.makeSLDProfile") def test_live_plot(mock: MagicMock) -> None: plot_data = data() @@ -177,17 +177,17 @@ def test_live_plot(mock: MagicMock) -> None: assert mock.call_count == 3 assert mock.call_args_list[0].args[0] == 2.07e-06 assert mock.call_args_list[0].args[1] == 6.28e-06 - assert mock.call_args_list[0].args[2] == 0.0 + assert mock.call_args_list[0].args[3] == 0.0 assert mock.call_args_list[0].args[4] == 1 assert mock.call_args_list[1].args[0] == 2.07e-06 assert mock.call_args_list[1].args[1] == 1.83e-06 - assert mock.call_args_list[1].args[2] == 0.0 + assert mock.call_args_list[1].args[3] == 0.0 assert mock.call_args_list[1].args[4] == 1 assert mock.call_args_list[2].args[0] == 2.07e-06 assert mock.call_args_list[2].args[1] == -5.87e-07 - assert mock.call_args_list[2].args[2] == 0.0 + assert mock.call_args_list[2].args[3] == 0.0 assert mock.call_args_list[2].args[4] == 1 diff --git a/tests/test_run.py b/tests/test_run.py index f488de4a..a3242d45 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -76,7 +76,7 @@ def reflectivity_calculation_problem(): problem.simulationLimits = [[0.011403, 0.59342], [0.011403, 0.59342]] problem.numberOfContrasts = 2.0 problem.numberOfLayers = 6.0 - problem.repeatLayers = [[0.0, 1.0], [0.0, 1.0]] + problem.repeatLayers = [1.0, 1.0] problem.layersDetails = [ np.array([2.0]), np.array([4.0]), @@ -237,7 +237,7 @@ def dream_problem(): problem.simulationLimits = [[0.011403, 0.59342], [0.011403, 0.59342]] problem.numberOfContrasts = 2.0 problem.numberOfLayers = 6.0 - problem.repeatLayers = [[0.0, 1.0], [0.0, 1.0]] + problem.repeatLayers = [1.0, 1.0] problem.layersDetails = [ np.array([2.0]), np.array([4.0]), From 113729f61dfa376922317fb1d417448a1b81f35c Mon Sep 17 00:00:00 2001 From: Paul Sharp <44529197+DrPaulSharp@users.noreply.github.com> Date: Thu, 17 Apr 2025 16:32:52 +0100 Subject: [PATCH 17/20] Updates jupyter notebook (#148) --- .../convert_rascal_project/convert_rascal.ipynb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/RATapi/examples/convert_rascal_project/convert_rascal.ipynb b/RATapi/examples/convert_rascal_project/convert_rascal.ipynb index c62be937..fbae2d5b 100644 --- a/RATapi/examples/convert_rascal_project/convert_rascal.ipynb +++ b/RATapi/examples/convert_rascal_project/convert_rascal.ipynb @@ -6,7 +6,7 @@ "source": [ "### RasCAL-1 to RAT\n", "\n", - "RasCAL-1 (R1) project structs can be converted to RAT Project classes, and vice versa. This is done via the functions `r1_to_project_class` and `project_class_to_r1`.\n", + "RasCAL-1 (R1) project structs can be converted to RAT Projects, and vice versa. This is done via the functions `r1_to_project` and `project_to_r1`.\n", "\n", "Converting from R1 to a `Project` is very simple. We use the example R1 project in the file `R1monolayerVolumeModel.mat`, which is a project for analysing a monolayer of DSPC with various deuterations (tail-deuterated, head-deuterated, fully deuterated, hydrogenated)" ] @@ -15,7 +15,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Simply give the file path to the function `r1_to_project_class`, and it returns a RAT `Project` that you can use exactly like any other." + "Simply give the file path to the function `r1_to_project`, and it returns a RAT `Project` that you can use exactly like any other." ] }, { @@ -24,9 +24,9 @@ "metadata": {}, "outputs": [], "source": [ - "from RATapi.utils.convert import r1_to_project_class\n", + "from RATapi.utils.convert import r1_to_project\n", "\n", - "project = r1_to_project_class(\"R1monolayerVolumeModel.mat\")\n", + "project = r1_to_project(\"R1monolayerVolumeModel.mat\")\n", "print(project)" ] }, @@ -108,7 +108,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "`project_class_to_r1` takes parameters `project` and `filename`, which are the `Project` object and filename for the produced .mat file respectively. This .mat file can then be loaded into RasCAL-1.\n", + "`project_to_r1` takes parameters `project` and `filename`, which are the `Project` object and filename for the produced .mat file respectively. This .mat file can then be loaded into RasCAL-1.\n", "\n", "Alternatively, if one sets `return_struct=True`, the struct is returned as a Python dictionary instead of being saved.\n", "\n", @@ -121,14 +121,14 @@ "metadata": {}, "outputs": [], "source": [ - "from RATapi.utils.convert import project_class_to_r1\n", + "from RATapi.utils.convert import project_to_r1\n", "from pprint import pp # for printing the struct\n", "\n", "# save to a file called lipid_bilayer.mat\n", - "project_class_to_r1(lipid_bilayer_project, filename=\"lipid_bilayer.mat\")\n", + "project_to_r1(lipid_bilayer_project, filename=\"lipid_bilayer.mat\")\n", "\n", "# return as a Python dictionary\n", - "struct = project_class_to_r1(lipid_bilayer_project, return_struct=True)\n", + "struct = project_to_r1(lipid_bilayer_project, return_struct=True)\n", "pp(struct)" ] } From 89e5fdd36eacc8033e1e6c3866be484efcea9944 Mon Sep 17 00:00:00 2001 From: Paul Sharp <44529197+DrPaulSharp@users.noreply.github.com> Date: Wed, 23 Apr 2025 17:21:41 +0100 Subject: [PATCH 18/20] Improves error message for cross check validation (#147) * Updates custom error messages * Splits error message into two for cross check validation * Tidies up tests and improves coverage * Reformats error messages * Improves error message for contrast model --- RATapi/controls.py | 2 +- RATapi/project.py | 121 +++++++++++++----- tests/test_project.py | 286 ++++++++++++++++++++++++++++-------------- 3 files changed, 280 insertions(+), 129 deletions(-) diff --git a/RATapi/controls.py b/RATapi/controls.py index 9a36fdee..d696fb57 100644 --- a/RATapi/controls.py +++ b/RATapi/controls.py @@ -155,7 +155,7 @@ def warn_setting_incorrect_properties(self, handler: ValidatorFunctionWrapHandle f" controls procedure are:\n " f"{', '.join(fields.get('procedure', []))}\n", } - custom_error_list = custom_pydantic_validation_error(exc.errors(), custom_error_msgs) + custom_error_list = custom_pydantic_validation_error(exc.errors(include_url=False), custom_error_msgs) raise ValidationError.from_exception_data(exc.title, custom_error_list, hide_input=True) from None if isinstance(model_input, validated_self.__class__): diff --git a/RATapi/project.py b/RATapi/project.py index 03f31e5e..ae2ed2cc 100644 --- a/RATapi/project.py +++ b/RATapi/project.py @@ -557,7 +557,6 @@ def update_renamed_models(self) -> "Project": for index, param in all_matches: if param in params: setattr(project_field[index], param, new_name) - self._all_names = self.get_all_names() return self @model_validator(mode="after") @@ -566,28 +565,45 @@ def cross_check_model_values(self) -> "Project": values = ["value_1", "value_2", "value_3", "value_4", "value_5"] for field in ["backgrounds", "resolutions"]: self.check_allowed_source(field) - self.check_allowed_values(field, values, getattr(self, f"{field[:-1]}_parameters").get_names()) + self.check_allowed_values( + field, + values, + getattr(self, f"{field[:-1]}_parameters").get_names(), + self._all_names[f"{field[:-1]}_parameters"], + ) self.check_allowed_values( "layers", ["thickness", "SLD", "SLD_real", "SLD_imaginary", "roughness"], self.parameters.get_names(), + self._all_names["parameters"], ) - self.check_allowed_values("contrasts", ["data"], self.data.get_names()) - self.check_allowed_values("contrasts", ["background"], self.backgrounds.get_names()) - self.check_allowed_values("contrasts", ["bulk_in"], self.bulk_in.get_names()) - self.check_allowed_values("contrasts", ["bulk_out"], self.bulk_out.get_names()) - self.check_allowed_values("contrasts", ["scalefactor"], self.scalefactors.get_names()) - self.check_allowed_values("contrasts", ["resolution"], self.resolutions.get_names()) - self.check_allowed_values("contrasts", ["domain_ratio"], self.domain_ratios.get_names()) + self.check_allowed_values("contrasts", ["data"], self.data.get_names(), self._all_names["data"]) + self.check_allowed_values( + "contrasts", ["background"], self.backgrounds.get_names(), self._all_names["backgrounds"] + ) + self.check_allowed_values("contrasts", ["bulk_in"], self.bulk_in.get_names(), self._all_names["bulk_in"]) + self.check_allowed_values("contrasts", ["bulk_out"], self.bulk_out.get_names(), self._all_names["bulk_out"]) + self.check_allowed_values( + "contrasts", ["scalefactor"], self.scalefactors.get_names(), self._all_names["scalefactors"] + ) + self.check_allowed_values( + "contrasts", ["resolution"], self.resolutions.get_names(), self._all_names["resolutions"] + ) + self.check_allowed_values( + "contrasts", ["domain_ratio"], self.domain_ratios.get_names(), self._all_names["domain_ratios"] + ) self.check_contrast_model_allowed_values( "contrasts", getattr(self, self._contrast_model_field).get_names(), + self._all_names[self._contrast_model_field], self._contrast_model_field, ) - self.check_contrast_model_allowed_values("domain_contrasts", self.layers.get_names(), "layers") + self.check_contrast_model_allowed_values( + "domain_contrasts", self.layers.get_names(), self._all_names["layers"], "layers" + ) return self @model_validator(mode="after") @@ -606,6 +622,12 @@ def check_protected_parameters(self) -> "Project": self._protected_parameters = self.get_all_protected_parameters() return self + @model_validator(mode="after") + def update_names(self) -> "Project": + """Following validation, update the list of all parameter names.""" + self._all_names = self.get_all_names() + return self + def __str__(self): output = "" for key, value in self.__dict__.items(): @@ -630,7 +652,9 @@ def get_all_protected_parameters(self): for class_list in parameter_class_lists } - def check_allowed_values(self, attribute: str, field_list: list[str], allowed_values: list[str]) -> None: + def check_allowed_values( + self, attribute: str, field_list: list[str], allowed_values: list[str], previous_values: list[str] + ) -> None: """Check the values of the given fields in the given model are in the supplied list of allowed values. Parameters @@ -641,6 +665,8 @@ def check_allowed_values(self, attribute: str, field_list: list[str], allowed_va The fields of the attribute to be checked for valid values. allowed_values : list [str] The list of allowed values for the fields given in field_list. + previous_values : list [str] + The list of allowed values for the fields given in field_list after the previous validation. Raises ------ @@ -649,14 +675,22 @@ def check_allowed_values(self, attribute: str, field_list: list[str], allowed_va """ class_list = getattr(self, attribute) - for model in class_list: + for index, model in enumerate(class_list): for field in field_list: value = getattr(model, field, "") if value and value not in allowed_values: - raise ValueError( - f'The value "{value}" in the "{field}" field of "{attribute}" must be defined in ' - f'"{values_defined_in[f"{attribute}.{field}"]}".', - ) + if value in previous_values: + raise ValueError( + f'The value "{value}" used in the "{field}" field of {attribute}[{index}] must be defined ' + f'in "{values_defined_in[f"{attribute}.{field}"]}". Please remove "{value}" from ' + f'"{attribute}[{index}].{field}" before attempting to delete it.', + ) + else: + raise ValueError( + f'The value "{value}" used in the "{field}" field of {attribute}[{index}] must be defined ' + f'in "{values_defined_in[f"{attribute}.{field}"]}". Please add "{value}" to ' + f'"{values_defined_in[f"{attribute}.{field}"]}" before including it in "{attribute}".', + ) def check_allowed_source(self, attribute: str) -> None: """Check that the source of a background or resolution is defined in the relevant field for its type. @@ -679,24 +713,37 @@ def check_allowed_source(self, attribute: str) -> None: """ class_list = getattr(self, attribute) - for model in class_list: + for index, model in enumerate(class_list): if model.type == TypeOptions.Constant: allowed_values = getattr(self, f"{attribute[:-1]}_parameters").get_names() + previous_values = self._all_names[f"{attribute[:-1]}_parameters"] elif model.type == TypeOptions.Data: allowed_values = self.data.get_names() + previous_values = self._all_names["data"] else: allowed_values = self.custom_files.get_names() + previous_values = self._all_names["custom_files"] if (value := model.source) != "" and value not in allowed_values: - raise ValueError( - f'The value "{value}" in the "source" field of "{attribute}" must be defined in ' - f'"{values_defined_in[f"{attribute}.{model.type}.source"]}".', - ) + if value in previous_values: + raise ValueError( + f'The value "{value}" used in the "source" field of {attribute}[{index}] must be defined in ' + f'"{values_defined_in[f"{attribute}.{model.type}.source"]}". Please remove "{value}" from ' + f'"{attribute}[{index}].source" before attempting to delete it.', + ) + else: + raise ValueError( + f'The value "{value}" used in the "source" field of {attribute}[{index}] must be defined in ' + f'"{values_defined_in[f"{attribute}.{model.type}.source"]}". Please add "{value}" to ' + f'"{values_defined_in[f"{attribute}.{model.type}.source"]}" before including it in ' + f'"{attribute}".', + ) def check_contrast_model_allowed_values( self, contrast_attribute: str, allowed_values: list[str], + previous_values: list[str], allowed_field: str, ) -> None: """Ensure the contents of the ``model`` for a contrast or domain contrast exist in the required project fields. @@ -707,6 +754,8 @@ def check_contrast_model_allowed_values( The specific contrast attribute of Project being validated (either "contrasts" or "domain_contrasts"). allowed_values : list [str] The list of allowed values for the model of the contrast_attribute. + previous_values : list [str] + The list of allowed values for the model of the contrast_attribute after the previous validation. allowed_field : str The name of the field in the project in which the allowed_values are defined. @@ -717,13 +766,22 @@ def check_contrast_model_allowed_values( """ class_list = getattr(self, contrast_attribute) - for contrast in class_list: - model_values = contrast.model - if model_values and not all(value in allowed_values for value in model_values): - raise ValueError( - f'The values: "{", ".join(str(i) for i in model_values)}" in the "model" field of ' - f'"{contrast_attribute}" must be defined in "{allowed_field}".', - ) + for index, contrast in enumerate(class_list): + if (model_values := contrast.model) and (missing_values := list(set(model_values) - set(allowed_values))): + if all(value in previous_values for value in model_values): + raise ValueError( + f"The value{'s' if len(missing_values) > 1 else ''}: " + f'"{", ".join(str(i) for i in missing_values)}" used in the "model" field of ' + f'{contrast_attribute}[{index}] must be defined in "{allowed_field}". Please remove all ' + f'unnecessary values from "model" before attempting to delete them.', + ) + else: + raise ValueError( + f"The value{'s' if len(missing_values) > 1 else ''}: " + f'"{", ".join(str(i) for i in missing_values)}" used in the "model" field of ' + f'{contrast_attribute}[{index}] must be defined in "{allowed_field}". Please add all ' + f'required values to "{allowed_field}" before including them in "{contrast_attribute}".', + ) def get_contrast_model_field(self): """Get the field used to define the contents of the "model" field in contrasts. @@ -945,7 +1003,7 @@ def wrapped_func(*args, **kwargs): Project.model_validate(self) except ValidationError as exc: class_list.data = previous_state - custom_error_list = custom_pydantic_validation_error(exc.errors()) + custom_error_list = custom_pydantic_validation_error(exc.errors(include_url=False)) raise ValidationError.from_exception_data(exc.title, custom_error_list, hide_input=True) from None except (TypeError, ValueError): class_list.data = previous_state @@ -980,9 +1038,8 @@ def try_relative_to(path: Path, relative_to: Path) -> str: else: warnings.warn( "Could not save custom file path as relative to the project directory, " - "which means that it may not work on other devices." - "If you would like to share your project, make sure your custom files " - "are in a subfolder of the project save location.", + "which means that it may not work on other devices. If you would like to share your project, " + "make sure your custom files are in a subfolder of the project save location.", stacklevel=2, ) return str(path.resolve()) diff --git a/tests/test_project.py b/tests/test_project.py index 82fc6388..e82a84da 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -1,6 +1,7 @@ """Test the project module.""" import copy +import re import tempfile import warnings from pathlib import Path @@ -140,7 +141,7 @@ def test_classlists(test_project) -> None: """The ClassLists in the "Project" model should contain instances of the models given by the dictionary "model_in_classlist". """ - for model in (fields := RATapi.Project.model_fields): + for model in (fields := test_project.model_fields): if get_origin(fields[model].annotation) == RATapi.ClassList: class_list = getattr(test_project, model) assert class_list._class_handle == get_args(fields[model].annotation)[0] @@ -173,11 +174,9 @@ def test_initialise_wrong_classes(input_model: Callable, model_params: dict) -> """If the "Project" model is initialised with incorrect classes, we should raise a ValidationError.""" with pytest.raises( pydantic.ValidationError, - match=( - "1 validation error for Project\nparameters\n" - " Value error, This ClassList only supports elements of type Parameter. In the input list:\n" - f" index 0 is of type {input_model.__name__}" - ), + match="1 validation error for Project\nparameters\n " + "Value error, This ClassList only supports elements of type Parameter. In the input list:\n" + f" index 0 is of type {input_model.__name__}", ): RATapi.Project(parameters=RATapi.ClassList(input_model(**model_params))) @@ -201,8 +200,8 @@ def test_initialise_wrong_layers( with pytest.raises( pydantic.ValidationError, match=f"1 validation error for Project\nlayers\n Value error, " - f'"The layers attribute contains {input_model.__name__}s, but the absorption parameter is {absorption}. ' - f'The attribute should be a ClassList of {actual_model_name} instead."', + f'"The layers attribute contains {input_model.__name__}s, but the absorption parameter is ' + f'{absorption}. The attribute should be a ClassList of {actual_model_name} instead."', ): RATapi.Project(absorption=absorption, layers=RATapi.ClassList(input_model(**model_params))) @@ -236,8 +235,8 @@ def test_initialise_ambiguous_layers(absorption: bool, model: RATapi.models.RATM def test_initialise_wrong_contrasts( input_model: RATapi.models.RATModel, calculation: Calculations, actual_model_name: str ) -> None: - """If the "Project" model is initialised with the incorrect contrast model given the value of calculation, we should - raise a ValidationError. + """If the "Project" model is initialised with the incorrect contrast model given the value of calculation, we + should raise a ValidationError. """ word = "without" if calculation == Calculations.Domains else "with" with pytest.raises( @@ -353,8 +352,8 @@ def test_assign_wrong_layers( with pytest.raises( pydantic.ValidationError, match=f"1 validation error for Project\nlayers\n Value error, " - f'"The layers attribute contains {wrong_input_model.__name__}s, but the absorption parameter is {absorption}. ' - f'The attribute should be a ClassList of {actual_model_name} instead."', + f'"The layers attribute contains {wrong_input_model.__name__}s, but the absorption parameter is ' + f'{absorption}. The attribute should be a ClassList of {actual_model_name} instead."', ): project.layers = RATapi.ClassList(wrong_input_model(**model_params)) @@ -373,8 +372,8 @@ def test_assign_wrong_contrasts(wrong_input_model: Callable, calculation: Calcul with pytest.raises( pydantic.ValidationError, match=f"1 validation error for Project\ncontrasts\n" - f' Value error, "The contrasts attribute contains contrasts {word} ratio, ' - f'but the calculation is {calculation}"', + f' Value error, "The contrasts attribute contains contrasts {word} ratio, but the calculation is ' + f'{calculation}"', ): project.contrasts = RATapi.ClassList(wrong_input_model()) @@ -608,8 +607,8 @@ def test_check_protected_parameters(delete_operation) -> None: with pytest.raises( pydantic.ValidationError, - match="1 validation error for Project\n Value error, Can't delete" - " the protected parameters: Substrate Roughness", + match="1 validation error for Project\n Value error, " + "Can't delete the protected parameters: Substrate Roughness", ): eval(delete_operation) @@ -661,15 +660,18 @@ def test_rename_models(test_project, model: str, fields: list[str]) -> None: ], ) def test_allowed_backgrounds(background_type, expected_field) -> None: - """If the source field of the Background model are set to values that are not specified in the background - parameters, we should raise a ValidationError. + """ + If the source field of the Background model is set to a value that is not specified in the appropriate ClassList, + we should raise a ValidationError. """ test_background = RATapi.models.Background(type=background_type, source="undefined") with pytest.raises( pydantic.ValidationError, - match="1 validation error for Project\n Value error, The value " - '"undefined" in the "source" field of "backgrounds" must be ' - f'defined in "{expected_field}".', + match=re.escape( + f'1 validation error for Project\n Value error, The value "undefined" used in the "source" field of ' + f'backgrounds[0] must be defined in "{expected_field}". Please add "undefined" to "{expected_field}" ' + f'before including it in "backgrounds".' + ), ): RATapi.Project(backgrounds=RATapi.ClassList(test_background)) @@ -690,9 +692,11 @@ def test_allowed_layers(field: str) -> None: with pytest.raises( pydantic.ValidationError, - match=f"1 validation error for Project\n Value error, The value " - f'"undefined" in the "{field}" field of "layers" must be ' - f'defined in "parameters".', + match=re.escape( + f'1 validation error for Project\n Value error, The value "undefined" used in the "{field}" ' + f'field of layers[0] must be defined in "parameters". Please add "undefined" to "parameters" ' + f'before including it in "layers".' + ), ): RATapi.Project( absorption=False, @@ -724,9 +728,11 @@ def test_allowed_absorption_layers(field: str) -> None: with pytest.raises( pydantic.ValidationError, - match=f"1 validation error for Project\n Value error, The value " - f'"undefined" in the "{field}" field of "layers" must be ' - f'defined in "parameters".', + match=re.escape( + f'1 validation error for Project\n Value error, The value "undefined" used in the "{field}" field of ' + f'layers[0] must be defined in "parameters". Please add "undefined" to "parameters" before including it ' + f'in "layers".' + ), ): RATapi.Project( absorption=True, @@ -756,9 +762,11 @@ def test_allowed_resolutions(resolution_type, expected_field) -> None: test_resolution = RATapi.models.Resolution(type=resolution_type, source="undefined") with pytest.raises( pydantic.ValidationError, - match="1 validation error for Project\n Value error, The value " - '"undefined" in the "source" field of "resolutions" must be ' - f'defined in "{expected_field}".', + match=re.escape( + f'1 validation error for Project\n Value error, The value "undefined" used in the "source" field of ' + f'resolutions[0] must be defined in "{expected_field}". Please add "undefined" to "{expected_field}" ' + f'before including it in "resolutions".' + ), ): RATapi.Project(resolutions=RATapi.ClassList(test_resolution)) @@ -781,9 +789,11 @@ def test_allowed_contrasts(field: str, model_name: str) -> None: test_contrast = RATapi.models.Contrast(**{field: "undefined"}) with pytest.raises( pydantic.ValidationError, - match=f"1 validation error for Project\n Value error, The value " - f'"undefined" in the "{field}" field of "contrasts" must be ' - f'defined in "{model_name}".', + match=re.escape( + f'1 validation error for Project\n Value error, The value "undefined" used in the "{field}" field of ' + f'contrasts[0] must be defined in "{model_name}". Please add "undefined" to "{model_name}" before ' + f'including it in "contrasts".' + ), ): RATapi.Project(calculation=Calculations.Normal, contrasts=RATapi.ClassList(test_contrast)) @@ -807,9 +817,11 @@ def test_allowed_contrasts_with_ratio(field: str, model_name: str) -> None: test_contrast = RATapi.models.ContrastWithRatio(**{field: "undefined"}) with pytest.raises( pydantic.ValidationError, - match=f"1 validation error for Project\n Value error, The value " - f'"undefined" in the "{field}" field of "contrasts" must be ' - f'defined in "{model_name}".', + match=re.escape( + f'1 validation error for Project\n Value error, The value "undefined" used in the "{field}" field of ' + f'contrasts[0] must be defined in "{model_name}". Please add "undefined" to "{model_name}" before ' + f'including it in "contrasts".' + ), ): RATapi.Project(calculation=Calculations.Domains, contrasts=RATapi.ClassList(test_contrast)) @@ -864,11 +876,14 @@ def test_allowed_contrast_models( """If any value in the model field of the contrasts is set to a value not specified in the appropriate part of the project, we should raise a ValidationError. """ + missing_values = list(set(test_contrast.model)) with pytest.raises( pydantic.ValidationError, - match=f"1 validation error for Project\n Value error, The values: " - f'"{", ".join(test_contrast.model)}" in the "model" field of ' - f'"contrasts" must be defined in "{field_name}".', + match=re.escape( + f"1 validation error for Project\n Value error, The value{'s' if len(missing_values) > 1 else ''}: " + f'"{", ".join(missing_values)}" used in the "model" field of contrasts[0] must be defined in ' + f'"{field_name}". Please add all required values to "{field_name}" before including them in "contrasts".' + ), ): RATapi.Project(calculation=input_calc, model=input_model, contrasts=RATapi.ClassList(test_contrast)) @@ -880,9 +895,11 @@ def test_allowed_domain_contrast_models() -> None: test_contrast = RATapi.models.DomainContrast(name="Test Domain Contrast", model=["undefined"]) with pytest.raises( pydantic.ValidationError, - match="1 validation error for Project\n Value error, The values: " - '"undefined" in the "model" field of "domain_contrasts" must be ' - 'defined in "layers".', + match=re.escape( + '1 validation error for Project\n Value error, The value: "undefined" used in the "model" field of ' + 'domain_contrasts[0] must be defined in "layers". Please add all required values to "layers" before ' + 'including them in "domain_contrasts".' + ), ): RATapi.Project(calculation=Calculations.Domains, domain_contrasts=RATapi.ClassList(test_contrast)) @@ -934,10 +951,11 @@ def test_get_all_protected_parameters(test_project) -> None: ) def test_check_allowed_values(test_value: str) -> None: """We should not raise an error if string values are defined and on the list of allowed values.""" + allowed_values = ["Substrate Roughness"] project = RATapi.Project.model_construct( layers=RATapi.ClassList(RATapi.models.Layer(**dict(layer_params, roughness=test_value))) ) - assert project.check_allowed_values("layers", ["roughness"], ["Substrate Roughness"]) is None + assert project.check_allowed_values("layers", ["roughness"], allowed_values, allowed_values) is None @pytest.mark.parametrize( @@ -951,11 +969,15 @@ def test_check_allowed_values_not_on_list(test_value: str) -> None: project = RATapi.Project.model_construct( layers=RATapi.ClassList(RATapi.models.Layer(**dict(layer_params, roughness=test_value))) ) + allowed_values = ["Substrate Roughness"] with pytest.raises( ValueError, - match=f'The value "{test_value}" in the "roughness" field of "layers" must be defined in "parameters".', + match=re.escape( + f'The value "{test_value}" used in the "roughness" field of layers[0] must be defined in "parameters". ' + f'Please add "{test_value}" to "parameters" before including it in "layers".' + ), ): - project.check_allowed_values("layers", ["roughness"], ["Substrate Roughness"]) + project.check_allowed_values("layers", ["roughness"], allowed_values, allowed_values) @pytest.mark.parametrize( @@ -1002,8 +1024,11 @@ def test_check_allowed_background_resolution_values_not_on_constant_list(test_va ) with pytest.raises( ValueError, - match=f'The value "{test_value}" in the "source" field of "backgrounds" must be ' - f'defined in "background_parameters".', + match=re.escape( + f'The value "{test_value}" used in the "source" field of backgrounds[0] must be defined in ' + f'"background_parameters". Please add "{test_value}" to "background_parameters" before including it in ' + f'"backgrounds".' + ), ): project.check_allowed_source( "backgrounds", @@ -1026,7 +1051,10 @@ def test_check_allowed_background_resolution_values_on_data_list(test_value: str ) with pytest.raises( ValueError, - match=f'The value "{test_value}" in the "source" field of "backgrounds" must be defined in "data".', + match=re.escape( + f'The value "{test_value}" used in the "source" field of backgrounds[0] must be defined in "data". Please ' + f'add "{test_value}" to "data" before including it in "backgrounds".' + ), ): project.check_allowed_source("backgrounds") @@ -1045,14 +1073,15 @@ def test_check_contrast_model_allowed_values(test_values: list[str]) -> None: project = RATapi.Project.model_construct( contrasts=RATapi.ClassList(RATapi.models.Contrast(name="Test Contrast", model=test_values)), ) - assert project.check_contrast_model_allowed_values("contrasts", ["Test Layer"], "layers") is None + assert project.check_contrast_model_allowed_values("contrasts", ["Test Layer"], ["Test Layer"], "layers") is None @pytest.mark.parametrize( "test_values", [ - ["Undefined Param"], - ["Test Layer", "Undefined Param"], + ["Undefined Param 1"], + ["Test Layer", "Undefined Param 1"], + ["Undefined Param 1 ", "Test Layer", "Undefined Param 2"], ], ) def test_check_allowed_contrast_model_not_on_list(test_values: list[str]) -> None: @@ -1062,12 +1091,46 @@ def test_check_allowed_contrast_model_not_on_list(test_values: list[str]) -> Non project = RATapi.Project.model_construct( contrasts=RATapi.ClassList(RATapi.models.Contrast(name="Test Contrast", model=test_values)), ) + allowed_values = ["Test Layer"] + missing_values = list(set(test_values) - set(allowed_values)) with pytest.raises( ValueError, - match=f'The values: "{", ".join(str(i) for i in test_values)}" in the "model" field ' - f'of "contrasts" must be defined in "layers".', + match=re.escape( + f'The value{"s" if len(missing_values) > 1 else ""}: "{", ".join(str(i) for i in missing_values)}" used ' + f'in the "model" field of contrasts[0] must be defined in "layers". Please add all required values to ' + f'"layers" before including them in "contrasts".' + ), ): - project.check_contrast_model_allowed_values("contrasts", ["Test Layer"], "layers") + project.check_contrast_model_allowed_values("contrasts", allowed_values, allowed_values, "layers") + + +@pytest.mark.parametrize( + "test_values", + [ + ["Undefined Param 1"], + ["Test Layer", "Undefined Param 1"], + ["Undefined Param 1", "Test Layer", "Undefined Param 2"], + ], +) +def test_check_allowed_contrast_model_removed_from_list(test_values: list[str]) -> None: + """If string values are defined in a non-empty list and any of them have been removed from the list of allowed + values we should raise a ValueError. + """ + project = RATapi.Project.model_construct( + contrasts=RATapi.ClassList(RATapi.models.Contrast(name="Test Contrast", model=test_values)), + ) + previous_values = ["Test Layer", "Undefined Param 1", "Undefined Param 2"] + allowed_values = ["Test Layer"] + missing_values = list(set(test_values) - set(allowed_values)) + with pytest.raises( + ValueError, + match=re.escape( + f'The value{"s" if len(missing_values) > 1 else ""}: "{", ".join(str(i) for i in missing_values)}" used ' + f'in the "model" field of contrasts[0] must be defined in "layers". Please remove all unnecessary values ' + f'from "model" before attempting to delete them.' + ), + ): + project.check_contrast_model_allowed_values("contrasts", allowed_values, previous_values, "layers") @pytest.mark.parametrize( @@ -1155,15 +1218,18 @@ def test_wrap_set(test_project, class_list: str, model_type: str, field: str) -> test_attribute = getattr(test_project, class_list) orig_class_list = copy.deepcopy(test_attribute) class_list_str = f"{class_list}{f'.{model_type}' if model_type else ''}.{field}" + index = 0 with pytest.raises( pydantic.ValidationError, - match=f"1 validation error for Project\n Value error, The value " - f'"undefined" in the "{field}" field of "{class_list}" must be ' - f"defined in " - f'"{RATapi.project.values_defined_in[class_list_str]}".', + match=re.escape( + f'1 validation error for Project\n Value error, The value "undefined" used in the "{field}" field of ' + f'{class_list}[{index}] must be defined in "{RATapi.project.values_defined_in[class_list_str]}". Please ' + f'add "undefined" to "{RATapi.project.values_defined_in[class_list_str]}" before including it in ' + f'"{class_list}".' + ), ): - test_attribute.set_fields(0, **{field: "undefined"}) + test_attribute.set_fields(index, **{field: "undefined"}) # Ensure invalid model was not changed assert test_attribute == orig_class_list @@ -1189,12 +1255,17 @@ def test_wrap_del(test_project, class_list: str, parameter: str, field: str) -> orig_class_list = copy.deepcopy(test_attribute) index = test_attribute.index(parameter) + sub_attribute_name = RATapi.project.model_names_used_in[class_list][0].attribute + sub_attribute = getattr(test_project, sub_attribute_name) + sub_index = [i for i, _ in enumerate(sub_attribute) if getattr(sub_attribute[i], field) == parameter][0] + with pytest.raises( pydantic.ValidationError, - match=f"1 validation error for Project\n Value error, The value " - f'"{parameter}" in the "{field}" field of ' - f'"{RATapi.project.model_names_used_in[class_list][0].attribute}" ' - f'must be defined in "{class_list}".', + match=re.escape( + f'1 validation error for Project\n Value error, The value "{parameter}" used in the "{field}" field of ' + f'{sub_attribute_name}[{sub_index}] must be defined in "{class_list}". Please remove "{parameter}" from ' + f'"{sub_attribute_name}[{sub_index}].{field}" before attempting to delete it.' + ), ): del test_attribute[index] @@ -1228,10 +1299,12 @@ def test_wrap_iadd(test_project, class_list: str, model_type: str, field: str, m with pytest.raises( pydantic.ValidationError, - match=f"1 validation error for Project\n Value error, The value " - f'"undefined" in the "{field}" field of "{class_list}" must be ' - f"defined in " - f'"{RATapi.project.values_defined_in[class_list_str]}".', + match=re.escape( + f'1 validation error for Project\n Value error, The value "undefined" used in the "{field}" ' + f"field of {class_list}[{len(test_attribute)}] must be defined in " + f'"{RATapi.project.values_defined_in[class_list_str]}". Please add "undefined" to ' + f'"{RATapi.project.values_defined_in[class_list_str]}" before including it in "{class_list}".' + ), ): test_attribute += [input_model(**{**model_params, field: "undefined"})] @@ -1266,10 +1339,12 @@ def test_wrap_append(test_project, class_list: str, model_type: str, field: str, with pytest.raises( pydantic.ValidationError, - match=f"1 validation error for Project\n Value error, The value " - f'"undefined" in the "{field}" field of "{class_list}" must be ' - f"defined in " - f'"{RATapi.project.values_defined_in[class_list_str]}".', + match=re.escape( + f'1 validation error for Project\n Value error, The value "undefined" used in the "{field}" ' + f"field of {class_list}[{len(test_attribute)}] must be defined in " + f'"{RATapi.project.values_defined_in[class_list_str]}". Please add "undefined" to ' + f'"{RATapi.project.values_defined_in[class_list_str]}" before including it in "{class_list}".' + ), ): test_attribute.append(input_model(**{**model_params, field: "undefined"})) @@ -1300,15 +1375,18 @@ def test_wrap_insert(test_project, class_list: str, model_type: str, field: str, orig_class_list = copy.deepcopy(test_attribute) input_model = model_classes[class_list] class_list_str = f"{class_list}{f'.{model_type}' if model_type else ''}.{field}" + index = 0 with pytest.raises( pydantic.ValidationError, - match=f"1 validation error for Project\n Value error, The value " - f'"undefined" in the "{field}" field of "{class_list}" must be ' - f"defined in " - f'"{RATapi.project.values_defined_in[class_list_str]}".', + match=re.escape( + f'1 validation error for Project\n Value error, The value "undefined" used in the "{field}" ' + f"field of {class_list}[{index}] must be defined in " + f'"{RATapi.project.values_defined_in[class_list_str]}". Please add "undefined" to ' + f'"{RATapi.project.values_defined_in[class_list_str]}" before including it in "{class_list}".' + ), ): - test_attribute.insert(0, input_model(**{**model_params, field: "undefined"})) + test_attribute.insert(index, input_model(**{**model_params, field: "undefined"})) # Ensure invalid model was not inserted assert test_attribute == orig_class_list @@ -1371,12 +1449,17 @@ def test_wrap_pop(test_project, class_list: str, parameter: str, field: str) -> orig_class_list = copy.deepcopy(test_attribute) index = test_attribute.index(parameter) + sub_attribute_name = RATapi.project.model_names_used_in[class_list][0].attribute + sub_attribute = getattr(test_project, sub_attribute_name) + sub_index = [i for i, _ in enumerate(sub_attribute) if getattr(sub_attribute[i], field) == parameter][0] + with pytest.raises( pydantic.ValidationError, - match=f"1 validation error for Project\n Value error, The value " - f'"{parameter}" in the "{field}" field of ' - f'"{RATapi.project.model_names_used_in[class_list][0].attribute}" ' - f'must be defined in "{class_list}".', + match=re.escape( + f'1 validation error for Project\n Value error, The value "{parameter}" used in the "{field}" field of ' + f'{sub_attribute_name}[{sub_index}] must be defined in "{class_list}". Please remove "{parameter}" from ' + f'"{sub_attribute_name}[{sub_index}].{field}" before attempting to delete it.' + ), ): test_attribute.pop(index) @@ -1403,12 +1486,17 @@ def test_wrap_remove(test_project, class_list: str, parameter: str, field: str) test_attribute = getattr(test_project, class_list) orig_class_list = copy.deepcopy(test_attribute) + sub_attribute_name = RATapi.project.model_names_used_in[class_list][0].attribute + sub_attribute = getattr(test_project, sub_attribute_name) + sub_index = [i for i, _ in enumerate(sub_attribute) if getattr(sub_attribute[i], field) == parameter][0] + with pytest.raises( pydantic.ValidationError, - match=f"1 validation error for Project\n Value error, The value " - f'"{parameter}" in the "{field}" field of ' - f'"{RATapi.project.model_names_used_in[class_list][0].attribute}" ' - f'must be defined in "{class_list}".', + match=re.escape( + f'1 validation error for Project\n Value error, The value "{parameter}" used in the "{field}" field of ' + f'{sub_attribute_name}[{sub_index}] must be defined in "{class_list}". Please remove "{parameter}" from ' + f'"{sub_attribute_name}[{sub_index}].{field}" before attempting to delete it.' + ), ): test_attribute.remove(parameter) @@ -1435,12 +1523,17 @@ def test_wrap_clear(test_project, class_list: str, parameter: str, field: str) - test_attribute = getattr(test_project, class_list) orig_class_list = copy.deepcopy(test_attribute) + sub_attribute_name = RATapi.project.model_names_used_in[class_list][0].attribute + sub_attribute = getattr(test_project, sub_attribute_name) + sub_index = [i for i, _ in enumerate(sub_attribute) if getattr(sub_attribute[i], field) == parameter][0] + with pytest.raises( pydantic.ValidationError, - match=f"1 validation error for Project\n Value error, The value " - f'"{parameter}" in the "{field}" field of ' - f'"{RATapi.project.model_names_used_in[class_list][0].attribute}" ' - f'must be defined in "{class_list}".', + match=re.escape( + f'1 validation error for Project\n Value error, The value "{parameter}" used in the "{field}" field of ' + f'{sub_attribute_name}[{sub_index}] must be defined in "{class_list}". Please remove "{parameter}" from ' + f'"{sub_attribute_name}[{sub_index}].{field}" before attempting to delete it.' + ), ): test_attribute.clear() @@ -1474,10 +1567,12 @@ def test_wrap_extend(test_project, class_list: str, model_type: str, field: str, with pytest.raises( pydantic.ValidationError, - match=f"1 validation error for Project\n Value error, The value " - f'"undefined" in the "{field}" field of "{class_list}" must be ' - f"defined in " - f'"{RATapi.project.values_defined_in[class_list_str]}".', + match=re.escape( + f'1 validation error for Project\n Value error, The value "undefined" used in the "{field}" ' + f"field of {class_list}[{len(test_attribute)}] must be defined in " + f'"{RATapi.project.values_defined_in[class_list_str]}". Please add "undefined" to ' + f'"{RATapi.project.values_defined_in[class_list_str]}" before including it in "{class_list}".' + ), ): test_attribute.extend([input_model(**{**model_params, field: "undefined"})]) @@ -1518,7 +1613,7 @@ def test_save_load(project, request): for file in original_project.custom_files: file.path = file.path.resolve() - for field in RATapi.Project.model_fields: + for field in original_project.model_fields: assert getattr(converted_project, field) == getattr(original_project, field) @@ -1539,9 +1634,8 @@ def test_relative_paths_warning(): with pytest.warns( match="Could not save custom file path as relative to the project directory, " - "which means that it may not work on other devices." - "If you would like to share your project, make sure your custom files " - "are in a subfolder of the project save location.", + "which means that it may not work on other devices. If you would like to share your project, " + "make sure your custom files are in a subfolder of the project save location.", ): assert ( Path(RATapi.project.try_relative_to(data_path, relative_path)) From b2d34a08baa7c9ca2091f240041c241bd4b57916 Mon Sep 17 00:00:00 2001 From: StephenNneji <34302892+StephenNneji@users.noreply.github.com> Date: Thu, 24 Apr 2025 11:17:12 +0100 Subject: [PATCH 19/20] Removes wait_for_close (#150) --- RATapi/utils/plotting.py | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/RATapi/utils/plotting.py b/RATapi/utils/plotting.py index f9abcee5..12ab5068 100644 --- a/RATapi/utils/plotting.py +++ b/RATapi/utils/plotting.py @@ -473,9 +473,7 @@ def plot_corner( fig.tight_layout() if return_fig: return fig - fig.show() - if block: - fig.wait_for_close() + plt.show(block=block) @assert_bayesian("Histogram") @@ -585,9 +583,7 @@ def plot_one_hist( if fig is not None: if return_fig: return fig - fig.show() - if block: - fig.wait_for_close() + plt.show(block=block) @assert_bayesian("Contour") @@ -662,9 +658,7 @@ def plot_contour( if fig is not None: if return_fig: return fig - fig.show() - if block: - fig.wait_for_close() + plt.show(block=block) def panel_plot_helper(plot_func: Callable, indices: list[int]) -> matplotlib.figure.Figure: @@ -804,9 +798,7 @@ def validate_dens_type(dens_type: Union[str, None], param: str): ) if return_fig: return fig - fig.show() - if block: - fig.wait_for_close() + plt.show(block=block) @assert_bayesian("Chain") @@ -858,9 +850,7 @@ def plot_one_chain(axes: Axes, i: int): fig = panel_plot_helper(plot_one_chain, params) if return_fig: return fig - fig.show() - if block: - fig.wait_for_close() + plt.show(block=block) def plot_bayes(project: RATapi.Project, results: RATapi.outputs.BayesResults): From 2679d081ad47aa09ad4d67ecd7ff3e45b149d5ab Mon Sep 17 00:00:00 2001 From: Paul Sharp <44529197+DrPaulSharp@users.noreply.github.com> Date: Thu, 24 Apr 2025 14:25:07 +0100 Subject: [PATCH 20/20] Creates new dev release (#151) --- .gitignore | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c1b6a85d..fde40baa 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,9 @@ htmlcov/ .coverage .cache/ +# Temp files +tmp/ + # Build _build docs/.buildinfo diff --git a/setup.py b/setup.py index d41a6ce8..f3f294db 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ from setuptools.command.build_clib import build_clib from setuptools.command.build_ext import build_ext -__version__ = "0.0.0.dev4" +__version__ = "0.0.0.dev5" PACKAGE_NAME = "RATapi" with open("README.md") as f: