easyreflectometry 1.3.2__tar.gz → 1.4.0__tar.gz

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 (53) hide show
  1. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/PKG-INFO +24 -24
  2. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/pyproject.toml +24 -25
  3. easyreflectometry-1.4.0/src/easyreflectometry/__version__.py +1 -0
  4. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/calculators/bornagain/calculator.py +1 -1
  5. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/calculators/calculator_base.py +3 -3
  6. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/calculators/factory.py +1 -1
  7. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/calculators/refl1d/wrapper.py +19 -6
  8. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/data/__init__.py +6 -4
  9. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/data/data_store.py +7 -6
  10. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/data/measurement.py +49 -9
  11. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/fitting.py +4 -1
  12. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/model/__init__.py +7 -5
  13. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/model/model.py +7 -5
  14. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/model/model_collection.py +5 -3
  15. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/model/resolution_functions.py +60 -0
  16. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/project.py +42 -7
  17. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/sample/__init__.py +14 -14
  18. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/sample/assemblies/base_assembly.py +13 -42
  19. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/sample/assemblies/repeating_multilayer.py +1 -1
  20. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/sample/assemblies/surfactant_layer.py +39 -58
  21. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/sample/base_core.py +1 -1
  22. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/sample/collections/base_collection.py +1 -1
  23. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/sample/elements/layers/layer.py +1 -1
  24. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/sample/elements/layers/layer_area_per_molecule.py +17 -23
  25. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/sample/elements/materials/material.py +1 -1
  26. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/sample/elements/materials/material_density.py +7 -10
  27. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/sample/elements/materials/material_mixture.py +7 -20
  28. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/sample/elements/materials/material_solvated.py +1 -1
  29. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/special/parsing.py +3 -6
  30. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/summary/summary.py +2 -2
  31. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/utils.py +2 -2
  32. easyreflectometry-1.3.2/src/easyreflectometry/__version__.py +0 -1
  33. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/.gitignore +0 -0
  34. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/AUTHORS.rst +0 -0
  35. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/LICENSE +0 -0
  36. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/README.md +0 -0
  37. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/__init__.py +0 -0
  38. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/calculators/__init__.py +0 -0
  39. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/calculators/bornagain/wrapper.py +0 -0
  40. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/calculators/refl1d/calculator.py +0 -0
  41. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/calculators/refnx/calculator.py +0 -0
  42. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/calculators/refnx/wrapper.py +0 -0
  43. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/calculators/wrapper_base.py +0 -0
  44. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/main.py +0 -0
  45. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/plot.py +0 -0
  46. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/sample/assemblies/gradient_layer.py +0 -0
  47. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/sample/assemblies/multilayer.py +0 -0
  48. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/sample/collections/layer_collection.py +0 -0
  49. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/sample/collections/material_collection.py +0 -0
  50. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/sample/collections/sample.py +0 -0
  51. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/special/calculations.py +0 -0
  52. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/summary/__init__.py +0 -0
  53. {easyreflectometry-1.3.2 → easyreflectometry-1.4.0}/src/easyreflectometry/summary/html_templates.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: easyreflectometry
3
- Version: 1.3.2
3
+ Version: 1.4.0
4
4
  Summary: A reflectometry python package built on the EasyScience framework.
5
5
  Project-URL: homepage, https://docs.easyreflectometry.org
6
6
  Project-URL: documentation, https://docs.easyreflectometry.org
@@ -41,33 +41,33 @@ Classifier: License :: OSI Approved :: BSD License
41
41
  Classifier: Operating System :: OS Independent
42
42
  Classifier: Programming Language :: Python :: 3
43
43
  Classifier: Programming Language :: Python :: 3 :: Only
44
- Classifier: Programming Language :: Python :: 3.10
45
44
  Classifier: Programming Language :: Python :: 3.11
46
45
  Classifier: Programming Language :: Python :: 3.12
47
46
  Classifier: Topic :: Scientific/Engineering
48
- Requires-Python: <3.13,>=3.10
49
- Requires-Dist: easyscience==1.3.0
50
- Requires-Dist: orsopy==1.2.1
51
- Requires-Dist: refl1d[webview]==1.0.0a12
52
- Requires-Dist: refnx==0.1.52
53
- Requires-Dist: scipp==25.2.0
54
- Requires-Dist: xhtml2pdf==0.2.17
47
+ Requires-Python: <3.13,>=3.11
48
+ Requires-Dist: bumps
49
+ Requires-Dist: easyscience
50
+ Requires-Dist: orsopy
51
+ Requires-Dist: refl1d>=1.0.0rc0
52
+ Requires-Dist: refnx
53
+ Requires-Dist: scipp
54
+ Requires-Dist: xhtml2pdf
55
55
  Provides-Extra: dev
56
- Requires-Dist: build==1.2.2.post1; extra == 'dev'
57
- Requires-Dist: codecov==2.1.13; extra == 'dev'
58
- Requires-Dist: coverage==7.7.0; extra == 'dev'
59
- Requires-Dist: coveralls==4.0.1; extra == 'dev'
60
- Requires-Dist: flake8==7.1.2; extra == 'dev'
61
- Requires-Dist: ipykernel==6.29.5; extra == 'dev'
62
- Requires-Dist: jupyter==1.1.1; extra == 'dev'
63
- Requires-Dist: jupyterlab==4.3.6; extra == 'dev'
64
- Requires-Dist: plopp==25.3.0; extra == 'dev'
65
- Requires-Dist: pooch==1.8.2; extra == 'dev'
66
- Requires-Dist: pytest-cov==6.0.0; extra == 'dev'
67
- Requires-Dist: pytest==8.3.5; extra == 'dev'
68
- Requires-Dist: ruff==0.11.0; extra == 'dev'
69
- Requires-Dist: toml==0.10.2; extra == 'dev'
70
- Requires-Dist: yapf==0.43.0; extra == 'dev'
56
+ Requires-Dist: build; extra == 'dev'
57
+ Requires-Dist: codecov; extra == 'dev'
58
+ Requires-Dist: coverage; extra == 'dev'
59
+ Requires-Dist: coveralls; extra == 'dev'
60
+ Requires-Dist: flake8; extra == 'dev'
61
+ Requires-Dist: ipykernel; extra == 'dev'
62
+ Requires-Dist: jupyter; extra == 'dev'
63
+ Requires-Dist: jupyterlab; extra == 'dev'
64
+ Requires-Dist: plopp; extra == 'dev'
65
+ Requires-Dist: pooch; extra == 'dev'
66
+ Requires-Dist: pytest; extra == 'dev'
67
+ Requires-Dist: pytest-cov; extra == 'dev'
68
+ Requires-Dist: ruff; extra == 'dev'
69
+ Requires-Dist: toml; extra == 'dev'
70
+ Requires-Dist: yapf; extra == 'dev'
71
71
  Provides-Extra: docs
72
72
  Requires-Dist: myst-parser; extra == 'docs'
73
73
  Requires-Dist: nbsphinx; extra == 'docs'
@@ -21,40 +21,40 @@ classifiers = [
21
21
  "Topic :: Scientific/Engineering",
22
22
  "Programming Language :: Python :: 3",
23
23
  "Programming Language :: Python :: 3 :: Only",
24
- "Programming Language :: Python :: 3.10",
25
24
  "Programming Language :: Python :: 3.11",
26
25
  "Programming Language :: Python :: 3.12",
27
26
  "Development Status :: 3 - Alpha"
28
27
  ]
29
28
 
30
- requires-python = ">=3.10,<3.13"
29
+ requires-python = ">=3.11,<3.13"
31
30
 
32
31
  dependencies = [
33
- "easyscience==1.3.0",
34
- "scipp==25.2.0",
35
- "refnx==0.1.52",
36
- "refl1d[webview]==1.0.0a12",
37
- "orsopy==1.2.1",
38
- "xhtml2pdf==0.2.17",
32
+ "easyscience",
33
+ "scipp",
34
+ "refnx",
35
+ "refl1d>=1.0.0rc0",
36
+ "orsopy",
37
+ "xhtml2pdf",
38
+ "bumps",
39
39
  ]
40
40
 
41
41
  [project.optional-dependencies]
42
42
  dev = [
43
- "build==1.2.2.post1",
44
- "codecov==2.1.13",
45
- "coverage==7.7.0",
46
- "coveralls==4.0.1",
47
- "flake8==7.1.2",
48
- "ipykernel==6.29.5",
49
- "jupyter==1.1.1",
50
- "jupyterlab==4.3.6",
51
- "plopp==25.3.0",
52
- "pooch==1.8.2",
53
- "pytest==8.3.5",
54
- "pytest-cov==6.0.0",
55
- "ruff==0.11.0",
56
- "toml==0.10.2",
57
- "yapf==0.43.0",
43
+ "build",
44
+ "codecov",
45
+ "coverage",
46
+ "coveralls",
47
+ "flake8",
48
+ "ipykernel",
49
+ "jupyter",
50
+ "jupyterlab",
51
+ "plopp",
52
+ "pooch",
53
+ "pytest",
54
+ "pytest-cov",
55
+ "ruff",
56
+ "toml",
57
+ "yapf",
58
58
  ]
59
59
 
60
60
  docs = [
@@ -133,10 +133,9 @@ force-single-line = true
133
133
  legacy_tox_ini = """
134
134
  [tox]
135
135
  isolated_build = True
136
- envlist = py{3.10,3.11,3.12}
136
+ envlist = py{3.11,3.12}
137
137
  [gh-actions]
138
138
  python =
139
- 3.10: py310
140
139
  3.11: py311
141
140
  3.12: py312
142
141
  [gh-actions:env]
@@ -0,0 +1 @@
1
+ __version__ = '1.4.0'
@@ -1,6 +1,6 @@
1
1
  __author__ = 'github.com/arm61'
2
2
 
3
- from easyscience.Objects.Inferface import ItemContainer
3
+ from easyscience.fitting.calculators.interface_factory import ItemContainer
4
4
 
5
5
  from easyreflectometry.model import Model
6
6
  from easyreflectometry.sample import Layer
@@ -4,8 +4,8 @@ from abc import ABCMeta
4
4
  from typing import Callable
5
5
 
6
6
  import numpy as np
7
- from easyscience.Objects.core import ComponentSerializer
8
- from easyscience.Objects.Inferface import ItemContainer
7
+ from easyscience.fitting.calculators.interface_factory import ItemContainer
8
+ from easyscience.io import SerializerComponent
9
9
 
10
10
  from easyreflectometry.model import Model
11
11
  from easyreflectometry.sample import BaseAssembly
@@ -17,7 +17,7 @@ from easyreflectometry.sample import Multilayer
17
17
  from .wrapper_base import WrapperBase
18
18
 
19
19
 
20
- class CalculatorBase(ComponentSerializer, metaclass=ABCMeta):
20
+ class CalculatorBase(SerializerComponent, metaclass=ABCMeta):
21
21
  """
22
22
  This class is a template and defines all properties that a calculator should have.
23
23
  """
@@ -1,7 +1,7 @@
1
1
  __author__ = 'github.com/wardsimon'
2
2
  from typing import Callable
3
3
 
4
- from easyscience.Objects.Inferface import InterfaceFactoryTemplate
4
+ from easyscience.fitting.calculators.interface_factory import InterfaceFactoryTemplate
5
5
 
6
6
  from easyreflectometry.calculators import CalculatorBase
7
7
 
@@ -42,9 +42,7 @@ class Refl1dWrapper(WrapperBase):
42
42
 
43
43
  :param name: The name of the item
44
44
  """
45
- self.storage['item'][name] = Repeat(
46
- names.Stack(names.Slab(names.SLD(), thickness=0, interface=0)), name=str(name)
47
- )
45
+ self.storage['item'][name] = Repeat(names.Stack(names.Slab(names.SLD(), thickness=0, interface=0)), name=str(name))
48
46
  del self.storage['item'][name].stack[0]
49
47
 
50
48
  def update_layer(self, name: str, **kwargs):
@@ -66,7 +64,9 @@ class Refl1dWrapper(WrapperBase):
66
64
  :param key: The given value keys
67
65
  """
68
66
  if key in ['magnetism_rhoM', 'magnetism_thetaM']:
69
- return getattr(self.storage['layer'][name].magnetism, key.split('_')[-1]).value #TODO: check if we want to return the raw value or the full Parameter # noqa: E501
67
+ return getattr(
68
+ self.storage['layer'][name].magnetism, key.split('_')[-1]
69
+ ).value # TODO: check if we want to return the raw value or the full Parameter # noqa: E501
70
70
  return super().get_layer_value(name, key)
71
71
 
72
72
  def create_model(self, name: str):
@@ -231,6 +231,7 @@ def _get_probe(
231
231
  model_name: str,
232
232
  storage: dict,
233
233
  oversampling_factor: int = 1,
234
+ magnetism: bool = False,
234
235
  ) -> names.QProbe:
235
236
  probe = names.QProbe(
236
237
  Q=q_array,
@@ -238,6 +239,12 @@ def _get_probe(
238
239
  intensity=storage['model'][model_name]['scale'],
239
240
  background=storage['model'][model_name]['bkg'],
240
241
  )
242
+
243
+ # Add theta_offset attribute if magnetism is enabled
244
+ # This is required for PolarizedQProbe to work correctly
245
+ if magnetism:
246
+ probe.theta_offset = names.Parameter.default(0, name='theta_offset')
247
+
241
248
  if oversampling_factor > 1:
242
249
  probe.calc_Qo = _get_oversampling_q(q_array, dq_array, oversampling_factor)
243
250
  return probe
@@ -250,7 +257,7 @@ def _get_polarized_probe(
250
257
  storage: dict,
251
258
  oversampling_factor: int = 1,
252
259
  all_polarizations: bool = False,
253
- ) -> names.PolarizedQProbe:
260
+ ) -> names.PolarizedNeutronQProbe:
254
261
  four_probes = []
255
262
  for i in range(4):
256
263
  if i == 0 or all_polarizations:
@@ -260,11 +267,17 @@ def _get_polarized_probe(
260
267
  model_name=model_name,
261
268
  storage=storage,
262
269
  oversampling_factor=oversampling_factor,
270
+ magnetism=True, # Enable magnetism for polarized probes
263
271
  )
264
272
  else:
265
273
  probe = None
266
274
  four_probes.append(probe)
267
- return names.PolarizedQProbe(xs=four_probes, name='polarized')
275
+
276
+ # Create polarized probe and work around initialization bug
277
+ polarized_probe = names.PolarizedNeutronQProbe.__new__(names.PolarizedNeutronQProbe)
278
+ polarized_probe._union_cache_key = None # Initialize missing attribute
279
+ polarized_probe.__init__(xs=four_probes, name='polarized')
280
+ return polarized_probe
268
281
 
269
282
 
270
283
  def _build_sample(storage: dict, model_name: str) -> names.Stack:
@@ -2,10 +2,12 @@ from .data_store import DataSet1D
2
2
  from .data_store import ProjectData
3
3
  from .measurement import load
4
4
  from .measurement import load_as_dataset
5
+ from .measurement import merge_datagroups
5
6
 
6
7
  __all__ = [
7
- "load",
8
- "load_as_dataset",
9
- "ProjectData",
10
- "DataSet1D",
8
+ 'load',
9
+ 'load_as_dataset',
10
+ 'merge_datagroups',
11
+ 'ProjectData',
12
+ 'DataSet1D',
11
13
  ]
@@ -6,15 +6,16 @@ from typing import TypeVar
6
6
  from typing import Union
7
7
 
8
8
  import numpy as np
9
- from easyscience.Objects.core import ComponentSerializer
10
- from easyscience.Utils.io.dict import DictSerializer
9
+ from easyscience.io import SerializerComponent
10
+ from easyscience.io import SerializerDict
11
11
 
12
+ # from easyscience.utils.io.dict import DictSerializer
12
13
  from easyreflectometry.model import Model
13
14
 
14
15
  T = TypeVar('T')
15
16
 
16
17
 
17
- class ProjectData(ComponentSerializer):
18
+ class ProjectData(SerializerComponent):
18
19
  def __init__(self, name='DataStore', exp_data=None, sim_data=None):
19
20
  self.name = name
20
21
  if exp_data is None:
@@ -25,7 +26,7 @@ class ProjectData(ComponentSerializer):
25
26
  self.sim_data = sim_data
26
27
 
27
28
 
28
- class DataStore(Sequence, ComponentSerializer):
29
+ class DataStore(Sequence, SerializerComponent):
29
30
  def __init__(self, *args, name='DataStore'):
30
31
  self.name = name
31
32
  self.items = list(args)
@@ -55,7 +56,7 @@ class DataStore(Sequence, ComponentSerializer):
55
56
  items = d['items']
56
57
  del d['items']
57
58
  obj = cls.from_dict(d)
58
- decoder = DictSerializer()
59
+ decoder = SerializerDict()
59
60
  obj.items = [decoder.decode(item) for item in items]
60
61
  return obj
61
62
 
@@ -68,7 +69,7 @@ class DataStore(Sequence, ComponentSerializer):
68
69
  return [self[idx] for idx in range(len(self)) if self[idx].is_simulation]
69
70
 
70
71
 
71
- class DataSet1D(ComponentSerializer):
72
+ class DataSet1D(SerializerComponent):
72
73
  def __init__(
73
74
  self,
74
75
  name: str = 'Series',
@@ -1,5 +1,6 @@
1
1
  __author__ = 'github.com/arm61'
2
2
 
3
+ import os
3
4
  from typing import TextIO
4
5
  from typing import Union
5
6
 
@@ -25,11 +26,16 @@ def load(fname: Union[TextIO, str]) -> sc.DataGroup:
25
26
  def load_as_dataset(fname: Union[TextIO, str]) -> DataSet1D:
26
27
  """Load data from an ORSO .ort file as a DataSet1D."""
27
28
  data_group = load(fname)
29
+ basename = os.path.splitext(os.path.basename(fname))[0]
30
+ data_name = 'R_' + basename
31
+ coords_name = 'Qz_' + basename
32
+ coords_name = list(data_group['coords'].keys())[0] if coords_name not in data_group['coords'] else coords_name
33
+ data_name = list(data_group['data'].keys())[0] if data_name not in data_group['data'] else data_name
28
34
  return DataSet1D(
29
- x=data_group['coords']['Qz_0'].values,
30
- y=data_group['data']['R_0'].values,
31
- ye=data_group['data']['R_0'].variances,
32
- xe=data_group['coords']['Qz_0'].variances,
35
+ x=data_group['coords'][coords_name].values,
36
+ y=data_group['data'][data_name].values,
37
+ ye=data_group['data'][data_name].variances,
38
+ xe=data_group['coords'][coords_name].variances,
33
39
  )
34
40
 
35
41
 
@@ -86,6 +92,8 @@ def _load_txt(fname: Union[TextIO, str]) -> sc.DataGroup:
86
92
  if ',' in first_line:
87
93
  delimiter = ','
88
94
 
95
+ basename = os.path.splitext(os.path.basename(fname))[0]
96
+
89
97
  try:
90
98
  # First load only the data to check column count
91
99
  data = np.loadtxt(fname, delimiter=delimiter, comments='#')
@@ -97,7 +105,7 @@ def _load_txt(fname: Union[TextIO, str]) -> sc.DataGroup:
97
105
 
98
106
  # Verify minimum column requirement
99
107
  if num_columns < 3:
100
- raise ValueError(f"File must contain at least 3 columns (found {num_columns})")
108
+ raise ValueError(f'File must contain at least 3 columns (found {num_columns})')
101
109
 
102
110
  # Now unpack the data based on column count
103
111
  if num_columns >= 4:
@@ -108,15 +116,47 @@ def _load_txt(fname: Union[TextIO, str]) -> sc.DataGroup:
108
116
 
109
117
  except (ValueError, IOError) as error:
110
118
  # Re-raise with more descriptive message
111
- raise ValueError(f"Failed to load data from {fname}: {str(error)}") from error
119
+ raise ValueError(f'Failed to load data from {fname}: {str(error)}') from error
112
120
 
113
- data = {'R_0': sc.array(dims=['Qz_0'], values=y, variances=np.square(e))}
121
+ data_name = 'R_' + basename
122
+ coords_name = 'Qz_' + basename
123
+ data = {data_name: sc.array(dims=[coords_name], values=y, variances=np.square(e))}
114
124
  coords = {
115
- data['R_0'].dims[0]: sc.array(
116
- dims=['Qz_0'],
125
+ data[data_name].dims[0]: sc.array(
126
+ dims=[coords_name],
117
127
  values=x,
118
128
  variances=np.square(xe),
119
129
  unit=sc.Unit('1/angstrom'),
120
130
  )
121
131
  }
122
132
  return sc.DataGroup(data=data, coords=coords)
133
+
134
+
135
+ def merge_datagroups(*data_groups: sc.DataGroup) -> sc.DataGroup:
136
+ """Merge multiple DataGroups into a single DataGroup."""
137
+ merged_data = {}
138
+ merged_coords = {}
139
+ merged_attrs = {}
140
+
141
+ for group in data_groups:
142
+ for key, value in group['data'].items():
143
+ if key not in merged_data:
144
+ merged_data[key] = value
145
+ else:
146
+ merged_data[key] = sc.concatenate([merged_data[key], value])
147
+
148
+ for key, value in group['coords'].items():
149
+ if key not in merged_coords:
150
+ merged_coords[key] = value
151
+ else:
152
+ merged_coords[key] = sc.concatenate([merged_coords[key], value])
153
+
154
+ if 'attrs' not in group:
155
+ continue
156
+ for key, value in group['attrs'].items():
157
+ if key not in merged_attrs:
158
+ merged_attrs[key] = value
159
+ else:
160
+ merged_attrs[key] = {**merged_attrs[key], **value}
161
+
162
+ return sc.DataGroup(data=merged_data, coords=merged_coords, attrs=merged_attrs)
@@ -50,10 +50,13 @@ class MultiFitter:
50
50
  )
51
51
  sld_profile = self.easy_science_multi_fitter._fit_objects[i].interface.sld_profile(self._models[i].unique_name)
52
52
  new_data[f'SLD_{id}'] = sc.array(dims=[f'z_{id}'], values=sld_profile[1] * 1e-6, unit=sc.Unit('1/angstrom') ** 2)
53
- new_data['attrs'][f'R_{id}_model'] = {'model': sc.scalar(self._models[i].as_dict())}
53
+ if 'attrs' in new_data:
54
+ new_data['attrs'][f'R_{id}_model'] = {'model': sc.scalar(self._models[i].as_dict())}
54
55
  new_data['coords'][f'z_{id}'] = sc.array(
55
56
  dims=[f'z_{id}'], values=sld_profile[0], unit=(1 / new_data['coords'][f'Qz_{id}'].unit).unit
56
57
  )
58
+ new_data['reduced_chi'] = float(result[i].reduced_chi)
59
+ new_data['success'] = result[i].success
57
60
  return new_data
58
61
 
59
62
  def fit_single_data_set_1d(self, data: DataSet1D) -> FitResults:
@@ -2,12 +2,14 @@ from .model import Model
2
2
  from .model_collection import ModelCollection
3
3
  from .resolution_functions import LinearSpline
4
4
  from .resolution_functions import PercentageFwhm
5
+ from .resolution_functions import Pointwise
5
6
  from .resolution_functions import ResolutionFunction
6
7
 
7
8
  __all__ = (
8
- "LinearSpline",
9
- "PercentageFwhm",
10
- "ResolutionFunction",
11
- "Model",
12
- "ModelCollection",
9
+ 'LinearSpline',
10
+ 'PercentageFwhm',
11
+ 'Pointwise',
12
+ 'ResolutionFunction',
13
+ 'Model',
14
+ 'ModelCollection',
13
15
  )
@@ -8,9 +8,9 @@ from typing import Optional
8
8
  from typing import Union
9
9
 
10
10
  import numpy as np
11
+ from easyscience import ObjBase as BaseObj
11
12
  from easyscience import global_object
12
- from easyscience.Objects.ObjectClasses import BaseObj
13
- from easyscience.Objects.variable import Parameter
13
+ from easyscience.variable import Parameter
14
14
 
15
15
  from easyreflectometry.sample import BaseAssembly
16
16
  from easyreflectometry.sample import Sample
@@ -42,6 +42,8 @@ DEFAULTS = {
42
42
  },
43
43
  }
44
44
 
45
+ COLORS = ['#0173B2', '#DE8F05', '#029E73', '#D55E00', '#CC78BC', '#CA9161', '#FBAFE4', '#949494', '#ECE133', '#56B4E9']
46
+
45
47
 
46
48
  class Model(BaseObj):
47
49
  """Model is the class that represents the experiment.
@@ -60,8 +62,8 @@ class Model(BaseObj):
60
62
  scale: Union[Parameter, Number, None] = None,
61
63
  background: Union[Parameter, Number, None] = None,
62
64
  resolution_function: Union[ResolutionFunction, None] = None,
63
- name: str = 'EasyModel',
64
- color: str = 'black',
65
+ name: str = 'Model',
66
+ color: str = COLORS[0],
65
67
  unique_name: Optional[str] = None,
66
68
  interface=None,
67
69
  ):
@@ -70,7 +72,7 @@ class Model(BaseObj):
70
72
  :param sample: The sample being modelled.
71
73
  :param scale: Scaling factor of profile.
72
74
  :param background: Linear background magnitude.
73
- :param name: Name of the model, defaults to 'EasyModel'.
75
+ :param name: Name of the model, defaults to 'Model'.
74
76
  :param resolution_function: Resolution function, defaults to PercentageFwhm.
75
77
  :param interface: Calculator interface, defaults to `None`.
76
78
 
@@ -4,6 +4,7 @@ from typing import List
4
4
  from typing import Optional
5
5
  from typing import Tuple
6
6
 
7
+ from easyreflectometry.model.model import COLORS
7
8
  from easyreflectometry.sample.collections.base_collection import BaseCollection
8
9
 
9
10
  from .model import Model
@@ -18,7 +19,7 @@ class ModelCollection(BaseCollection):
18
19
  def __init__(
19
20
  self,
20
21
  *models: Tuple[Model],
21
- name: str = 'EasyModels',
22
+ name: str = 'Models',
22
23
  interface=None,
23
24
  unique_name: Optional[str] = None,
24
25
  populate_if_none: bool = True,
@@ -41,7 +42,8 @@ class ModelCollection(BaseCollection):
41
42
  :param model: Model to add.
42
43
  """
43
44
  if model is None:
44
- model = Model(name='EasyModel added', interface=self.interface)
45
+ color = COLORS[len(self) % len(COLORS)]
46
+ model = Model(name='Model', interface=self.interface, color=color)
45
47
  self.append(model)
46
48
 
47
49
  def duplicate_model(self, index: int):
@@ -77,6 +79,6 @@ class ModelCollection(BaseCollection):
77
79
  collection.add_model(Model.from_dict(model_data))
78
80
 
79
81
  if len(collection) != len(this_dict['data']):
80
- raise ValueError(f"Expected {len(collection)} models, got {len(this_dict['data'])}")
82
+ raise ValueError(f'Expected {len(collection)} models, got {len(this_dict["data"])}')
81
83
 
82
84
  return collection
@@ -30,6 +30,8 @@ class ResolutionFunction:
30
30
  return PercentageFwhm(data['constant'])
31
31
  if data['smearing'] == 'LinearSpline':
32
32
  return LinearSpline(data['q_data_points'], data['fwhm_values'])
33
+ if data['smearing'] == 'Pointwise':
34
+ return Pointwise([data['q_data_points'], data['R_data_points'], data['sQz_data_points']])
33
35
  raise ValueError('Unknown resolution function type')
34
36
 
35
37
 
@@ -60,3 +62,61 @@ class LinearSpline(ResolutionFunction):
60
62
  self, skip: Optional[List[str]] = None
61
63
  ) -> dict[str, str]: # skip is kept for consistency of the as_dict signature
62
64
  return {'smearing': 'LinearSpline', 'q_data_points': list(self.q_data_points), 'fwhm_values': list(self.fwhm_values)}
65
+
66
+
67
+ # add pointwise smearing funtion
68
+ class Pointwise(ResolutionFunction):
69
+ def __init__(self, q_data_points: list[np.ndarray]):
70
+ self.q_data_points = q_data_points
71
+ self.q = None
72
+
73
+ def smearing(self, q: Union[np.ndarray, float] = None) -> np.ndarray:
74
+ Qz = self.q_data_points[0]
75
+ R = self.q_data_points[1]
76
+ sQz = self.q_data_points[2]
77
+ if q is None:
78
+ q = self.q_data_points[0]
79
+ self.q = q
80
+ sQzs = np.sqrt(sQz)
81
+ if isinstance(Qz, float):
82
+ Qz = np.array(Qz)
83
+
84
+ smeared = self.apply_smooth_smearing(Qz, R, sQzs)
85
+ return smeared
86
+
87
+ def as_dict(
88
+ self, skip: Optional[List[str]] = None
89
+ ) -> dict[str, str]: # skip is kept for consistency of the as_dict signature
90
+ return {
91
+ 'smearing': 'Pointwise',
92
+ 'q_data_points': list(self.q_data_points[0]),
93
+ 'R_data_points': list(self.q_data_points[1]),
94
+ 'sQz_data_points': list(self.q_data_points[2]),
95
+ }
96
+
97
+ def gaussian_smearing(self, qt, Qz, R, sQz):
98
+ weights = np.exp(-0.5 * ((qt - Qz) / sQz) ** 2)
99
+ if np.sum(weights) == 0 or not np.isfinite(np.sum(weights)):
100
+ return np.sum(R)
101
+ weights /= sQz * np.sqrt(2 * np.pi)
102
+ return np.sum(R * weights) / np.sum(weights)
103
+
104
+ def apply_smooth_smearing(self, Qz, R, sQzs):
105
+ """
106
+ Apply smooth resolution smearing using convolution with Gaussian kernel.
107
+ """
108
+ if self.q is None:
109
+ R_smeared = np.zeros_like(Qz)
110
+ else:
111
+ R_smeared = np.zeros_like(self.q)
112
+
113
+ if not isinstance(Qz, np.ndarray):
114
+ Qz = np.array(Qz)
115
+ if not isinstance(R, np.ndarray):
116
+ R = np.array(R)
117
+ R_smeared = np.zeros_like(self.q)
118
+
119
+ for i, qt in enumerate(self.q):
120
+ R_smeared[i] = self.gaussian_smearing(qt, Qz, R, sQzs)
121
+
122
+ return R_smeared