diff --git a/tests/t_parsing/t_parser.py b/tests/t_parsing/t_parser.py index 56433e9..c9ce93a 100644 --- a/tests/t_parsing/t_parser.py +++ b/tests/t_parsing/t_parser.py @@ -21,17 +21,10 @@ def test_report_ok(self): print(f'Parsing report: {as_str}') def test_metadata_ok(self): - metadata = self.pattern.metadata - properties = [metadata.anode_material, metadata.measurement_date] - print(f'anode material {metadata.anode_material}') - print(f'measurement date {metadata.measurement_date}') - - for prop in properties: - self.assertIsNotNone(obj=prop) - - primary_wavelength = metadata.primary - secondary_wavelength = metadata.secondary - ratio = metadata.ratio + metadata = self.pattern.experiment + primary_wavelength = metadata.primary_wavelength + secondary_wavelength = metadata.secondary_wavelength + ratio = metadata.artifacts.secondary_to_primary print(f'prim, sec, ratio {primary_wavelength}, {secondary_wavelength}, {ratio}') for prop in [primary_wavelength, secondary_wavelength, ratio]: diff --git a/tests/t_pattern/t_xrdpattern.py b/tests/t_pattern/t_xrdpattern.py index b38abb1..5c25c4c 100644 --- a/tests/t_pattern/t_xrdpattern.py +++ b/tests/t_pattern/t_xrdpattern.py @@ -35,7 +35,7 @@ def test_data_ok(self): def test_from_angle_data(self): angles = [1.0, 2.0, 3.0] intensities = [10.0, 20.0, 100.0] - pattern = XrdPattern.from_intensitiy_map(angles=angles, intensities=intensities) + pattern = XrdPattern.from_intensitiy_map(two_theta_values=angles, intensities=intensities) self.check_data_ok(*pattern.get_pattern_data(apply_standardization=False)) if __name__ == "__main__": diff --git a/xrdpattern/core/__init__.py b/xrdpattern/core/__init__.py index 251ede4..984c4ff 100644 --- a/xrdpattern/core/__init__.py +++ b/xrdpattern/core/__init__.py @@ -1,2 +1 @@ -from .metadata import Metadata -from .pattern_info import PatternInfo \ No newline at end of file +from .pattern_data import PatternData \ No newline at end of file diff --git a/xrdpattern/core/metadata.py b/xrdpattern/core/metadata.py deleted file mode 100644 index 15a2aa8..0000000 --- a/xrdpattern/core/metadata.py +++ /dev/null @@ -1,79 +0,0 @@ -from __future__ import annotations -from datetime import datetime -from typing import Optional -from holytools.abstract import JsonDataclass -from dataclasses import dataclass - -from typing import Iterator, Tuple - - -# ------------------------------------------- - -@dataclass -class Metadata(JsonDataclass): - primary : Optional[float] - secondary : Optional[float] - ratio : Optional[float] - anode_material: Optional[str] = None - temp_celcius: Optional[float] = None - measurement_date: Optional[datetime] = None - - @classmethod - def make_empty(cls): - return Metadata(primary=None, secondary=None, ratio=None) - - @classmethod - def from_header_str(cls, header_str: str) -> Metadata: - metadata_map = cls.get_key_value_dict(header_str=header_str) - - def get_float(key: str) -> Optional[float]: - val = metadata_map.get(key) - if val: - val = float(val) - return val - - metadata = cls( - primary=get_float('ALPHA1'), - secondary=get_float('ALPHA2'), - ratio=get_float('ALPHA_RATIO'), - anode_material=metadata_map.get('ANODE_MATERIAL'), - temp_celcius=get_float('TEMP_CELCIUS'), - measurement_date=cls.get_date_time(metadata_map.get('MEASURE_DATE'), metadata_map.get('MEASURE_TIME')) - ) - - return metadata - - @classmethod - def get_key_value_dict(cls, header_str: str) -> dict: - key_value_dict = {} - for key, value in cls.get_key_value_pairs(header_str): - key_value_dict[key] = value - return key_value_dict - - @staticmethod - def get_key_value_pairs(header_str: str) -> Iterator[Tuple[str, str]]: - commented_lines = [line for line in header_str.splitlines() if line.startswith('#')] - for line in commented_lines: - key_value = line[1:].split(':', 1) - if len(key_value) == 2: - yield key_value[0].strip(), key_value[1].strip() - - @staticmethod - def get_date_time(date_str: str, time_str: str) -> Optional[datetime]: - if date_str and time_str: - combined_str = date_str + ' ' + time_str - return datetime.strptime(combined_str, '%m/%d/%Y %H:%M:%S') - return None - - - def __eq__(self, other): - if not isinstance(other, Metadata): - return False - primary_match = self.primary == other.primary - secondary_match = self.secondary == other.secondary - ratio_match = self.ratio == other.ratio - anode_match = self.anode_material == other.anode_material - temp_match = self.temp_celcius == other.temp_celcius - date_match = self.measurement_date == other.measurement_date - - return anode_match and temp_match and date_match and primary_match and secondary_match and ratio_match \ No newline at end of file diff --git a/xrdpattern/core/pattern_info.py b/xrdpattern/core/pattern_data.py similarity index 69% rename from xrdpattern/core/pattern_info.py rename to xrdpattern/core/pattern_data.py index 46e00d4..08a0323 100644 --- a/xrdpattern/core/pattern_info.py +++ b/xrdpattern/core/pattern_data.py @@ -5,26 +5,22 @@ from holytools.abstract import JsonDataclass from dataclasses import dataclass, fields from typing import Optional -from .metadata import Metadata + +from xrdpattern.powder import PowderExperiment # ------------------------------------------- @dataclass -class PatternInfo(JsonDataclass): +class PatternData(JsonDataclass): two_theta_values : list[float] intensities : list[float] - metadata: Metadata + experiment : PowderExperiment name : Optional[str] = None - def get_wavelength(self, primary : bool = True) -> Optional[float]: - wavelength = self.metadata.primary if primary else self.metadata.secondary - return wavelength - - def set_wavelength(self, new_wavelength : float, primary : bool = True): - if primary: - self.metadata.primary = new_wavelength - else: - self.metadata.secondary = new_wavelength + @classmethod + def from_intensitiy_map(cls, two_theta_values: list[float], intensities: list[float]) -> PatternData: + metadata = PowderExperiment.make_empty() + return cls(two_theta_values=two_theta_values, intensities=intensities, experiment=metadata) def to_dict(self): return {f.name: getattr(self, f.name) for f in fields(self)} @@ -51,15 +47,14 @@ def get_standardized_map(self, start_val : float, stop_val : float, num_entries return std_angles,std_intensities - def __eq__(self, other : PatternInfo): - if not isinstance(other, PatternInfo): + def __eq__(self, other : PatternData): + if not isinstance(other, PatternData): return False angles_equal = self.two_theta_values == other.two_theta_values intensities_equal = self.intensities == other.intensities - metadata_equal = self.metadata == other.metadata - return angles_equal and intensities_equal and metadata_equal + return angles_equal and intensities_equal if __name__ == '__main__': diff --git a/xrdpattern/parsing/csv/csv_read.py b/xrdpattern/parsing/csv/csv_read.py index cc2522b..df90f01 100644 --- a/xrdpattern/parsing/csv/csv_read.py +++ b/xrdpattern/parsing/csv/csv_read.py @@ -5,7 +5,7 @@ from holytools.abstract import SelectableEnum -from xrdpattern.core import Metadata, PatternInfo +from xrdpattern.core import PatternData from .table_selector import TableSelector, TextTable, NumericalTable @@ -44,7 +44,7 @@ def as_matrix(self, fpath : str) -> NumericalTable: return TableSelector.get_numerical_subtable(table=table) - def read_csv(self, fpath: str) -> list[PatternInfo]: + def read_csv(self, fpath: str) -> list[PatternData]: matrix = self.as_matrix(fpath=fpath) x_axis_row = matrix.get_data(row=0) data_rows = [matrix.get_data(row=row) for row in range(1, matrix.get_row_count())] @@ -63,7 +63,7 @@ def read_csv(self, fpath: str) -> list[PatternInfo]: two_theta_degs = x_axis_row for intensities in data_rows: - new = PatternInfo(two_theta_values=two_theta_degs, intensities=intensities, metadata=Metadata.make_empty()) + new = PatternData.from_intensitiy_map(two_theta_values=two_theta_degs, intensities=intensities) pattern_infos.append(new) x_axis_type = 'QValues' if is_qvalues else 'TwoThetaDegs' diff --git a/xrdpattern/parsing/parser.py b/xrdpattern/parsing/parser.py index 88b48f5..256ffaa 100644 --- a/xrdpattern/parsing/parser.py +++ b/xrdpattern/parsing/parser.py @@ -4,10 +4,13 @@ from typing import Optional from dataclasses import dataclass from holytools.fsys import SaveManager -from xrdpattern.core import PatternInfo, Metadata +from xrdpattern.core import PatternData from .data_files import XrdFormat, Formats, get_xylib_repr from .csv import CsvParser, Orientation from xrdpattern.parsing.stoe import StoeReader +from ..powder import PowderExperiment + + # ------------------------------------------- @dataclass @@ -27,7 +30,7 @@ def __init__(self, parser_options : ParserOptions = ParserOptions()): # ------------------------------------------- # pattern - def get_pattern_info_list(self, fpath : str) -> list[PatternInfo]: + def get_pattern_info_list(self, fpath : str) -> list[PatternData]: suffix = SaveManager.get_suffix(fpath) if not suffix in Formats.get_allowed_suffixes(): raise ValueError(f"File {fpath} has unsupported format .{suffix}") @@ -47,26 +50,26 @@ def get_pattern_info_list(self, fpath : str) -> list[PatternInfo]: else: raise ValueError(f"Format .{the_format} is not supported") for pattern in pattern_infos: - if pattern.get_wavelength(primary=True) is None and self.default_wavelength_angstr: - pattern.set_wavelength(new_wavelength=self.default_wavelength_angstr) + if pattern.experiment.primary_wavelength is None and self.default_wavelength_angstr: + pattern.experiment.artifacts.primary_wavelength = self.default_wavelength_angstr for info in pattern_infos: info.name = os.path.basename(fpath) return pattern_infos @staticmethod - def from_json(fpath: str) -> PatternInfo: + def from_json(fpath: str) -> PatternData: with open(fpath, 'r') as file: data = file.read() - new_pattern = PatternInfo.from_str(json_str=data) + new_pattern = PatternData.from_str(json_str=data) return new_pattern @staticmethod - def from_data_file(fpath: str, format_hint : XrdFormat) -> PatternInfo: + def from_data_file(fpath: str, format_hint : XrdFormat) -> PatternData: xylib_repr = get_xylib_repr(fpath=fpath, format_hint=format_hint) header,data_str = xylib_repr.get_header(), xylib_repr.get_data() - metadata = Metadata.from_header_str(header_str=header) + metadata = PowderExperiment.from_xylib_header(header_str=header) angles, intensities= [], [] data_rows = [row for row in data_str.split('\n') if not row.strip() == ''] @@ -75,11 +78,11 @@ def from_data_file(fpath: str, format_hint : XrdFormat) -> PatternInfo: deg, intensity = float(deg_str), float(intensity_str) angles.append(deg) intensities.append(intensity) - return PatternInfo(two_theta_values=angles,intensities=intensities,metadata=metadata) + return PatternData(two_theta_values=angles, intensities=intensities, experiment=metadata) @staticmethod - def from_csv(fpath : str) -> list[PatternInfo]: + def from_csv(fpath : str) -> list[PatternData]: if CsvParser.has_two_columns(fpath=fpath): orientation = Orientation.VERTICAL else: diff --git a/xrdpattern/parsing/stoe/reader.py b/xrdpattern/parsing/stoe/reader.py index 9f16481..2286e98 100644 --- a/xrdpattern/parsing/stoe/reader.py +++ b/xrdpattern/parsing/stoe/reader.py @@ -1,5 +1,7 @@ -from xrdpattern.core import Metadata, PatternInfo +from xrdpattern.core import PatternData from .quantities import Quantity, FloatQuantity, IntegerQuantity +from ...powder import Artifacts, PowderExperiment + class BinaryReader: def read(self, fpath : str): @@ -35,16 +37,17 @@ def read(self, fpath : str): super().read_bytes(byte_content=byte_content) - def get_pattern_info(self, fpath : str) -> PatternInfo: + def get_pattern_info(self, fpath : str) -> PatternData: self.read(fpath=fpath) - metadata = Metadata(primary=self.primary_wavelength.get_value(), - secondary=self.secondary_wavelength.get_value(), - ratio=self.ratio.get_value()) + experiment = PowderExperiment.make_empty() + experiment.artifacts = Artifacts(primary_wavelength=self.primary_wavelength.get_value(), + secondary_wavelength=self.secondary_wavelength.get_value(), + secondary_to_primary=self.ratio.get_value()) two_theta_values = self._get_x_values() intensities = self._get_y_values() - return PatternInfo(metadata=metadata, two_theta_values=two_theta_values, intensities=intensities) + return PatternData(two_theta_values=two_theta_values, intensities=intensities, experiment=experiment) def _get_x_values(self) -> list[float]: diff --git a/xrdpattern/pattern/pattern.py b/xrdpattern/pattern/pattern.py index ee969fa..d39d47d 100644 --- a/xrdpattern/pattern/pattern.py +++ b/xrdpattern/pattern/pattern.py @@ -1,16 +1,20 @@ from __future__ import annotations -import matplotlib.pyplot as plt + import os -from uuid import uuid4 from typing import Optional -from holytools.fsys import SaveManager +from uuid import uuid4 + +import matplotlib.pyplot as plt +from holytools.fsys import SaveManager +from xrdpattern.core import PatternData from xrdpattern.parsing import Parser, ParserOptions -from xrdpattern.core import PatternInfo, Metadata from .pattern_report import PatternReport + + # ------------------------------------------- -class XrdPattern(PatternInfo): +class XrdPattern(PatternData): def plot(self, apply_standardization=True): plt.figure(figsize=(10, 6)) plt.ylabel('Intensity') @@ -52,11 +56,6 @@ def save(self, fpath : str, force_overwrite : bool = False): with open(fpath, 'w') as f: f.write(self.to_str()) - @classmethod - def from_intensitiy_map(cls, angles: list[float], intensities: list[float]) -> XrdPattern: - metadata = Metadata.make_empty() - return XrdPattern(two_theta_values=angles, intensities=intensities, metadata=metadata) - # ------------------------------------------- # get @@ -66,15 +65,11 @@ def get_parsing_report(self, datafile_fpath : str) -> PatternReport: pattern_health.add_critical('No data found. Degree over intensity is empty!') elif len(self.two_theta_values) < 10: pattern_health.add_critical('Data is too short. Less than 10 entries!') - if self.get_wavelength(primary=True) is None: - pattern_health.add_error('Primary wavelength missing!') - if self.get_wavelength(primary=False) is None: + if self.experiment.primary_wavelength is None: + pattern_health.add_error('Primary wavelength missing!') + if self.experiment.secondary_wavelength is None: pattern_health.add_warning('No secondary wavelength found') - if self.metadata.anode_material is None: - pattern_health.add_warning('No anode material found') - if self.metadata.measurement_date is None: - pattern_health.add_warning('No measurement datetime found') return pattern_health diff --git a/xrdpattern/powder/experiment/experiment.py b/xrdpattern/powder/experiment/experiment.py index 6d2e8dc..e023822 100644 --- a/xrdpattern/powder/experiment/experiment.py +++ b/xrdpattern/powder/experiment/experiment.py @@ -1,6 +1,8 @@ from __future__ import annotations from dataclasses import dataclass +from typing import Iterator, Tuple, Optional + from torch import Tensor from xrdpattern.powder.structure import CrystalStructure, CrystalBase, AtomicSite @@ -65,6 +67,41 @@ def add_region_quantity(self, list_obj : list) -> QuantityRegion: return region + @classmethod + def from_xylib_header(cls, header_str: str) -> PowderExperiment: + metadata_map = cls.get_key_value_dict(header_str=header_str) + + def get_float(key: str) -> Optional[float]: + val = metadata_map.get(key) + if val: + val = float(val) + return val + + experiment = cls.make_empty() + experiment.artifacts = Artifacts( + primary_wavelength=get_float('ALPHA1'), + secondary_wavelength=get_float('ALPHA2'), + secondary_to_primary=get_float('ALPHA_RATIO') + ) + experiment.powder.temp_in_kelvin = get_float('TEMP_CELCIUS') + 273.15 + + return experiment + + @classmethod + def get_key_value_dict(cls, header_str: str) -> dict: + key_value_dict = {} + for key, value in cls.get_key_value_pairs(header_str): + key_value_dict[key] = value + return key_value_dict + + @staticmethod + def get_key_value_pairs(header_str: str) -> Iterator[Tuple[str, str]]: + commented_lines = [line for line in header_str.splitlines() if line.startswith('#')] + for line in commented_lines: + key_value = line[1:].split(':', 1) + if len(key_value) == 2: + yield key_value[0].strip(), key_value[1].strip() + @staticmethod def get_padded_base(base: CrystalBase, nan_padding : bool) -> CrystalBase: if len(base) > NUM_ATOMIC_SITES: @@ -134,10 +171,6 @@ def crystallite_size(self) -> float: def temp_in_celcius(self) -> float: return self.powder.temp_in_kelvin - @property - def primary_wavelength(self) -> float: - return self.artifacts.primary_wavelength - @property def crystal_structure(self) -> CrystalStructure: return self.powder.crystal_structure @@ -146,6 +179,14 @@ def crystal_structure(self) -> CrystalStructure: def domain(self) -> bool: return self.is_simulated + @property + def primary_wavelength(self) -> float: + return self.artifacts.primary_wavelength + + @property + def secondary_wavelength(self) -> float: + return self.artifacts.secondary_wavelength + import torch