ratapi 0.0.0.dev16__cp315-cp315-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ratapi/__init__.py +42 -0
- ratapi/classlist.py +620 -0
- ratapi/controls.py +258 -0
- ratapi/eventManager.dll +0 -0
- ratapi/events.py +88 -0
- ratapi/examples/__init__.py +25 -0
- ratapi/examples/absorption/__init__.py +1 -0
- ratapi/examples/absorption/absorption.py +172 -0
- ratapi/examples/absorption/volume_thiol_bilayer.py +144 -0
- ratapi/examples/convert_rascal_project/Model_IIb.py +90 -0
- ratapi/examples/convert_rascal_project/__init__.py +1 -0
- ratapi/examples/convert_rascal_project/convert_rascal.py +49 -0
- ratapi/examples/data/D2O_spin_down.dat +100 -0
- ratapi/examples/data/D2O_spin_up.dat +101 -0
- ratapi/examples/data/DSPC_D2O.dat +82 -0
- ratapi/examples/data/DSPC_SMW.dat +82 -0
- ratapi/examples/data/H2O_spin_down.dat +102 -0
- ratapi/examples/data/H2O_spin_up.dat +102 -0
- ratapi/examples/data/__init__.py +1 -0
- ratapi/examples/data/c_PLP0016596.dat +146 -0
- ratapi/examples/data/c_PLP0016601.dat +97 -0
- ratapi/examples/data/c_PLP0016607.dat +104 -0
- ratapi/examples/data/d2o_background_data.dat +82 -0
- ratapi/examples/domains/__init__.py +1 -0
- ratapi/examples/domains/alloy_domains.py +34 -0
- ratapi/examples/domains/domains_XY_model.py +75 -0
- ratapi/examples/domains/domains_custom_XY.py +86 -0
- ratapi/examples/domains/domains_custom_layers.py +60 -0
- ratapi/examples/domains/domains_standard_layers.py +92 -0
- ratapi/examples/languages/__init__.py +1 -0
- ratapi/examples/languages/custom_bilayer.py +72 -0
- ratapi/examples/languages/run_custom_file_languages.py +41 -0
- ratapi/examples/languages/setup_problem.py +130 -0
- ratapi/examples/normal_reflectivity/DSPC_custom_XY.py +149 -0
- ratapi/examples/normal_reflectivity/DSPC_custom_layers.py +130 -0
- ratapi/examples/normal_reflectivity/DSPC_data_background.py +220 -0
- ratapi/examples/normal_reflectivity/DSPC_function_background.py +219 -0
- ratapi/examples/normal_reflectivity/DSPC_standard_layers.py +210 -0
- ratapi/examples/normal_reflectivity/__init__.py +1 -0
- ratapi/examples/normal_reflectivity/background_function.py +16 -0
- ratapi/examples/normal_reflectivity/custom_XY_DSPC.py +141 -0
- ratapi/examples/normal_reflectivity/custom_bilayer_DSPC.py +89 -0
- ratapi/inputs.py +603 -0
- ratapi/models.py +717 -0
- ratapi/outputs.py +821 -0
- ratapi/project.py +1091 -0
- ratapi/rat_core.cp315-win_amd64.pyd +0 -0
- ratapi/run.py +142 -0
- ratapi/utils/__init__.py +1 -0
- ratapi/utils/convert.py +597 -0
- ratapi/utils/custom_errors.py +40 -0
- ratapi/utils/enums.py +203 -0
- ratapi/utils/matlab.py +254 -0
- ratapi/utils/orso.py +247 -0
- ratapi/utils/plotting.py +1316 -0
- ratapi/wrappers.py +147 -0
- ratapi-0.0.0.dev16.dist-info/DELVEWHEEL +2 -0
- ratapi-0.0.0.dev16.dist-info/METADATA +61 -0
- ratapi-0.0.0.dev16.dist-info/RECORD +63 -0
- ratapi-0.0.0.dev16.dist-info/WHEEL +5 -0
- ratapi-0.0.0.dev16.dist-info/top_level.txt +1 -0
- ratapi.libs/msvcp140-a4c2229bdc2a2a630acdc095b4d86008.dll +0 -0
- ratapi.libs/vcomp140-f96f3a14d88d8846f31f3ab38a490304.dll +0 -0
ratapi/models.py
ADDED
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
"""The models module. Contains the pydantic models used by RAT to store project parameters."""
|
|
2
|
+
|
|
3
|
+
import pathlib
|
|
4
|
+
import warnings
|
|
5
|
+
from contextlib import suppress
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import prettytable
|
|
10
|
+
from pydantic import BaseModel, Field, ValidationInfo, field_validator, model_validator
|
|
11
|
+
|
|
12
|
+
from ratapi.utils.enums import BackgroundActions, Hydration, Languages, Priors, TypeOptions
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from enum import StrEnum
|
|
16
|
+
except ImportError:
|
|
17
|
+
from strenum import StrEnum
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Create a counter for each model
|
|
21
|
+
background_number = ["Background", 0]
|
|
22
|
+
contrast_number = ["Contrast", 0]
|
|
23
|
+
custom_file_number = ["Custom File", 0]
|
|
24
|
+
data_number = ["Data", 0]
|
|
25
|
+
domain_contrast_number = ["Domain Contrast", 0]
|
|
26
|
+
layer_number = ["Layer", 0]
|
|
27
|
+
parameter_number = ["Parameter", 0]
|
|
28
|
+
resolution_number = ["Resolution", 0]
|
|
29
|
+
|
|
30
|
+
_model_counter = {
|
|
31
|
+
"Background": background_number,
|
|
32
|
+
"Contrast": contrast_number,
|
|
33
|
+
"ContrastWithRatio": contrast_number,
|
|
34
|
+
"CustomFile": custom_file_number,
|
|
35
|
+
"Data": data_number,
|
|
36
|
+
"DomainContrast": domain_contrast_number,
|
|
37
|
+
"Layer": layer_number,
|
|
38
|
+
"AbsorptionLayer": layer_number,
|
|
39
|
+
"Parameter": parameter_number,
|
|
40
|
+
"ProtectedParameter": parameter_number,
|
|
41
|
+
"Resolution": resolution_number,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _model_name_factory(model_name: str) -> str:
|
|
46
|
+
"""Generate a unique name for model using a global counter.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
model_name : str
|
|
51
|
+
The name of the model class.
|
|
52
|
+
"""
|
|
53
|
+
title, number = _model_counter[model_name]
|
|
54
|
+
_model_counter[model_name][1] += 1
|
|
55
|
+
return f"New {title} {(number + 1)}"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class RATModel(BaseModel, validate_assignment=True, extra="forbid"):
|
|
59
|
+
"""A BaseModel where enums are represented by their value."""
|
|
60
|
+
|
|
61
|
+
def __repr__(self):
|
|
62
|
+
fields_repr = ", ".join(
|
|
63
|
+
repr(v) if a is None else f"{a}={v.value!r}" if isinstance(v, StrEnum) else f"{a}={v!r}"
|
|
64
|
+
for a, v in self.__repr_args__()
|
|
65
|
+
)
|
|
66
|
+
return f"{self.__repr_name__()}({fields_repr})"
|
|
67
|
+
|
|
68
|
+
@field_validator("name", mode="after", check_fields=False)
|
|
69
|
+
@classmethod
|
|
70
|
+
def update_counter(cls, name: str) -> str:
|
|
71
|
+
"""Update the auto name counter if a similar name is manually given.
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
----------
|
|
75
|
+
name : str
|
|
76
|
+
The name of the model.
|
|
77
|
+
"""
|
|
78
|
+
title, number = _model_counter[cls.__name__]
|
|
79
|
+
prefix = f"New {title} "
|
|
80
|
+
if name.startswith(prefix):
|
|
81
|
+
with suppress(ValueError):
|
|
82
|
+
new_number = int(name[len(prefix) :])
|
|
83
|
+
if new_number > number:
|
|
84
|
+
_model_counter[cls.__name__][1] = new_number
|
|
85
|
+
return name
|
|
86
|
+
|
|
87
|
+
def __str__(self):
|
|
88
|
+
table = prettytable.PrettyTable()
|
|
89
|
+
table.field_names = [key.replace("_", " ") for key in self.display_fields]
|
|
90
|
+
table.add_row(list(self.display_fields.values()))
|
|
91
|
+
return table.get_string()
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def display_fields(self) -> dict:
|
|
95
|
+
"""A dictionary of which fields should be displayed by this model and their values."""
|
|
96
|
+
return self.__dict__
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class Signal(RATModel):
|
|
100
|
+
"""Base model for background & resolution signals."""
|
|
101
|
+
|
|
102
|
+
name: str = Field(default="New Signal", min_length=1)
|
|
103
|
+
type: TypeOptions = TypeOptions.Constant
|
|
104
|
+
source: str = ""
|
|
105
|
+
value_1: str = ""
|
|
106
|
+
value_2: str = ""
|
|
107
|
+
value_3: str = ""
|
|
108
|
+
value_4: str = ""
|
|
109
|
+
value_5: str = ""
|
|
110
|
+
|
|
111
|
+
def __setattr__(self, name, value):
|
|
112
|
+
if name == "type":
|
|
113
|
+
warnings.warn(f"Changing the type of {self.name} clears its source and value fields.", stacklevel=2)
|
|
114
|
+
for attr in ["source", "value_1", "value_2", "value_3", "value_4", "value_5"]:
|
|
115
|
+
with warnings.catch_warnings():
|
|
116
|
+
warnings.simplefilter("ignore")
|
|
117
|
+
super().__setattr__(attr, "")
|
|
118
|
+
|
|
119
|
+
super().__setattr__(name, value)
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def display_fields(self) -> dict:
|
|
123
|
+
"""The fields which should be visible in a table and their values.
|
|
124
|
+
|
|
125
|
+
Returns
|
|
126
|
+
-------
|
|
127
|
+
dict
|
|
128
|
+
A dictionary of the fields which should be visible in a table and their values.
|
|
129
|
+
|
|
130
|
+
"""
|
|
131
|
+
visible_fields = ["name", "type", "source"]
|
|
132
|
+
if self.type != TypeOptions.Constant:
|
|
133
|
+
visible_fields.append("value_1")
|
|
134
|
+
if self.type == TypeOptions.Function:
|
|
135
|
+
visible_fields.extend(["value_2", "value_3", "value_4", "value_5"])
|
|
136
|
+
|
|
137
|
+
return {f: getattr(self, f) for f in visible_fields}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class Background(Signal):
|
|
141
|
+
"""A background signal.
|
|
142
|
+
|
|
143
|
+
Parameters
|
|
144
|
+
----------
|
|
145
|
+
name : str
|
|
146
|
+
The name of the background.
|
|
147
|
+
type : TypeOptions
|
|
148
|
+
The type of background (constant, function or data)
|
|
149
|
+
source : str
|
|
150
|
+
The source of the background;
|
|
151
|
+
|
|
152
|
+
- if type is 'constant', this should be the name of a background parameter.
|
|
153
|
+
- if type is 'data', this should be the name of a dataset defined in `Project.data`.
|
|
154
|
+
- if type is 'function', this should be the name of a custom function defined in `Project.custom_files`.
|
|
155
|
+
|
|
156
|
+
value_1, value_2, value_3, value_4, value_5 : str
|
|
157
|
+
Values required by the background.
|
|
158
|
+
|
|
159
|
+
- if type is 'constant', all values will be ignored.
|
|
160
|
+
- if type is 'data', value_1 may be the parameter name for an optional offset. Other values are ignored.
|
|
161
|
+
- if type is 'function', these values may be the names of up to 5 parameters which are passed to the function.
|
|
162
|
+
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
name: str = Field(default_factory=lambda: _model_name_factory("Background"), min_length=1)
|
|
166
|
+
|
|
167
|
+
@model_validator(mode="after")
|
|
168
|
+
def check_unsupported_parameters(self):
|
|
169
|
+
"""Raise an error if the parameters given are not supported for the given type."""
|
|
170
|
+
if self.type == TypeOptions.Constant:
|
|
171
|
+
expected_empty_fields = ["value_1", "value_2", "value_3", "value_4", "value_5"]
|
|
172
|
+
elif self.type == TypeOptions.Data:
|
|
173
|
+
expected_empty_fields = ["value_2", "value_3", "value_4", "value_5"]
|
|
174
|
+
else:
|
|
175
|
+
return self
|
|
176
|
+
|
|
177
|
+
non_empty_fields = [v for v in expected_empty_fields if getattr(self, v) != ""]
|
|
178
|
+
if non_empty_fields:
|
|
179
|
+
raise ValueError(
|
|
180
|
+
f'The following values are not supported by the "{self.type}" Background type: '
|
|
181
|
+
f"{', '.join(non_empty_fields)}"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
return self
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class Contrast(RATModel):
|
|
188
|
+
"""A group of all of the components of a model.
|
|
189
|
+
|
|
190
|
+
Parameters
|
|
191
|
+
----------
|
|
192
|
+
name : str
|
|
193
|
+
The name of the contrast.
|
|
194
|
+
data : str
|
|
195
|
+
The name of the dataset used by the contrast.
|
|
196
|
+
background : str
|
|
197
|
+
The name of the background for the contrast.
|
|
198
|
+
background_action : BackgroundActions
|
|
199
|
+
Whether the background should be added ('add') or subtracted ('subtract') from the data.
|
|
200
|
+
bulk_in : str
|
|
201
|
+
The name of the bulk-in parameter which defines the SLD of the interface between the
|
|
202
|
+
first layer and the environment.
|
|
203
|
+
bulk_out : str
|
|
204
|
+
The name of the bulk-out parameter which defines the SLD of the interface between the last
|
|
205
|
+
layer and the environment.
|
|
206
|
+
scalefactor : str
|
|
207
|
+
The name of the scalefactor which defines how much the data for this contrast should be scaled.
|
|
208
|
+
resolution : str
|
|
209
|
+
The name of the instrument resolution for this contrast.
|
|
210
|
+
resample : bool
|
|
211
|
+
Whether adaptive resampling should be used for interface microslicing.
|
|
212
|
+
repeat_layers : int
|
|
213
|
+
For standard layers, the number of times the set of layers defined in the model should be repeated.
|
|
214
|
+
model : list[str]
|
|
215
|
+
If this is a standard layers model, this should be a list of layer names
|
|
216
|
+
that make up the slab model for this contrast.
|
|
217
|
+
For custom models, this should be a list containing just the custom file name for the
|
|
218
|
+
custom model function.
|
|
219
|
+
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
name: str = Field(default_factory=lambda: _model_name_factory("Contrast"), min_length=1)
|
|
223
|
+
data: str = ""
|
|
224
|
+
background: str = ""
|
|
225
|
+
background_action: BackgroundActions = BackgroundActions.Add
|
|
226
|
+
bulk_in: str = ""
|
|
227
|
+
bulk_out: str = ""
|
|
228
|
+
scalefactor: str = ""
|
|
229
|
+
resolution: str = ""
|
|
230
|
+
resample: bool = False
|
|
231
|
+
repeat_layers: int = Field(default=1, gt=0)
|
|
232
|
+
model: list[str] = []
|
|
233
|
+
|
|
234
|
+
@model_validator(mode="before")
|
|
235
|
+
@classmethod
|
|
236
|
+
def domain_ratio_error(cls, data: Any):
|
|
237
|
+
"""If the extra input 'domain_ratio' is given, give a more descriptive error."""
|
|
238
|
+
if isinstance(data, dict) and data.get("domain_ratio", False):
|
|
239
|
+
raise ValueError(
|
|
240
|
+
"The Contrast class does not support domain ratios. Use the ContrastWithRatio class instead."
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
return data
|
|
244
|
+
|
|
245
|
+
def __str__(self):
|
|
246
|
+
table = prettytable.PrettyTable()
|
|
247
|
+
table.field_names = [key.replace("_", " ") for key in self.__dict__]
|
|
248
|
+
model_entry = "\n".join(element for element in self.model)
|
|
249
|
+
table.add_row(
|
|
250
|
+
[
|
|
251
|
+
self.name,
|
|
252
|
+
self.data,
|
|
253
|
+
self.background,
|
|
254
|
+
self.background_action,
|
|
255
|
+
self.bulk_in,
|
|
256
|
+
self.bulk_out,
|
|
257
|
+
self.scalefactor,
|
|
258
|
+
self.resolution,
|
|
259
|
+
self.resample,
|
|
260
|
+
self.repeat_layers,
|
|
261
|
+
model_entry,
|
|
262
|
+
]
|
|
263
|
+
)
|
|
264
|
+
return table.get_string()
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class ContrastWithRatio(RATModel):
|
|
268
|
+
"""A group of all of the components of a model, including domain terms.
|
|
269
|
+
|
|
270
|
+
Parameters
|
|
271
|
+
----------
|
|
272
|
+
name : str
|
|
273
|
+
The name of the contrast.
|
|
274
|
+
data : str
|
|
275
|
+
The name of the dataset used by the contrast.
|
|
276
|
+
background : str
|
|
277
|
+
The name of the background for the contrast.
|
|
278
|
+
background_action : BackgroundActions
|
|
279
|
+
Whether the background should be added ('add') or subtracted ('subtract') from the data.
|
|
280
|
+
bulk_in : str
|
|
281
|
+
The name of the bulk-in parameter which defines the SLD of the interface between the
|
|
282
|
+
first layer and the environment.
|
|
283
|
+
bulk_out : str
|
|
284
|
+
The name of the bulk-out parameter which defines the SLD of the interface between the last
|
|
285
|
+
layer and the environment.
|
|
286
|
+
scalefactor : str
|
|
287
|
+
resolution : str
|
|
288
|
+
The name of the instrument resolution for this contrast.
|
|
289
|
+
resample : bool
|
|
290
|
+
Whether adaptive resampling should be used for interface microslicing.
|
|
291
|
+
repeat_layers : int
|
|
292
|
+
For standard layers, the number of times the set of layers defined in the model should be repeated.
|
|
293
|
+
domain_ratio : str
|
|
294
|
+
The name of the domain ratio parameter describing how the first domain should be weighted
|
|
295
|
+
relative to the second.
|
|
296
|
+
model : list[str]
|
|
297
|
+
If this is a standard layers model, this should be a list of the names of the two domain contrasts
|
|
298
|
+
which make up the domain model for this contrast.
|
|
299
|
+
For custom models, this should be a list containing just the custom file name for the
|
|
300
|
+
custom model function.
|
|
301
|
+
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
name: str = Field(default_factory=lambda: _model_name_factory("ContrastWithRatio"), min_length=1)
|
|
305
|
+
data: str = ""
|
|
306
|
+
background: str = ""
|
|
307
|
+
background_action: BackgroundActions = BackgroundActions.Add
|
|
308
|
+
bulk_in: str = ""
|
|
309
|
+
bulk_out: str = ""
|
|
310
|
+
scalefactor: str = ""
|
|
311
|
+
resolution: str = ""
|
|
312
|
+
resample: bool = False
|
|
313
|
+
repeat_layers: int = Field(default=1, gt=0)
|
|
314
|
+
domain_ratio: str = ""
|
|
315
|
+
model: list[str] = []
|
|
316
|
+
|
|
317
|
+
def __str__(self):
|
|
318
|
+
table = prettytable.PrettyTable()
|
|
319
|
+
table.field_names = [key.replace("_", " ") for key in self.__dict__]
|
|
320
|
+
model_entry = "\n".join(element for element in self.model)
|
|
321
|
+
table.add_row(
|
|
322
|
+
[
|
|
323
|
+
self.name,
|
|
324
|
+
self.data,
|
|
325
|
+
self.background,
|
|
326
|
+
self.background_action,
|
|
327
|
+
self.bulk_in,
|
|
328
|
+
self.bulk_out,
|
|
329
|
+
self.scalefactor,
|
|
330
|
+
self.resolution,
|
|
331
|
+
self.resample,
|
|
332
|
+
self.repeat_layers,
|
|
333
|
+
self.domain_ratio,
|
|
334
|
+
model_entry,
|
|
335
|
+
]
|
|
336
|
+
)
|
|
337
|
+
return table.get_string()
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class CustomFile(RATModel):
|
|
341
|
+
"""A file containing functions to use for a custom model or function background.
|
|
342
|
+
|
|
343
|
+
Parameters
|
|
344
|
+
----------
|
|
345
|
+
name : str
|
|
346
|
+
The name of this custom file object.
|
|
347
|
+
filename : str
|
|
348
|
+
The name of the file containing the custom function.
|
|
349
|
+
function_name : str
|
|
350
|
+
The name of the custom function within the file.
|
|
351
|
+
language : Languages
|
|
352
|
+
What language the custom function is written in: 'matlab', 'python', or 'cpp' (C++)
|
|
353
|
+
path : pathlib.Path
|
|
354
|
+
The path to the custom file.
|
|
355
|
+
|
|
356
|
+
"""
|
|
357
|
+
|
|
358
|
+
name: str = Field(default_factory=lambda: _model_name_factory("CustomFile"), min_length=1)
|
|
359
|
+
filename: str = ""
|
|
360
|
+
function_name: str = ""
|
|
361
|
+
language: Languages = Languages.Python
|
|
362
|
+
path: pathlib.Path = pathlib.Path(".")
|
|
363
|
+
|
|
364
|
+
def model_post_init(self, __context: Any) -> None:
|
|
365
|
+
"""Autogenerate the function name from the filename if not set.
|
|
366
|
+
|
|
367
|
+
If a filename is supplied but the ``function_name`` field is not set, the ``function_name`` should be set to
|
|
368
|
+
the filename without the extension.
|
|
369
|
+
|
|
370
|
+
"""
|
|
371
|
+
if "filename" in self.model_fields_set and "function_name" not in self.model_fields_set:
|
|
372
|
+
self.function_name = pathlib.Path(self.filename).stem
|
|
373
|
+
|
|
374
|
+
@model_validator(mode="after")
|
|
375
|
+
def set_matlab_function_name(self):
|
|
376
|
+
"""For a matlab custom function, ``function_name`` should be set to the filename without the extension."""
|
|
377
|
+
if self.language == Languages.Matlab and self.function_name != pathlib.Path(self.filename).stem:
|
|
378
|
+
self.function_name = pathlib.Path(self.filename).stem
|
|
379
|
+
|
|
380
|
+
return self
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class Data(RATModel, arbitrary_types_allowed=True):
|
|
384
|
+
"""A dataset required for a contrast.
|
|
385
|
+
|
|
386
|
+
Parameters
|
|
387
|
+
----------
|
|
388
|
+
name : str
|
|
389
|
+
The name of this dataset.
|
|
390
|
+
data : np.ndarray[np.float64]
|
|
391
|
+
The (x, y, error) data for this dataset, given as a Numpy array of three columns.
|
|
392
|
+
data_range : list[float]
|
|
393
|
+
simulation_range : list[float]
|
|
394
|
+
|
|
395
|
+
"""
|
|
396
|
+
|
|
397
|
+
name: str = Field(default_factory=lambda: _model_name_factory("Data"), min_length=1)
|
|
398
|
+
data: np.ndarray = np.empty([0, 3])
|
|
399
|
+
data_range: list[float] = Field(default=[], min_length=2, max_length=2)
|
|
400
|
+
simulation_range: list[float] = Field(default=[], min_length=2, max_length=2)
|
|
401
|
+
|
|
402
|
+
@field_validator("data")
|
|
403
|
+
@classmethod
|
|
404
|
+
def check_data_dimension(cls, data: np.ndarray) -> np.ndarray:
|
|
405
|
+
"""Ensure the data is be a two-dimensional array containing at least three columns."""
|
|
406
|
+
try:
|
|
407
|
+
data.shape[1]
|
|
408
|
+
except IndexError:
|
|
409
|
+
raise ValueError('"data" must have at least two dimensions') from None
|
|
410
|
+
else:
|
|
411
|
+
if data.shape[1] < 3:
|
|
412
|
+
raise ValueError('"data" must have at least three columns') from None
|
|
413
|
+
return data
|
|
414
|
+
|
|
415
|
+
@field_validator("data_range", "simulation_range")
|
|
416
|
+
@classmethod
|
|
417
|
+
def check_min_max(cls, limits: list[float], info: ValidationInfo) -> list[float]:
|
|
418
|
+
"""Ensure the data range and simulation range maximum is greater than the minimum."""
|
|
419
|
+
if limits[0] > limits[1]:
|
|
420
|
+
raise ValueError(f'{info.field_name} "min" value is greater than the "max" value')
|
|
421
|
+
return limits
|
|
422
|
+
|
|
423
|
+
def model_post_init(self, __context: Any) -> None:
|
|
424
|
+
"""Automatically generate ``data_range`` and ``simulation_range`` from the data.
|
|
425
|
+
|
|
426
|
+
If the ``data_range`` and ``simulation_range`` fields are not set, but ``data`` is supplied, the ranges are
|
|
427
|
+
set to the min and max values of the first column (assumed to be q) of the supplied data.
|
|
428
|
+
"""
|
|
429
|
+
if self.data.shape[0] > 0:
|
|
430
|
+
data_min = float(np.min(self.data[:, 0]))
|
|
431
|
+
data_max = float(np.max(self.data[:, 0]))
|
|
432
|
+
for field in ["data_range", "simulation_range"]:
|
|
433
|
+
if field not in self.model_fields_set:
|
|
434
|
+
getattr(self, field).extend([data_min, data_max])
|
|
435
|
+
|
|
436
|
+
@model_validator(mode="after")
|
|
437
|
+
def check_ranges(self) -> "Data":
|
|
438
|
+
"""Check that ``data_range`` is within the q-range of the data, and ``simulation_range`` is outside it.
|
|
439
|
+
|
|
440
|
+
The limits of the "data_range" field must lie within the range of the supplied data, whilst the limits
|
|
441
|
+
of the "simulation_range" field must lie outside the range of the supplied data.
|
|
442
|
+
"""
|
|
443
|
+
if self.data.shape[0] > 0:
|
|
444
|
+
data_min = np.min(self.data[:, 0])
|
|
445
|
+
data_max = np.max(self.data[:, 0])
|
|
446
|
+
if "data_range" in self.model_fields_set and (
|
|
447
|
+
self.data_range[0] < data_min or self.data_range[1] > data_max
|
|
448
|
+
):
|
|
449
|
+
raise ValueError(
|
|
450
|
+
f"The data_range value of: {self.data_range} must lie within the min/max values of "
|
|
451
|
+
f"the data: [{data_min}, {data_max}]",
|
|
452
|
+
)
|
|
453
|
+
if "simulation_range" in self.model_fields_set and (
|
|
454
|
+
self.simulation_range[0] > data_min or self.simulation_range[1] < data_max
|
|
455
|
+
):
|
|
456
|
+
raise ValueError(
|
|
457
|
+
f"The simulation_range value of: {self.simulation_range} must lie outside of the "
|
|
458
|
+
f"min/max values of the data: [{data_min}, {data_max}]",
|
|
459
|
+
)
|
|
460
|
+
return self
|
|
461
|
+
|
|
462
|
+
def __eq__(self, other: object) -> bool:
|
|
463
|
+
if isinstance(other, BaseModel):
|
|
464
|
+
# When comparing instances of generic types for equality, as long as all field values are equal,
|
|
465
|
+
# only require their generic origin types to be equal, rather than exact type equality.
|
|
466
|
+
# This prevents headaches like MyGeneric(x=1) != MyGeneric[Any](x=1).
|
|
467
|
+
self_type = self.__pydantic_generic_metadata__["origin"] or self.__class__
|
|
468
|
+
other_type = other.__pydantic_generic_metadata__["origin"] or other.__class__
|
|
469
|
+
|
|
470
|
+
return (
|
|
471
|
+
self_type == other_type
|
|
472
|
+
and self.name == other.name
|
|
473
|
+
and (self.data == other.data).all()
|
|
474
|
+
and self.data_range == other.data_range
|
|
475
|
+
and self.simulation_range == other.simulation_range
|
|
476
|
+
and self.__pydantic_private__ == other.__pydantic_private__
|
|
477
|
+
and self.__pydantic_extra__ == other.__pydantic_extra__
|
|
478
|
+
)
|
|
479
|
+
else:
|
|
480
|
+
return NotImplemented # delegate to the other item in the comparison
|
|
481
|
+
|
|
482
|
+
def __str__(self):
|
|
483
|
+
table = prettytable.PrettyTable()
|
|
484
|
+
table.field_names = [key.replace("_", " ") for key in self.__dict__]
|
|
485
|
+
array_entry = f"{'Data array: [' + ' x '.join(str(i) for i in self.data.shape) if self.data.size > 0 else '['}]"
|
|
486
|
+
table.add_row([self.name, array_entry, self.data_range, self.simulation_range])
|
|
487
|
+
return table.get_string()
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
class DomainContrast(RATModel):
|
|
491
|
+
"""A group of layers required for a domain.
|
|
492
|
+
|
|
493
|
+
Parameters
|
|
494
|
+
----------
|
|
495
|
+
name : str
|
|
496
|
+
The name of this domain contrast.
|
|
497
|
+
model : list[str]
|
|
498
|
+
A list of layer names that make up the slab model for this contrast.
|
|
499
|
+
|
|
500
|
+
"""
|
|
501
|
+
|
|
502
|
+
name: str = Field(default_factory=lambda: _model_name_factory("DomainContrast"), min_length=1)
|
|
503
|
+
model: list[str] = []
|
|
504
|
+
|
|
505
|
+
def __str__(self):
|
|
506
|
+
table = prettytable.PrettyTable()
|
|
507
|
+
table.field_names = [key.replace("_", " ") for key in self.__dict__]
|
|
508
|
+
model_entry = "\n".join(element for element in self.model)
|
|
509
|
+
table.add_row([self.name, model_entry])
|
|
510
|
+
return table.get_string()
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
class Layer(RATModel, populate_by_name=True):
|
|
514
|
+
"""A slab model layer with given physical properties.
|
|
515
|
+
|
|
516
|
+
Parameters
|
|
517
|
+
----------
|
|
518
|
+
name : str
|
|
519
|
+
The name of this layer.
|
|
520
|
+
thickness : str
|
|
521
|
+
The name of the parameter describing the thickness of this layer.
|
|
522
|
+
SLD : str
|
|
523
|
+
The name of the parameter describing the scattering length density
|
|
524
|
+
of this layer.
|
|
525
|
+
roughness : str
|
|
526
|
+
The name of the parameter describing the roughness of this layer.
|
|
527
|
+
hydration : str
|
|
528
|
+
hydrate_with : str
|
|
529
|
+
|
|
530
|
+
"""
|
|
531
|
+
|
|
532
|
+
name: str = Field(default_factory=lambda: _model_name_factory("Layer"), min_length=1)
|
|
533
|
+
thickness: str
|
|
534
|
+
SLD: str = Field(validation_alias="SLD_real")
|
|
535
|
+
roughness: str
|
|
536
|
+
hydration: str = ""
|
|
537
|
+
hydrate_with: Hydration = Hydration.BulkOut
|
|
538
|
+
|
|
539
|
+
@model_validator(mode="before")
|
|
540
|
+
@classmethod
|
|
541
|
+
def sld_imaginary_error(cls, data: Any):
|
|
542
|
+
"""If the extra input 'sld_imaginary' is given, give a more descriptive error."""
|
|
543
|
+
if isinstance(data, dict) and data.get("SLD_imaginary", False):
|
|
544
|
+
raise ValueError("The Layer class does not support imaginary SLD. Use the AbsorptionLayer class instead.")
|
|
545
|
+
|
|
546
|
+
return data
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
class AbsorptionLayer(RATModel, populate_by_name=True):
|
|
550
|
+
"""A slab model layer with a non-negligible absorption term.
|
|
551
|
+
|
|
552
|
+
Parameters
|
|
553
|
+
----------
|
|
554
|
+
name : str
|
|
555
|
+
The name of this layer.
|
|
556
|
+
thickness : str
|
|
557
|
+
The name of the parameter describing the thickness of this layer.
|
|
558
|
+
SLD_real : str
|
|
559
|
+
The name of the parameter describing the real (scattering) term
|
|
560
|
+
for the scattering length density of this layer.
|
|
561
|
+
SLD_imaginary : str
|
|
562
|
+
The name of the parameter describing the imaginary (absorption) term
|
|
563
|
+
for the scattering length density of this layer.
|
|
564
|
+
roughness : str
|
|
565
|
+
The name of the parameter describing the roughness of this layer.
|
|
566
|
+
hydration : str
|
|
567
|
+
hydrate_with : str
|
|
568
|
+
|
|
569
|
+
"""
|
|
570
|
+
|
|
571
|
+
name: str = Field(default_factory=lambda: _model_name_factory("AbsorptionLayer"), min_length=1)
|
|
572
|
+
thickness: str
|
|
573
|
+
SLD_real: str = Field(validation_alias="SLD")
|
|
574
|
+
SLD_imaginary: str
|
|
575
|
+
roughness: str
|
|
576
|
+
hydration: str = ""
|
|
577
|
+
hydrate_with: Hydration = Hydration.BulkOut
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
class Parameter(RATModel):
|
|
581
|
+
"""A parameter needed to specify the model.
|
|
582
|
+
|
|
583
|
+
Parameters
|
|
584
|
+
----------
|
|
585
|
+
name : str
|
|
586
|
+
The name of this parameter.
|
|
587
|
+
min : float
|
|
588
|
+
The minimum value that this parameter could take when fitted.
|
|
589
|
+
value : float
|
|
590
|
+
The value of this parameter.
|
|
591
|
+
max : float
|
|
592
|
+
The maximum value that this parameter could take when fitted.
|
|
593
|
+
fit : bool
|
|
594
|
+
Whether this parameter should be fitted in a calculation.
|
|
595
|
+
prior_type : Priors
|
|
596
|
+
For Bayesian calculations, whether the prior likelihood
|
|
597
|
+
is assumed to be 'uniform' or 'gaussian'.
|
|
598
|
+
mu, sigma : float
|
|
599
|
+
If the prior type is Gaussian, the mu and sigma values describing
|
|
600
|
+
the Gaussian function for the prior likelihood.
|
|
601
|
+
|
|
602
|
+
"""
|
|
603
|
+
|
|
604
|
+
name: str = Field(default_factory=lambda: _model_name_factory("Parameter"), min_length=1)
|
|
605
|
+
min: float = 0.0
|
|
606
|
+
value: float = 0.0
|
|
607
|
+
max: float = 0.0
|
|
608
|
+
fit: bool = False
|
|
609
|
+
prior_type: Priors = Priors.Uniform
|
|
610
|
+
mu: float = 0.0
|
|
611
|
+
sigma: float = np.inf
|
|
612
|
+
|
|
613
|
+
show_priors: bool = False
|
|
614
|
+
|
|
615
|
+
def model_post_init(self, __context: Any) -> None:
|
|
616
|
+
"""Apply parameter value to limits if they are not set."""
|
|
617
|
+
if "value" in self.model_fields_set:
|
|
618
|
+
if self.value > 0.0:
|
|
619
|
+
if "max" not in self.model_fields_set:
|
|
620
|
+
self.max = self.value
|
|
621
|
+
if "min" not in self.model_fields_set:
|
|
622
|
+
self.min = self.value
|
|
623
|
+
elif self.value < 0.0:
|
|
624
|
+
if "min" not in self.model_fields_set:
|
|
625
|
+
self.min = self.value
|
|
626
|
+
if "max" not in self.model_fields_set:
|
|
627
|
+
self.max = self.value
|
|
628
|
+
|
|
629
|
+
@model_validator(mode="after")
|
|
630
|
+
def check_min_max(self) -> "Parameter":
|
|
631
|
+
"""Ensure the maximum value of a parameter is greater than the minimum."""
|
|
632
|
+
if self.min > self.max:
|
|
633
|
+
raise ValueError(f"The maximum value {self.max} must be greater than the minimum value {self.min}")
|
|
634
|
+
return self
|
|
635
|
+
|
|
636
|
+
@model_validator(mode="after")
|
|
637
|
+
def check_value_in_range(self) -> "Parameter":
|
|
638
|
+
"""Ensure the value of a parameter lies within its defined bounds."""
|
|
639
|
+
if self.value < self.min or self.value > self.max:
|
|
640
|
+
raise ValueError(f"value {self.value} is not within the defined range: {self.min} <= value <= {self.max}")
|
|
641
|
+
return self
|
|
642
|
+
|
|
643
|
+
@property
|
|
644
|
+
def display_fields(self) -> dict:
|
|
645
|
+
"""Only display Prior information if ``show_priors`` is true."""
|
|
646
|
+
visible_fields = ["name", "min", "value", "max", "fit"]
|
|
647
|
+
if self.show_priors:
|
|
648
|
+
visible_fields.append("prior_type")
|
|
649
|
+
if self.prior_type == Priors.Gaussian:
|
|
650
|
+
visible_fields.extend(["mu", "sigma"])
|
|
651
|
+
|
|
652
|
+
return {f: getattr(self, f) for f in visible_fields}
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
class ProtectedParameter(Parameter):
|
|
656
|
+
"""A Parameter with a fixed name."""
|
|
657
|
+
|
|
658
|
+
name: str = Field(frozen=True, min_length=1)
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
class Resolution(Signal):
|
|
662
|
+
"""An instrument resolution.
|
|
663
|
+
|
|
664
|
+
Parameters
|
|
665
|
+
----------
|
|
666
|
+
name : str
|
|
667
|
+
The name of the resolution.
|
|
668
|
+
type : TypeOptions
|
|
669
|
+
The type of resolution: 'constant', 'data', or (NOT YET IMPLEMENTED) 'function'.
|
|
670
|
+
source : str
|
|
671
|
+
The source data for the resolution;
|
|
672
|
+
|
|
673
|
+
- if type is 'constant', this should be the name of a background parameter.
|
|
674
|
+
- if type is 'data', this should be empty (resolution data is in the contrast data).
|
|
675
|
+
- if type is 'function' (NOT YET IMPLEMENTED),
|
|
676
|
+
this should be the name of a custom function defined in `Project.custom_files`.
|
|
677
|
+
|
|
678
|
+
value_1, value_2, value_3, value_4, value_5 : str
|
|
679
|
+
Values required by the background.
|
|
680
|
+
|
|
681
|
+
- if type is 'constant' or 'data', all values will be ignored.
|
|
682
|
+
- if type is 'function' (NOT YET IMPLEMENTED),
|
|
683
|
+
these values may be the names of up to 5 parameters which are passed to the function.
|
|
684
|
+
|
|
685
|
+
"""
|
|
686
|
+
|
|
687
|
+
name: str = Field(default_factory=lambda: _model_name_factory("Resolution"), min_length=1)
|
|
688
|
+
|
|
689
|
+
@field_validator("type")
|
|
690
|
+
@classmethod
|
|
691
|
+
def validate_unimplemented_resolutions(cls, type: TypeOptions):
|
|
692
|
+
"""Raise an error if currently unsupported function resolutions are used."""
|
|
693
|
+
# when function resolutions are added, fix the commented-out parts of
|
|
694
|
+
# test_project.py::test_rename_models
|
|
695
|
+
# and test_project.py::test_allowed_resolutions
|
|
696
|
+
if type == TypeOptions.Function:
|
|
697
|
+
raise NotImplementedError("Function resolutions are not yet supported.")
|
|
698
|
+
return type
|
|
699
|
+
|
|
700
|
+
@model_validator(mode="after")
|
|
701
|
+
def check_unsupported_parameters(self):
|
|
702
|
+
"""Raise an error if the parameters given are not supported for the given type."""
|
|
703
|
+
if self.type == TypeOptions.Constant:
|
|
704
|
+
expected_empty_fields = ["value_1", "value_2", "value_3", "value_4", "value_5"]
|
|
705
|
+
elif self.type == TypeOptions.Data:
|
|
706
|
+
expected_empty_fields = ["source", "value_1", "value_2", "value_3", "value_4", "value_5"]
|
|
707
|
+
else:
|
|
708
|
+
return self
|
|
709
|
+
|
|
710
|
+
non_empty_fields = [v for v in expected_empty_fields if getattr(self, v) != ""]
|
|
711
|
+
if non_empty_fields:
|
|
712
|
+
raise ValueError(
|
|
713
|
+
f'The following values are not supported by the "{self.type}" Resolution type: '
|
|
714
|
+
f"{', '.join(non_empty_fields)}"
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
return self
|