Skip to content

Commit 14d172f

Browse files
authored
Extract functionality for creating and manipulating a project should be moved from the app to the lib (#189)
* first extraction steps * use data container * most parts are wokring * still some work needs to be done on experiments data * added reset * added save * added load * added functionality * more flexible * bug fix * default project * add materials * test for default * current_path to projeckt_path * project * tests update * reset functionality * reset * project * streamlining * code cleaning * code cleaning * from project_path to path * fix path * pr response * added test for creation of empty collections * pyproject
1 parent a0b0df6 commit 14d172f

File tree

17 files changed

+689
-47
lines changed

17 files changed

+689
-47
lines changed

src/easyreflectometry/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
from importlib import metadata
22

3+
from .project import Project
4+
35
try:
46
__version__ = metadata.version(__package__ or __name__)
57
except metadata.PackageNotFoundError:
68
__version__ = '0.0.0'
9+
10+
__all__ = [
11+
Project,
12+
__version__,
13+
]

src/easyreflectometry/model/model.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ def __init__(
6060
background: Union[Parameter, Number, None] = None,
6161
resolution_function: Union[ResolutionFunction, None] = None,
6262
name: str = 'EasyModel',
63+
unique_name: Optional[str] = None,
6364
interface=None,
6465
):
6566
"""Constructor.
@@ -83,6 +84,7 @@ def __init__(
8384

8485
super().__init__(
8586
name=name,
87+
unique_name=unique_name,
8688
sample=sample,
8789
scale=scale,
8890
background=background,

src/easyreflectometry/model/model_collection.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99

1010
from .model import Model
1111

12-
DEFAULT_COLLECTION = [Model()]
12+
13+
# Needs to be a function, elements are added to the global_object.map
14+
def DEFAULT_ELEMENTS(interface):
15+
return (Model(interface),)
1316

1417

1518
class ModelCollection(BaseCollection):
@@ -23,7 +26,7 @@ def __init__(
2326
):
2427
if not models:
2528
if populate_if_none:
26-
models = self._make_default_collection(DEFAULT_COLLECTION, interface)
29+
models = DEFAULT_ELEMENTS(interface)
2730
else:
2831
models = []
2932
# Needed to ensure an empty list is created when saving and instatiating the object as_dict -> from_dict

src/easyreflectometry/project.py

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
import datetime
2+
import json
3+
import os
4+
from pathlib import Path
5+
from typing import List
6+
from typing import Optional
7+
from typing import Union
8+
9+
from easyscience import global_object
10+
from easyscience.fitting import AvailableMinimizers
11+
12+
from easyreflectometry.data.data_store import DataSet1D
13+
from easyreflectometry.model import Model
14+
from easyreflectometry.model import ModelCollection
15+
from easyreflectometry.sample import Layer
16+
from easyreflectometry.sample import MaterialCollection
17+
from easyreflectometry.sample import Multilayer
18+
from easyreflectometry.sample import Sample
19+
from easyreflectometry.sample.collections.base_collection import BaseCollection
20+
21+
22+
class Project:
23+
def __init__(self):
24+
self._info = self._default_info()
25+
self._path = Path(os.path.expanduser('~'))
26+
self._models = ModelCollection(populate_if_none=False, unique_name='project_models')
27+
self._materials = MaterialCollection(populate_if_none=False, unique_name='project_materials')
28+
self._calculator = None
29+
self._minimizer: AvailableMinimizers = None
30+
self._experiments: List[DataSet1D] = None
31+
self._colors = None
32+
self._report = None
33+
34+
# Project flags
35+
self._project_created = False
36+
self._project_with_experiments = False
37+
38+
def reset(self):
39+
del self._models
40+
del self._materials
41+
global_object.map._clear()
42+
43+
self._models = ModelCollection(populate_if_none=False, unique_name='project_models')
44+
self._materials = MaterialCollection(populate_if_none=False, unique_name='project_materials')
45+
46+
self._info = self._default_info()
47+
self._path = Path(os.path.expanduser('~'))
48+
self._calculator = None
49+
self._minimizer = None
50+
self._experiments = None
51+
self._colors = None
52+
self._report = None
53+
54+
# Project flags
55+
self._project_created = False
56+
self._project_with_experiments = False
57+
58+
@property
59+
def path(self):
60+
return self._path
61+
62+
@path.setter
63+
def path(self, path: Union[Path, str]):
64+
self._path = Path(path)
65+
66+
@property
67+
def models(self) -> ModelCollection:
68+
return self._models
69+
70+
@models.setter
71+
def models(self, models: ModelCollection) -> None:
72+
self._replace_collection(models, self._models)
73+
self._materials.extend(self._get_materials_in_models())
74+
75+
@property
76+
def minimizer(self) -> AvailableMinimizers:
77+
return self._minimizer
78+
79+
@minimizer.setter
80+
def minimizer(self, minimizer: AvailableMinimizers) -> None:
81+
self._minimizer = minimizer
82+
83+
@property
84+
def experiments(self) -> List[DataSet1D]:
85+
return self._experiments
86+
87+
@experiments.setter
88+
def experiments(self, experiments: List[DataSet1D]) -> None:
89+
self._experiments = experiments
90+
91+
@property
92+
def path_json(self):
93+
return self._path / 'project.json'
94+
95+
def default_model(self):
96+
self._replace_collection(MaterialCollection(), self._materials)
97+
98+
layers = [
99+
Layer(material=self._materials[0], thickness=0.0, roughness=0.0, name='Vacuum Layer'),
100+
Layer(material=self._materials[1], thickness=100.0, roughness=3.0, name='Multi-layer'),
101+
Layer(material=self._materials[2], thickness=0.0, roughness=1.2, name='Si Layer'),
102+
]
103+
items = [
104+
Multilayer(layers[0], name='Superphase'),
105+
Multilayer(layers[1], name='Multi-layer'),
106+
Multilayer(layers[2], name='Subphase'),
107+
]
108+
sample = Sample(*items)
109+
sample[0].layers[0].thickness.enabled = False
110+
sample[0].layers[0].roughness.enabled = False
111+
sample[-1].layers[-1].thickness.enabled = False
112+
self._replace_collection([Model(sample=sample)], self._models)
113+
114+
def add_material(self, material: MaterialCollection) -> None:
115+
if material in self._materials:
116+
print(f'WARNING: Material {material} is already in material collection')
117+
else:
118+
self._materials.append(material)
119+
120+
def remove_material(self, index: int) -> None:
121+
if self._materials[index] in self._get_materials_in_models():
122+
print(f'ERROR: Material {self._materials[index]} is used in models')
123+
else:
124+
self._materials.pop(index)
125+
126+
def _default_info(self):
127+
return dict(
128+
name='Example Project',
129+
short_description='reflectometry, 1D',
130+
samples='None',
131+
experiments='None',
132+
modified=datetime.datetime.now().strftime('%d.%m.%Y %H:%M'),
133+
)
134+
135+
def create_project_dir(self):
136+
if not os.path.exists(self._path):
137+
os.makedirs(self._path)
138+
os.makedirs(self._path / 'experiments')
139+
else:
140+
print(f'ERROR: Directory {self._path} already exists')
141+
142+
def save_project_json(self, overwrite=False):
143+
if self.path_json.exists() and not overwrite:
144+
print(f'File already exists {self.path_json}. Overwriting...')
145+
self.path_json.unlink()
146+
try:
147+
project_json = json.dumps(self.as_dict(include_materials_not_in_model=True), indent=4)
148+
self.path_json.parent.mkdir(exist_ok=True, parents=True)
149+
with open(self.path_json, 'w') as file:
150+
file.write(project_json)
151+
except Exception as exception:
152+
print(exception)
153+
154+
def load_project_json(self, path: Optional[Union[Path, str]] = None):
155+
path = Path(path)
156+
if path is None:
157+
path = self.path_json
158+
if path.exists():
159+
with open(path, 'r') as file:
160+
project_dict = json.load(file)
161+
self.reset()
162+
self.from_dict(project_dict)
163+
self._path = path.parent
164+
else:
165+
print(f'ERROR: File {path} does not exist')
166+
167+
def as_dict(self, include_materials_not_in_model=False):
168+
project_dict = {}
169+
project_dict['info'] = self._info
170+
project_dict['project_with_experiments'] = self._project_with_experiments
171+
project_dict['project_created'] = self._project_created
172+
if self._models is not None:
173+
project_dict['models'] = self._models.as_dict(skip=['interface'])
174+
if include_materials_not_in_model:
175+
self._as_dict_add_materials_not_in_model_dict(project_dict)
176+
if self._project_with_experiments:
177+
self._as_dict_add_experiments(project_dict)
178+
if self._minimizer is not None:
179+
project_dict['minimizer'] = self._minimizer.name
180+
if self._calculator is not None:
181+
project_dict['calculator'] = [self._calculator.current_interface_name]
182+
if self._colors is not None:
183+
project_dict['colors'] = self._colors
184+
return project_dict
185+
186+
def _as_dict_add_materials_not_in_model_dict(self, project_dict: dict):
187+
materials_not_in_model = []
188+
for material in self._materials:
189+
if material not in self._get_materials_in_models():
190+
materials_not_in_model.append(material)
191+
if len(materials_not_in_model) > 0:
192+
project_dict['materials_not_in_model'] = MaterialCollection(materials_not_in_model).as_dict(skip=['interface'])
193+
194+
def _as_dict_add_experiments(self, project_dict: dict):
195+
project_dict['experiments'] = []
196+
project_dict['experiments_models'] = []
197+
project_dict['experiments_names'] = []
198+
for experiment in self._experiments:
199+
if self._experiments[0].xe is not None:
200+
project_dict['experiments'].append([experiment.x, experiment.y, experiment.ye, experiment.xe])
201+
else:
202+
project_dict['experiments'].append([experiment.x, experiment.y, experiment.ye])
203+
project_dict['experiments_models'].append(experiment.model.name)
204+
project_dict['experiments_names'].append(experiment.name)
205+
206+
def from_dict(self, project_dict: dict):
207+
keys = list(project_dict.keys())
208+
self._info = project_dict['info']
209+
self._project_with_experiments = project_dict['project_with_experiments']
210+
if 'models' in keys:
211+
self._models = None
212+
self._models = ModelCollection.from_dict(project_dict['models'])
213+
214+
self._replace_collection(self._get_materials_in_models(), self._materials)
215+
216+
if 'materials_not_in_model' in keys:
217+
self._materials.extend(MaterialCollection.from_dict(project_dict['materials_not_in_model']))
218+
219+
if 'minimizer' in keys:
220+
self._minimizer = AvailableMinimizers[project_dict['minimizer']]
221+
else:
222+
self._minimizer = None
223+
if 'experiments' in keys:
224+
self._experiments = self._from_dict_extract_experiments(project_dict)
225+
else:
226+
self._experiments = None
227+
if 'calculator' in keys:
228+
self._calculator = project_dict['calculator']
229+
else:
230+
self._calculator = None
231+
232+
def _from_dict_extract_experiments(self, project_dict: dict):
233+
self._experiments: List[DataSet1D] = []
234+
235+
for i in range(len(project_dict['experiments'])):
236+
self._experiments.append(
237+
DataSet1D(
238+
name=project_dict['experiments_names'][i],
239+
x=project_dict['experiments'][i][0],
240+
y=project_dict['experiments'][i][1],
241+
ye=project_dict['experiments'][i][2],
242+
xe=project_dict['experiments'][i][3],
243+
model=self._models[project_dict['experiments_models'][i]],
244+
)
245+
)
246+
247+
def _get_materials_in_models(self) -> MaterialCollection:
248+
materials_in_model = MaterialCollection(populate_if_none=False)
249+
for model in self._models:
250+
for assembly in model.sample:
251+
for layer in assembly.layers:
252+
materials_in_model.append(layer.material)
253+
return materials_in_model
254+
255+
def _replace_collection(self, src_collection: BaseCollection, dst_collection: BaseCollection) -> None:
256+
# Clear the destination collection
257+
for i in range(len(dst_collection)):
258+
dst_collection.pop(0)
259+
260+
for element in src_collection:
261+
dst_collection.append(element)

src/easyreflectometry/sample/assemblies/gradient_layer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ def __init__(
2323
roughness: Optional[float] = 0.2,
2424
discretisation_elements: int = 10,
2525
name: str = 'EasyGradienLayer',
26+
unique_name: Optional[str] = None,
2627
interface=None,
2728
):
2829
"""Constructor.
@@ -58,6 +59,7 @@ def __init__(
5859
super().__init__(
5960
layers=gradient_layers,
6061
name=name,
62+
unique_name=unique_name,
6163
interface=interface,
6264
type='Gradient-layer',
6365
)

src/easyreflectometry/sample/assemblies/multilayer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ def __init__(
2323
self,
2424
layers: Union[Layer, list[Layer], LayerCollection, None] = None,
2525
name: str = 'EasyMultilayer',
26+
unique_name: Optional[str] = None,
2627
interface=None,
2728
type: str = 'Multi-layer',
2829
populate_if_none: Optional[bool] = True,
@@ -47,7 +48,7 @@ def __init__(
4748
# Else collisions might occur in global_object.map
4849
self.populate_if_none = False
4950

50-
super().__init__(name, layers=layers, type=type, interface=interface)
51+
super().__init__(name, unique_name=unique_name, layers=layers, type=type, interface=interface)
5152

5253
def add_layer(self, *layers: tuple[Layer]) -> None:
5354
"""Add a layer to the multi layer.

src/easyreflectometry/sample/assemblies/surfactant_layer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def __init__(
3030
tail_layer: Optional[LayerAreaPerMolecule] = None,
3131
head_layer: Optional[LayerAreaPerMolecule] = None,
3232
name: str = 'EasySurfactantLayer',
33+
unique_name: Optional[str] = None,
3334
constrain_area_per_molecule: bool = False,
3435
conformal_roughness: bool = False,
3536
interface=None,
@@ -68,6 +69,7 @@ def __init__(
6869
surfactant = LayerCollection(tail_layer, head_layer, name=name)
6970
super().__init__(
7071
name=name,
72+
unique_name=unique_name,
7173
type='Surfactant Layer',
7274
layers=surfactant,
7375
interface=interface,

0 commit comments

Comments
 (0)