|
| 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) |
0 commit comments