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.
Files changed (63) hide show
  1. ratapi/__init__.py +42 -0
  2. ratapi/classlist.py +620 -0
  3. ratapi/controls.py +258 -0
  4. ratapi/eventManager.dll +0 -0
  5. ratapi/events.py +88 -0
  6. ratapi/examples/__init__.py +25 -0
  7. ratapi/examples/absorption/__init__.py +1 -0
  8. ratapi/examples/absorption/absorption.py +172 -0
  9. ratapi/examples/absorption/volume_thiol_bilayer.py +144 -0
  10. ratapi/examples/convert_rascal_project/Model_IIb.py +90 -0
  11. ratapi/examples/convert_rascal_project/__init__.py +1 -0
  12. ratapi/examples/convert_rascal_project/convert_rascal.py +49 -0
  13. ratapi/examples/data/D2O_spin_down.dat +100 -0
  14. ratapi/examples/data/D2O_spin_up.dat +101 -0
  15. ratapi/examples/data/DSPC_D2O.dat +82 -0
  16. ratapi/examples/data/DSPC_SMW.dat +82 -0
  17. ratapi/examples/data/H2O_spin_down.dat +102 -0
  18. ratapi/examples/data/H2O_spin_up.dat +102 -0
  19. ratapi/examples/data/__init__.py +1 -0
  20. ratapi/examples/data/c_PLP0016596.dat +146 -0
  21. ratapi/examples/data/c_PLP0016601.dat +97 -0
  22. ratapi/examples/data/c_PLP0016607.dat +104 -0
  23. ratapi/examples/data/d2o_background_data.dat +82 -0
  24. ratapi/examples/domains/__init__.py +1 -0
  25. ratapi/examples/domains/alloy_domains.py +34 -0
  26. ratapi/examples/domains/domains_XY_model.py +75 -0
  27. ratapi/examples/domains/domains_custom_XY.py +86 -0
  28. ratapi/examples/domains/domains_custom_layers.py +60 -0
  29. ratapi/examples/domains/domains_standard_layers.py +92 -0
  30. ratapi/examples/languages/__init__.py +1 -0
  31. ratapi/examples/languages/custom_bilayer.py +72 -0
  32. ratapi/examples/languages/run_custom_file_languages.py +41 -0
  33. ratapi/examples/languages/setup_problem.py +130 -0
  34. ratapi/examples/normal_reflectivity/DSPC_custom_XY.py +149 -0
  35. ratapi/examples/normal_reflectivity/DSPC_custom_layers.py +130 -0
  36. ratapi/examples/normal_reflectivity/DSPC_data_background.py +220 -0
  37. ratapi/examples/normal_reflectivity/DSPC_function_background.py +219 -0
  38. ratapi/examples/normal_reflectivity/DSPC_standard_layers.py +210 -0
  39. ratapi/examples/normal_reflectivity/__init__.py +1 -0
  40. ratapi/examples/normal_reflectivity/background_function.py +16 -0
  41. ratapi/examples/normal_reflectivity/custom_XY_DSPC.py +141 -0
  42. ratapi/examples/normal_reflectivity/custom_bilayer_DSPC.py +89 -0
  43. ratapi/inputs.py +603 -0
  44. ratapi/models.py +717 -0
  45. ratapi/outputs.py +821 -0
  46. ratapi/project.py +1091 -0
  47. ratapi/rat_core.cp315-win_amd64.pyd +0 -0
  48. ratapi/run.py +142 -0
  49. ratapi/utils/__init__.py +1 -0
  50. ratapi/utils/convert.py +597 -0
  51. ratapi/utils/custom_errors.py +40 -0
  52. ratapi/utils/enums.py +203 -0
  53. ratapi/utils/matlab.py +254 -0
  54. ratapi/utils/orso.py +247 -0
  55. ratapi/utils/plotting.py +1316 -0
  56. ratapi/wrappers.py +147 -0
  57. ratapi-0.0.0.dev16.dist-info/DELVEWHEEL +2 -0
  58. ratapi-0.0.0.dev16.dist-info/METADATA +61 -0
  59. ratapi-0.0.0.dev16.dist-info/RECORD +63 -0
  60. ratapi-0.0.0.dev16.dist-info/WHEEL +5 -0
  61. ratapi-0.0.0.dev16.dist-info/top_level.txt +1 -0
  62. ratapi.libs/msvcp140-a4c2229bdc2a2a630acdc095b4d86008.dll +0 -0
  63. 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