Skip to content

Commit

Permalink
parse data analysis in Casa from main file and export files
Browse files Browse the repository at this point in the history
lukaspie committed Nov 21, 2024
1 parent 0e76bf2 commit 7f7f0c4
Showing 12 changed files with 2,073 additions and 1,801 deletions.
281 changes: 0 additions & 281 deletions src/pynxtools_xps/config/config_txt_vamas_export.json

This file was deleted.

156 changes: 120 additions & 36 deletions src/pynxtools_xps/config/config_vms.json
Original file line number Diff line number Diff line change
@@ -2,16 +2,18 @@
"/ENTRY":{
"@default":"data",
"title":"@eln",
"start_time":"@attrs:time_stamp",
"start_time":"['@attrs:time_stamp', '@eln']",
"end_time":"@eln",
"experiment_institution":"@attrs:institute_id",
"experiment_identifier":"@attrs:experiment_id",
"experiment_identifier":{
"identifier":"['@attrs:experiment_id', '@eln']"
},
"experiment_facility":"@eln",
"experiment_laboratory":"@eln",
"entry_identifier":"@eln",
"duration":null,
"duration":"@eln",
"duration/@units":"s",
"method":"@attrs:region/analysis_method",
"method":"['@attrs:analysis_method', '@eln']",
"program_name":"@eln"
},
"/ENTRY/geometries":{
@@ -27,7 +29,7 @@
"z":[0, 0 , 1],
"z/@units":"m",
"coordinate_system_transformations":{
"AXISNAME[z_rotation]":"@attrs:source_azimuth_angle",
"AXISNAME[z_rotation]":"!@attrs:source_azimuth_angle",
"AXISNAME[z_rotation]/@units":"@attrs:source_azimuth_angle/@units",
"AXISNAME[z_rotation]/@transformation_type":"rotation",
"AXISNAME[z_rotation]/@vector":[0, 0, 1],
@@ -39,7 +41,7 @@
"AXISNAME[y_flip]/@vector":[0, 1, 0],
"AXISNAME[y_flip]/@depends_on":"y_rotation",

"AXISNAME[y_rotation]":"@attrs:source_polar_angle",
"AXISNAME[y_rotation]":"!@attrs:source_polar_angle",
"AXISNAME[y_rotation]/@units":"@attrs:source_polar_angle/@units",
"AXISNAME[y_rotation]/@transformation_type":"rotation",
"AXISNAME[y_rotation]/@vector":[0, -1, 0],
@@ -72,12 +74,12 @@
"energy_resolution":{
"physical_quantity":"energy",
"type":"derived",
"resolution":"@attrs:step_size",
"resolution/@units":"@attrs:step_size/@units"
"resolution":"!['@attrs:step_size', '@eln']",
"resolution/@units":"['@attrs:step_size@units', '@eln']"
},
"sourceTYPE[source_xray]":{
"type":"@eln",
"name":"@attrs:source_label",
"name":"['@attrs:source_label', '@eln']",
"probe":"photon",
"device_information":{
"vendor":"@eln",
@@ -95,22 +97,22 @@
"distance/@units":"@eln",
"extent":"@attrs:extent",
"extent/@units":"@attrs:extent/@units",
"incident_energy":"@attrs:excitation_energy",
"incident_energy/@units":"eV",
"incident_energy":"['@attrs:excitation_energy', '@attrs:characteristic_energy']",
"incident_energy/@units":"['@attrs:excitation_energy/@units', '@attrs:characteristic_energy/@units', 'eV']",
"incident_energy_spread":null,
"incident_energy_spread/@units":null,
"incident_polarization":null,
"incident_polarization/@units":null,
"associated_source":"/entry/instrument/source_xray",
"depends_on":"/entry/instrument/beam_xray/transformations/beam_polar_angle_of_incidence",
"transformations":{
"beam_polar_angle_of_incidence":"@attrs:source_polar_angle",
"beam_polar_angle_of_incidence":"!@attrs:source_polar_angle",
"beam_polar_angle_of_incidence/@units":"@attrs:source_polar_angle/@units",
"beam_polar_angle_of_incidence/@transformation_type":"rotation",
"beam_polar_angle_of_incidence/@vector":[0, -1, 0],
"beam_polar_angle_of_incidence/@depends_on":"beam_azimuth_angle",

"beam_azimuth_angle":"@attrs:source_azimuth_angle",
"beam_azimuth_angle":"!@attrs:source_azimuth_angle",
"beam_azimuth_angle/@units":"@attrs:source_azimuth_angle/@units",
"beam_azimuth_angle/@transformation_type":"rotation",
"beam_azimuth_angle/@vector":[0, 0, -1],
@@ -121,7 +123,7 @@
"@default":"detector",
"name":"@eln",
"description":"@eln",
"work_function":"@attrs:work_function",
"work_function":"['@attrs:work_function', '@eln']",
"work_function/@units":"eV",
"fast_axes":null,
"slow_axes":"energy",
@@ -139,7 +141,7 @@
"energy_resolution":{
"physical_quantity":"energy",
"type":"estimated",
"resolution":"@attrs:step_size",
"resolution":"!@attrs:step_size",
"resolution/@units":"eV"
},
"transmission_function":null,
@@ -164,9 +166,9 @@
},
"ENERGYDISPERSION[energydispersion]":{
"scheme":"@eln",
"pass_energy":"@attrs:pass_energy",
"pass_energy/@units":"@attrs:pass_energy/@units",
"energy_scan_mode":"@attrs:scan_mode",
"pass_energy":"['@attrs:pass_energy', '@eln']",
"pass_energy/@units":"['@attrs:pass_energy/@units', '@eln']",
"energy_scan_mode":"['@attrs:scan_mode', '@eln']",
"radius":"@eln",
"radius/@units":"@eln",
"device_information":{
@@ -185,8 +187,8 @@
"detector_type":"@eln",
"detector_voltage":"@eln",
"detector_voltage/@units":"@eln",
"count_time":"@attrs:dwell_time",
"count_time/@units":"@attrs:dwell_time/@units",
"count_time":"['@attrs:dwell_time', '@attrs:acquisition_time']",
"count_time/@units":"['@attrs:dwell_time/@units', '@attrs:acquisition_time/@units']",
"acquisition_mode":"@attrs:signal_mode",
"device_information":{
"vendor":"@eln",
@@ -207,13 +209,13 @@
},
"depends_on":"/entry/instrument/electronanalyser/transformations/analyser_take_off_polar_angle",
"transformations":{
"analyser_take_off_polar_angle":"@attrs:analyser_take_off_polar_angle",
"analyser_take_off_polar_angle":"!@attrs:analyser_take_off_polar_angle",
"analyser_take_off_polar_angle/@units":"@attrs:analyser_take_off_polar_angle/@units",
"analyser_take_off_polar_angle/@transformation_type":"rotation",
"analyser_take_off_polar_angle/@vector":[0, -1, 0],
"analyser_take_off_polar_angle/@depends_on":"analyser_take_off_azimuth_angle",

"analyser_take_off_azimuth_angle":"@attrs:analyser_take_off_azimuth_angle",
"analyser_take_off_azimuth_angle":"!@attrs:analyser_take_off_azimuth_angle",
"analyser_take_off_azimuth_angle/@units":"@attrs:analyser_take_off_azimuth_angle/@units",
"analyser_take_off_azimuth_angle/@transformation_type":"rotation",
"analyser_take_off_azimuth_angle/@vector":[0, 0, -1],
@@ -307,8 +309,90 @@
}
}
},
"/ENTRY/FIT[fit]":{
"@default":"data",
"label":"!@attrs:fit_label",
"data":{
"@axes":["input_independent"],
"@signal":"input_dependent",
"@auxiliary_signals":["envelope"],
"input_independent":"@link:/entry/data/energy",
"input_dependent":"@link:/entry/data/data",
"envelope":"['@attrs:fit_envelope_cps/data', '@attrs:fit_envelope_cps/data']",
"envelope/@units":"counts_per_second",
"residual": null,
"residual/@units":"counts_per_second"
},
"peakPEAK[peak*]":{
"@default":"data",
"label":"@attrs:component*/name",
"data":{
"@axes":["position"],
"@signal":"intensity",
"intensity":"['@attrs:component*/data_cps', '@attrs:component*/data']",
"intensity/@units":"counts_per_second",
"position":"@data:energy",
"position/@units":"@attrs:energy_units"
},
"function":{
"description":"@attrs:component*/lineshape",
"formula":"@attrs:component*/formula",
"position":{
"value":"@attrs:component*/position",
"value/@units":"@attrs:component*/position/@units",
"min_value":"@attrs:component*/position_min",
"min_value/@units":"@attrs:component*/position_min/@units",
"max_value":"@attrs:component*/position_max",
"max_value/@units":"@attrs:component*/position_max/@units"
},
"width":{
"value":"@attrs:component*/width",
"value/@units":"@attrs:component*/width/@units",
"min_value":"@attrs:component*/width_min",
"min_value/@units":"@attrs:component*/width_min/@units",
"max_value":"@attrs:component*/width_max",
"max_value/@units":"@attrs:component*/width_max/@units"
},
"area":{
"value":"@attrs:component*/area",
"min_value":"@attrs:component*/area_min",
"max_value":"@attrs:component*/area_max"
}
},
"total_area":"@attrs:component*/area",
"relative_sensitivity_factor":"@attrs:component*/rsf",
"relative_atomic_concentration":"@attrs:component*/atomic_concentration"
},
"backgroundBACKGROUND[background*]":{
"@default":"data",
"label":"@attrs:region*/name",
"data":{
"@axes":["position"],
"@signal":"intensity",
"intensity":"['@attrs:background_intensity/data_cps', '@attrs:background_intensity/data', '@attrs:region*/data_cps', '@attrs:region*/data']",
"intensity/@units":"counts_per_second",
"position":"@data:energy",
"position/@units":"@attrs:energy_units"
},
"function":{
"description":"@attrs:region*/bg_type",
"formula":"@attrs:region*/formula"
}
},
"error_function":{
"description": null,
"formula": null
},
"global_fit_function":{
"description": null,
"formula": null
},
"figure_of_meritMETRIC[figure_of_merit]": null,
"figure_of_meritMETRIC[figure_of_merit]/@metric": null,
"figure_of_meritMETRIC[figure_of_merit]/@units": null
},
"/ENTRY/SAMPLE[sample]":{
"name":"@attrs:sample_name",
"name":"['@attrs:sample_name', '@eln']",
"identifier":{
"identifier":"@eln"
},
@@ -330,22 +414,22 @@
}
},
"temperature_env":{
"temperature_sensor":"@link:/entry/instrument/manipulator/temperature_sensor",
"sample_heater":"@link:/entry/instrument/manipulator/sample_heater",
"temperature_sensor":"!@link:/entry/instrument/manipulator/temperature_sensor",
"sample_heater":"!@link:/entry/instrument/manipulator/sample_heater",
"cryostat":null
},
"gas_pressure_env":{
"pressure_gauge":"@link:/entry/instrument/pressure_gauge"
"pressure_gauge":"!@link:/entry/instrument/pressure_gauge"
},
"bias_env":{
"potentiostat":"@link:/entry/instrument/manipulator/sample_bias_potentiostat",
"voltmeter":"@link:/entry/instrument/manipulator/sample_bias_voltmeter"
"potentiostat":"!@link:/entry/instrument/manipulator/sample_bias_potentiostat",
"voltmeter":"!@link:/entry/instrument/manipulator/sample_bias_voltmeter"
},
"drain_current_env":{
"amperemeter":"@link:/entry/instrument/manipulator/drain_current_amperemeter"
"amperemeter":"!@link:/entry/instrument/manipulator/drain_current_amperemeter"
},
"flood_gun_current_env":{
"flood_gun":"@link:/entry/instrument/flood_gun"
"flood_gun":"!@link:/entry/instrument/flood_gun"
},
"depends_on":"/entry/sample/transformations/sample_rotation_angle",
"transformations":{
@@ -355,14 +439,14 @@
"sample_rotation_angle/@vector":[0, 0, -1],
"sample_rotation_angle/@depends_on":"sample_normal_tilt_azimuth_angle",

"sample_normal_polar_angle_of_tilt":"@attrs:sample_normal_polar_angle_of_tilt",
"sample_normal_polar_angle_of_tilt":"!@attrs:sample_normal_polar_angle_of_tilt",
"sample_normal_polar_angle_of_tilt/@units":"@attrs:sample_normal_polar_angle_of_tilt/@units",
"sample_normal_polar_angle_of_tilt/@transformation_type":"rotation",
"sample_normal_polar_angle_of_tilt/@vector":[0, -1, 0],
"sample_normal_polar_angle_of_tilt/@depends_on":"sample_normal_tilt_azimuth_angle",

"sample_normal_tilt_azimuth_angle":"@attrs:sample/sample_normal_tilt_azimuth_angle",
"sample_normal_tilt_azimuth_angle/@units":"@attrs:sample/sample_normal_tilt_azimuth_angle/@units",
"sample_normal_tilt_azimuth_angle":"!@attrs:sample_normal_tilt_azimuth_angle",
"sample_normal_tilt_azimuth_angle/@units":"@attrs:sample_normal_tilt_azimuth_angle/@units",
"sample_normal_tilt_azimuth_angle/@transformation_type":"rotation",
"sample_normal_tilt_azimuth_angle/@vector":[0, 0, -1],
"sample_normal_tilt_azimuth_angle/@depends_on":"/entry/geometries/xps_coordinate_system/coordinate_transformations/z_rotation"
@@ -373,12 +457,12 @@
"@signal":"data",
"data":"@data:average",
"data_errors":"@data:errors",
"data/@units":"@attrs:y_units_1",
"data/@units":"['@attrs:y_units_1','@attrs:y_units', 'counts_per_second']",
"DATA[*]":"@data:*.scans",
"DATA[*]/@units":"@attrs:y_units_1",
"DATA[*]/@units":"['@attrs:y_units_1','@attrs:y_units', 'counts_per_second']",
"energy":"@data:energy",
"energy/@type":"@attrs:energy_label",
"energy/@units":"@attrs:energy_units",
"energy/@units":"['@attrs:energy_units', 'eV']",
"energy/@reference":null,
"@energy_indices":0
}
36 changes: 18 additions & 18 deletions src/pynxtools_xps/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
#
# Copyright The NOMAD Authors.
#
# This file is part of NOMAD. See https://nomad-lab.eu for further info.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
#
# Copyright The NOMAD Authors.
#
# This file is part of NOMAD. See https://nomad-lab.eu for further info.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

742 changes: 413 additions & 329 deletions src/pynxtools_xps/models/backgrounds.py

Large diffs are not rendered by default.

924 changes: 454 additions & 470 deletions src/pynxtools_xps/models/lineshapes.py

Large diffs are not rendered by default.

156 changes: 111 additions & 45 deletions src/pynxtools_xps/reader.py
Original file line number Diff line number Diff line change
@@ -42,7 +42,7 @@
from pynxtools_xps.specs.sle.sle_specs import SleMapperSpecs
from pynxtools_xps.specs.xml.xml_specs import XmlMapperSpecs
from pynxtools_xps.specs.xy.xy_specs import XyMapperSpecs
from pynxtools_xps.vms.txt_vamas_export import TxtMapperVamasExport
from pynxtools_xps.vms.vamas_export import TxtMapperVamasExport, CsvMapperVamasResult
from pynxtools_xps.vms.vamas import VamasMapper

logger = logging.getLogger(__name__)
@@ -109,6 +109,29 @@ def concatenate_values(value1, value2):
return concatenated


def _check_multiple_extensions(file_paths: Tuple[str] = None) -> bool:
"""
Determines if a list of file paths contains more than one unique file extension.
This method accepts a list of file paths (as strings or `Path` objects) and checks
if there are multiple unique file extensions present in the list. A file extension
is identified as the substring after the last period (`.`) in the file name.
Parameters:
file_paths (Tuple[str]): A tuple of file paths, which can be strings or
`Path` objects. Defaults to None.
Returns:
bool: True if more than one unique file extension is found, False otherwise.
Raises:
TypeError: If `file_paths` is not a tuple of strings or `Path` objects.
"""
extensions = {str(path).split(".")[-1] for path in file_paths if "." in str(path)}

return len(extensions) > 1


# pylint: disable=too-few-public-methods
class XPSReader(MultiFormatReader):
"""Reader for XPS."""
@@ -130,13 +153,17 @@ class XPSReader(MultiFormatReader):
".spe",
".sle",
".slh",
".txt",
".vms",
".xml",
".xy",
".txt", # This is last because of the processing_order
]

__prmt_metadata_file_ext__ = {".csv": ".txt"}

__vendors__ = ["kratos", "phi", "scienta", "specs", "unkwown"]
__prmt_vndr_cls: Dict[str, Dict] = {
".csv": {"unknown": CsvMapperVamasResult},
".ibw": {"scienta": MapperScienta},
".npl": {"unkwown": VamasMapper},
".pro": {"phi": MapperPhi},
@@ -153,7 +180,7 @@ class XPSReader(MultiFormatReader):

__file_err_msg__ = (
"Need an XPS data file with one of the following extensions: "
f"{__prmt_file_ext__}"
f"data files: {__prmt_file_ext__}, metadata files: {__prmt_metadata_file_ext__}."
)

__vndr_err_msg__ = (
@@ -173,7 +200,15 @@ def __init__(self, *args, **kwargs):
".json": self.set_config_file,
}

for ext in XPSReader.__prmt_file_ext__:
self.processing_order = (
XPSReader.__prmt_file_ext__
+ list(XPSReader.__prmt_metadata_file_ext__.keys())
+ list(self.extensions.keys())
)

for ext in XPSReader.__prmt_file_ext__ + list(
XPSReader.__prmt_metadata_file_ext__.keys()
):
self.extensions[ext] = self.handle_data_file

def set_config_file(self, file_path: str) -> Dict[str, Any]:
@@ -266,7 +301,7 @@ def _check_for_vendors(file_path: str) -> str:
return list(vendor_dict.keys())[0]
if file_ext == ".txt":
return _check_for_vendors_txt(file_path)
return None
raise ValueError(XPSReader.__vndr_err_msg__)

def _check_for_vendors_txt(file_path: str) -> str:
"""
@@ -294,7 +329,7 @@ def _check_for_vendors_txt(file_path: str) -> str:
if any(vendor_opt in contents for vendor_opt in vendor_options):
return vendor
if contents[:6] == "[Info]":
# This is for picking the Scienta reader is "scienta"
# This is for picking the Scienta reader if "scienta"
# is not in the file
return vendor
return "unknown"
@@ -303,23 +338,29 @@ def _check_for_vendors_txt(file_path: str) -> str:

if file_ext in XPSReader.__prmt_file_ext__:
vendor = _check_for_vendors(file_path)
try:
parser = XPSReader.__prmt_vndr_cls[file_ext][vendor]()

parser.parse_file(file_path, **self.kwargs)
self.config_file = XPSReader.reader_dir.joinpath(
"config", parser.config_file
)
data_dict = parser.data_dict

except ValueError as val_err:
raise ValueError(XPSReader.__vndr_err_msg__) from val_err
except KeyError as key_err:
raise KeyError(XPSReader.__vndr_err_msg__) from key_err
else:
raise ValueError(XPSReader.__file_err_msg__)

self.xps_data_dicts += [data_dict]
parser = XPSReader.__prmt_vndr_cls[file_ext][vendor]()
parser.parse_file(file_path, **self.kwargs)
data_dict = parser.data_dict

self.config_file = XPSReader.reader_dir.joinpath(
"config", parser.config_file
)
self.xps_data_dicts += [data_dict]

elif file_ext in XPSReader.__prmt_metadata_file_ext__:
vendor = _check_for_vendors(file_path)

metadata_parser = XPSReader.__prmt_vndr_cls[file_ext][vendor]()
metadata_parser.parse_file(file_path, **self.kwargs)

main_file_ext = XPSReader.__prmt_metadata_file_ext__[file_ext]

main_file_dicts = [
d for d in self.xps_data_dicts if d.get("file_ext") == main_file_ext
]

metadata_parser.update_main_file_dict(main_file_dicts)

return {}

@@ -422,23 +463,28 @@ def check_for_same_entries(
common_entries, dict_indices = check_for_same_entries(self.xps_data_dicts)

if common_entries:
for entry, indices in zip(common_entries, dict_indices):
dicts_with_common_entries = [self.xps_data_dicts[i] for i in indices]

for i, data_dict in enumerate(dicts_with_common_entries):
for key, value in data_dict.copy().items():
new_key = key.replace(f"/ENTRY[{entry}]", f"/ENTRY[{entry}{i}]")
if key == "data":
for entry_name, xarr in value.copy().items():
if entry_name == entry:
new_entry_name = entry_name.replace(
f"{entry}", f"{entry}{i}"
)
value[new_entry_name] = xarr
del value[entry_name]
if new_key != key:
data_dict[new_key] = value
del data_dict[key]
if not self.overwrite_keys:
for entry, indices in zip(common_entries, dict_indices):
dicts_with_common_entries = [
self.xps_data_dicts[i] for i in indices
]

for i, data_dict in enumerate(dicts_with_common_entries):
for key, value in data_dict.copy().items():
new_key = key.replace(
f"/ENTRY[{entry}]", f"/ENTRY[{entry}{i}]"
)
if key == "data":
for entry_name, xarr in value.copy().items():
if entry_name == entry:
new_entry_name = entry_name.replace(
f"{entry}", f"{entry}{i}"
)
value[new_entry_name] = xarr
del value[entry_name]
if new_key != key:
data_dict[new_key] = value
del data_dict[key]

for data_dict in self.xps_data_dicts:
# If there are multiple input data files of the same type,
@@ -449,8 +495,9 @@ def check_for_same_entries(
]

self.xps_data = {**self.xps_data, **data_dict}
for key, value1, value2 in existing:
self.xps_data[key] = concatenate_values(value1, value2)
if not self.overwrite_keys:
for key, value1, value2 in existing:
self.xps_data[key] = concatenate_values(value1, value2)

def _get_analyser_names(self) -> List[str]:
"""
@@ -627,11 +674,28 @@ def get_signals(key: str) -> List[str]:

return list(map(str, data_vars))

if path.startswith("@data:*"):
return get_signals(key=path.split(":*.")[-1])
def get_processes(process_key: str) -> List[str]:
# pattern = re.compile(rf"/ENTRY\[{self.callbacks.entry_name}]/({process_key}\d+)/")
pattern = re.compile(
rf"/ENTRY\[{self.callbacks.entry_name}]\.*/{process_key}([a-zA-Z0-9_]+)"
)

process_names = {
match for key in self.xps_data for match in pattern.findall(key)
}

return sorted(process_names)

patterns: Dict[str, Any] = {
r"data/DATA": lambda: get_signals(path.split(":*.")[-1]),
r"DETECTOR\[[a-zA-Z0-9_]+\]/raw_data": lambda: get_signals("channels"),
"peak": lambda: get_processes("component"),
"background": lambda: get_processes("region"),
}

if any(x in path for x in ["counts", "raw/@units"]):
return get_signals(key="channels")
for pattern, func in patterns.items():
if re.search(pattern, key):
return func()

return get_signals(key="scans")

@@ -711,6 +775,8 @@ def read(
objects: Tuple[Any] = None,
**kwargs,
) -> dict:
self.overwrite_keys = _check_multiple_extensions(file_paths)

template = super().read(template, file_paths, objects, suppress_warning=True)
self.set_root_default(template)

7 changes: 4 additions & 3 deletions src/pynxtools_xps/reader_utils.py
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@
Helper functions for populating NXmpes template
"""

import os
import logging
import re
from abc import ABC, abstractmethod
@@ -62,7 +63,6 @@ def __init__(self):
self.file: Union[str, Path] = ""
self.raw_data: List[str] = []
self._xps_dict: Dict[str, Any] = {}
self._root_path = ""

self.parser = None

@@ -93,8 +93,8 @@ def parse_file(self, file, **kwargs):
self.parser = self._select_parser()
self.raw_data = self.parser.parse_file(file, **kwargs)

file_key = f"{self._root_path}/File"
self._xps_dict[file_key] = file
self._xps_dict["File"] = file
self._xps_dict["file_ext"] = os.path.splitext(file)[1]

self.construct_data()

@@ -437,6 +437,7 @@ def align_name_part(name_part: str):
"-": "_",
":": "_",
"+": "_",
"/": "_",
}
)

87 changes: 65 additions & 22 deletions src/pynxtools_xps/vms/casa_data_model.py
Original file line number Diff line number Diff line change
@@ -39,10 +39,13 @@
from pynxtools_xps.models.backgrounds import (
LinearBackground,
Shirley,
StepUp,
StepDown,
TougaardU3,
TougaardU4,
)


logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

@@ -114,21 +117,23 @@ def process_comments(self, comment_list: List[str]):

def flatten_metadata(self):
# Write process data
process_key_map: Dict[str, List[str]] = {
"process": ["energy_calibrations", "intensity_calibrations", "smoothings"],
"peak_fitting": ["regions", "components"],
}
process_keys: List[str] = [
"energy_calibrations",
"intensity_calibrations",
"smoothings",
"regions",
"components",
]

flattened_dict: Dict[str, Any] = {}

for grouping, process_key_list in process_key_map.items():
for spectrum_key in process_key_list:
processes = self.casa_data[spectrum_key]
for i, process in enumerate(processes):
process_key = f"{spectrum_key}/{spectrum_key.rstrip('s')}{i}"
for key, value in process.dict().items():
key = key.replace("_units", "/@units")
flattened_dict[f"{process_key}/{key}"] = value
for process_key in process_keys:
processes = self.casa_data[process_key]
for i, process in enumerate(processes):
spectrum_key = f"{process_key.rstrip('s')}{i}"
for key, value in process.dict().items():
key = key.replace("_units", "/@units")
flattened_dict[f"{spectrum_key}/{key}"] = value

return flattened_dict

@@ -329,9 +334,19 @@ class CasaEnergyCalibration(XpsDataclass):
measured_energies_units: str = "eV"
aligned_energy: float = 0.0
aligned_energy_units: str = "eV"
operation: str = "eV"
operation: str = "ADD"
range_calibration: bool = False

def apply_energy_shift(self, x: float):
if self.operation == "addition" and self.energy_type == "binding":
return x - self.energy_offset

elif self.operation == "addition" and self.energy_type == "kinetic":
return x + self.energy_offset

if self.range_calibration:
pass # ToDo: apply range calibration


@dataclass
class CasaIntensityCalibration(XpsDataclass):
@@ -356,31 +371,57 @@ class CasaRegion(XpsDataclass):
av_width: float = 0.0
av_width_units: str = "eV"
start_offset: float = 0.0
start_offset_units: str = "counts_per_second"
start_offset_units: str = ""
end_offset: float = 0.0
end_offset_units: str = "counts_per_second"
end_offset_units: str = ""
cross_section: list = field(default_factory=list)
tag: str = ""
unknown_0: float = 0.0
unknown_1: float = 0.0
rsf_effective: float = 0.0

def calculate_background(self, x: np.array, y: np.array):
def calculate_background(self, x: np.ndarray, y: np.ndarray):
backgrounds: Dict[str, Any] = {
"Linear": LinearBackground,
"Shirley": Shirley,
# "Step Down": StepDown,
"Step Up": StepUp,
"Step Down": StepDown,
"U 3 Tougaard": TougaardU3,
"U 4 Tougaard": TougaardU4,
}

leading_letters, background_parameters = split_after_letters(self.bg_type)
leading_letters, _ = split_after_letters(self.bg_type)
background_params = [float(param) for param in self.cross_section]

try:
background_class = backgrounds[leading_letters]

background = background_class(*background_parameters)
try:
background = background_class(*background_params)
except TypeError:
background = background_class()

if self.end > self.start:
min_x = self.start
max_x = self.end
else:
min_x = self.end
max_x = self.start

region = np.argwhere((x >= min_x) & (x <= max_x))
fit_region = slice(region[0, 0], region[-1, 0], 1)

self.start_offset = 100

y_start_offset = y[0] * (self.start_offset / 100.0)
y_end_offset = y[-1] * (self.end_offset / 100.0)
y[0] -= y_start_offset
y[-1] -= y_end_offset

x, y = x[fit_region], y[fit_region]

self.data = background.calc_background(x, y)

self.lineshape = background.calc_background(x, y)
self.formula = background.formula()
self.description = str(background)

@@ -428,7 +469,9 @@ class CasaComponent(XpsDataclass):
tag: str = ""
const: str = "" # CONST

def calculate_lineshape(self, x: np.array):
atomic_concentration: float = 0.0

def calculate_lineshape(self, x: np.ndarray):
lineshapes: Dict[str, Any] = {
"GL": GaussianLorentzianProduct,
"SGL": GaussianLorentzianSum,
@@ -446,7 +489,7 @@ def calculate_lineshape(self, x: np.array):

peak = peak_class(*peak_parameters)

self.lineshape = peak.calc_lineshape(x)
self.data = peak.calc_lineshape(x)
self.formula = peak.formula()
self.description = str(peak)

594 changes: 0 additions & 594 deletions src/pynxtools_xps/vms/txt_vamas_export.py

This file was deleted.

44 changes: 41 additions & 3 deletions src/pynxtools_xps/vms/vamas.py
Original file line number Diff line number Diff line change
@@ -134,6 +134,12 @@ class VamasMapper(XPSMapper):

config_file = "config_vms.json"

def __init__(self):
self.multiple_spectra_groups: bool = True
self.same_spectrum_names: bool = False

super().__init__()

def _select_parser(self):
"""
Select parser based on the structure of the Vamas file,
@@ -149,11 +155,30 @@ def _select_parser(self):

def construct_data(self):
"""Map VMS format to NXmpes-ready dict."""
# pylint: disable=duplicate-code

def has_duplicate_spectrum_type(spectra: list[dict]) -> bool:
"""
Check if any two or more spectra in the list have the same 'spectrum_type'.
"""
seen = set()
return any(
spectrum.get("spectrum_type") in seen
or seen.add(spectrum["spectrum_type"])
for spectrum in spectra
if spectrum.get("spectrum_type")
)

spectra = deepcopy(self.raw_data)

self._xps_dict["data"]: Dict[str, Any] = {}

if len({spectrum.get("group_name") for spectrum in spectra}) == 1:
self.multiple_spectra_groups = False

if not self.multiple_spectra_groups:
if has_duplicate_spectrum_type(spectra):
self.same_spectrum_names = True

for spectrum in spectra:
self._update_xps_dict_with_spectrum(spectrum)

@@ -163,12 +188,17 @@ def _update_xps_dict_with_spectrum(self, spectrum: Dict[str, Any]):
"""

entry_parts = []
for part in ["group_name", "spectrum_type"]:

parts_to_use = ["group_name"] * bool(self.multiple_spectra_groups) + [
"spectrum_type"
]

for part in parts_to_use:
val = spectrum.get(part, None)
if val:
entry_parts += [val]

if len(entry_parts) == 1:
if len(entry_parts) == 1 and self.same_spectrum_names:
entry_parts += [spectrum["time_stamp"]]

entry = construct_entry_name(entry_parts)
@@ -745,14 +775,22 @@ def build_list(self):
if "casa" in comment_dict:
casa_process = comment_dict["casa"]

for energy_calibration in casa_process.casa_data["energy_calibrations"]:
block.x = energy_calibration.apply_energy_shift(block.x)

for region in casa_process.casa_data["regions"]:
region.calculate_background(block.x, block.y)
region.data_cps = region.data / block.dwell_time

for component in casa_process.casa_data["components"]:
component.calculate_lineshape(block.x)
component.data_cps = component.data / block.dwell_time

flattened_casa_data = casa_process.flatten_metadata()

if casa_process.casa_data["components"]:
flattened_casa_data["fit_label"] = spectrum_type

comment_dict.update(flattened_casa_data)
del comment_dict["casa"]

11 changes: 11 additions & 0 deletions src/pynxtools_xps/vms/vamas_data_model.py
Original file line number Diff line number Diff line change
@@ -143,6 +143,17 @@ class VamasBlock(XpsDataclass):
num_ord_values: int = 0
future_upgrade_block_entries: list = field(default_factory=list)

def convert_to_binding_energy_scale(self):
"""
Convert from kinetic to binding energy.
ToDo: check that components are also converted.
"""
self.abscissa_label = "binding energy"

self.x = self.source_energy - self.x
self.abscissa_start = float(min(self.x))


@dataclass
class ExpVariable(XpsDataclass):
836 changes: 836 additions & 0 deletions src/pynxtools_xps/vms/vamas_export.py

Large diffs are not rendered by default.

0 comments on commit 7f7f0c4

Please sign in to comment.