Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Elab metadata integration #70

Merged
merged 48 commits into from
Apr 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
5e2e332
add logging, elabftw metadata retrieval, and angle and offset sliders…
rettigl Jan 14, 2025
1eb138f
keep 0-valued entries
rettigl Jan 14, 2025
165ace4
add requirements
rettigl Jan 15, 2025
0f70b66
fix tests
rettigl Jan 15, 2025
e5746fd
remove pump laser beam section if pump closed
rettigl Jan 16, 2025
0f3101c
use default user config path
rettigl Jan 16, 2025
ae9b44e
use config_v1.yaml
rettigl Jan 16, 2025
0896ce7
add logging and delay as metadata
rettigl Jan 17, 2025
a942aee
remove typing
rettigl Jan 17, 2025
b36c20b
add tests
rettigl Jan 20, 2025
24e6bc4
remove call logger
rettigl Jan 20, 2025
0ebee0e
small fixes
rettigl Jan 22, 2025
a0bf9d2
reset metadata and config
rettigl Jan 22, 2025
b9478cf
fix tests
rettigl Jan 22, 2025
38513cb
add metadata support for pump2
rettigl Jan 23, 2025
1050b1d
return as float32 to save memory
rettigl Jan 26, 2025
7d57d01
add further tests and fixes
rettigl Jan 28, 2025
d5858d5
rename metadata tests
rettigl Jan 28, 2025
29ce768
fix typo
rettigl Jan 28, 2025
c9a8855
remove coverage restrictions
rettigl Jan 28, 2025
af1191b
fix matplotlib warnings
rettigl Jan 29, 2025
e3871b3
fix time-zone warning
rettigl Jan 29, 2025
20d53bd
properly remove contours
rettigl Feb 3, 2025
d74b10a
correct time stamp formatting
rettigl Feb 3, 2025
0d337da
add logging tests, and fix issue if cwd is not writable
rettigl Feb 8, 2025
1a3f372
fix logfile name and example config
rettigl Feb 8, 2025
fd2b313
fix tests
rettigl Feb 9, 2025
a988e6b
disable propagation of base_logger if logfile cannot be opened
rettigl Feb 9, 2025
4efb02d
fix beam status
rettigl Feb 10, 2025
f716b77
fix polarization and pump status conversions
rettigl Feb 11, 2025
488209c
add program name and version
rettigl Feb 12, 2025
2eef16a
add option to shift energy axis to EF, and add axis labels
rettigl Feb 12, 2025
c9496db
fix labels
rettigl Feb 12, 2025
5a8fe95
remove also source_pump etc. if not applied
rettigl Feb 27, 2025
5bde7dc
don't add entries if not found in epics archiver
rettigl Mar 3, 2025
cece16f
Merge pull request #71 from OpenCOMPES/fix_warnings
rettigl Mar 24, 2025
da8671a
allow photon energy from manual metadata
rettigl Mar 24, 2025
e347762
Merge pull request #73 from OpenCOMPES/shift_energy_axis
rettigl Mar 24, 2025
7890bca
add pulse energy
rettigl Mar 24, 2025
ead6db7
estimate collection time for static exposures
rettigl Feb 6, 2025
e88e6df
Merge pull request #72 from OpenCOMPES/add_static_duration
rettigl Mar 24, 2025
9a8e277
use LUT data for delay as well
rettigl Mar 24, 2025
9a153b3
update config and example
rettigl Mar 24, 2025
dddc80c
fix test
rettigl Mar 24, 2025
1e9b52a
fix config
rettigl Mar 24, 2025
fe3ce79
use newest pynxtools
rettigl Mar 24, 2025
35d602b
update pynxtools
rettigl Mar 26, 2025
6fedc6b
Merge pull request #74 from OpenCOMPES/pynxtools_update
rettigl Apr 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion .cspell/custom-dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
allclose
ALLUSERSPROFILE
amperemeter
appauthor
appname
arange
archiver
argwhere
Expand All @@ -18,6 +20,7 @@ basepath
bitshift
bysource
calib
caplog
checkscan
clim
codemirror
Expand All @@ -32,16 +35,21 @@ dapolymatrix
dataconverter
dataframe
delaystage
delenv
dtype
dxda
dxde
dyda
dyde
Ekin
electronanalyser
elab
elabapi
elabid
electronanalyzer
elems
endstation
energydispersion
entityid
eshift
faddr
Faradayweg
Expand Down Expand Up @@ -72,6 +80,7 @@ kwds
labview
Laurenz
lensmodes
levelname
lineh
linev
listf
Expand Down Expand Up @@ -102,6 +111,7 @@ Nxpix
Nxpixels
Nypixels
OPCPA
orcid
pcolormesh
Phoibos
polyfit
Expand All @@ -120,6 +130,7 @@ rrvec
rtol
rtype
scanvector
sharelink
specsanalyzer
Specslab
specsscan
Expand All @@ -131,9 +142,11 @@ toctree
tomlkit
topfloor
tqdm
trarpes
typehints
TZCYXS
undoc
userid
venv
viewcode
vline
Expand Down
4 changes: 2 additions & 2 deletions docs/specsanalyzer/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ The config module contains a mechanics to collect configuration parameters from
It will load an (optional) provided config file, or alternatively use a passed python dictionary as initial config dictionary, and subsequently look for the following additional config files to load:

* ``folder_config``: A config file of name :file:`specs_config.yaml` in the current working directory. This is mostly intended to pass calibration parameters of the workflow between different notebook instances.
* ``user_config``: A config file provided by the user, stored as :file:`.specsanalyzer/config.yaml` in the current user's home directly. This is intended to give a user the option for individual configuration modifications of system settings.
* ``system_config``: A config file provided by the system administrator, stored as :file:`/etc/specsanalyzer/config.yaml` on Linux-based systems, and :file:`%ALLUSERSPROFILE%/specsanalyzer/config.yaml` on Windows. This should provide all necessary default parameters for using the specsanalyzer processor with a given setup. For an example for the setup at the Fritz Haber Institute setup, see :ref:`example_config`
* ``user_config``: A config file provided by the user, stored as :file:`.config/specsanalyzer/config_v1.yaml` in the current user's home directly. This is intended to give a user the option for individual configuration modifications of system settings.
* ``system_config``: A config file provided by the system administrator, stored as :file:`/etc/specsanalyzer/config_v1.yaml` on Linux-based systems, and :file:`%ALLUSERSPROFILE%/specsanalyzer/config_v1.yaml` on Windows. This should provide all necessary default parameters for using the specsanalyzer processor with a given setup. For an example for the setup at the Fritz Haber Institute setup, see :ref:`example_config`
* ``default_config``: The default configuration shipped with the package. Typically, all parameters here should be overwritten by any of the other configuration files.

The config mechanism returns the combined dictionary, and reports the loaded configuration files. In order to disable or overwrite any of the configuration files, they can be also given as optional parameters (path to a file, or python dictionary).
Expand Down
4 changes: 4 additions & 0 deletions docs/specsscan/helpers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ Helpers
.. automodule:: specsscan.helpers
:members:
:undoc-members:

.. automodule:: specsscan.metadata
:members:
:undoc-members:
13 changes: 4 additions & 9 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,16 @@ classifiers = [
"Operating System :: OS Independent",
]
dependencies = [
"elabapi-python>=5.0",
"h5py>=3.6.0",
"imutils>=0.5.4",
"ipympl>=0.9.1",
"ipywidgets>=7.7.1",
"matplotlib>=3.5.1,<3.10.0",
"matplotlib>=3.5.1",
"numpy>=1.21.6",
"opencv-python>=4.8.1.78",
"pynxtools-mpes>=0.2.1",
"pynxtools>=0.9.3",
"pynxtools-mpes>=0.2.2",
"pynxtools>=0.10.1",
"python-dateutil>=2.8.2",
"pyyaml>=6.0",
"xarray>=0.20.2",
Expand Down Expand Up @@ -83,12 +84,6 @@ all = [
"specsanalyzer[dev,docs,notebook]",
]

[tool.coverage.report]
omit = [
"config.py",
"config-3.py",
]

[tool.ruff]
include = ["specsanalyzer/*.py", "specsscan/*.py", "tests/*.py"]
lint.select = [
Expand Down
143 changes: 117 additions & 26 deletions src/specsanalyzer/config.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,35 @@
"""This module contains a config library for loading yaml/json files into dicts"""
from __future__ import annotations

import copy
import json
import os
import platform
from importlib.util import find_spec
from pathlib import Path

import yaml
from platformdirs import user_config_path

from specsanalyzer.logging import setup_logging

package_dir = os.path.dirname(find_spec("specsanalyzer").origin)

USER_CONFIG_PATH = user_config_path(
appname="specsanalyzer",
appauthor="OpenCOMPES",
ensure_exists=True,
)
SYSTEM_CONFIG_PATH = (
Path(os.environ["ALLUSERSPROFILE"]).joinpath("specsanalyzer")
if platform.system() == "Windows"
else Path("/etc/").joinpath("specsanalyzer")
)
ENV_DIR = Path(".env")

# Configure logging
logger = setup_logging("config")


def parse_config(
config: dict | str = None,
Expand All @@ -36,12 +55,13 @@ def parse_config(
user_config (dict | str, optional): user-based config dictionary
or file path. The loaded dictionary is completed with the user-based values,
taking preference over system and default values.
Defaults to the file ".specsanalyzer/config.yaml" in the current user's home directory.
Defaults to the file ".config/specsanalyzer/config_v1.yaml" in the current user's home
directory.
system_config (dict | str, optional): system-wide config dictionary
or file path. The loaded dictionary is completed with the system-wide values,
taking preference over default values.
Defaults to the file "/etc/specsanalyzer/config.yaml" on linux,
and "%ALLUSERSPROFILE%/specsanalyzer/config.yaml" on windows.
Defaults to the file "/etc/specsanalyzer/config_v1.yaml" on linux,
and "%ALLUSERSPROFILE%/specsanalyzer/config_v1.yaml" on windows.
default_config (dict | str, optional): default config dictionary
or file path. The loaded dictionary is completed with the default values.
Defaults to *package_dir*/config/default.yaml".
Expand All @@ -57,62 +77,51 @@ def parse_config(
config = {}

if isinstance(config, dict):
config_dict = config
config_dict = copy.deepcopy(config)
else:
config_dict = load_config(config)
if verbose:
print(f"Configuration loaded from: [{str(Path(config).resolve())}]")
logger.info(f"Configuration loaded from: [{str(Path(config).resolve())}]")

folder_dict: dict = None
if isinstance(folder_config, dict):
folder_dict = folder_config
folder_dict = copy.deepcopy(folder_config)
else:
if folder_config is None:
folder_config = "./specs_config.yaml"
if Path(folder_config).exists():
folder_dict = load_config(folder_config)
if verbose:
print(f"Folder config loaded from: [{str(Path(folder_config).resolve())}]")
logger.info(f"Folder config loaded from: [{str(Path(folder_config).resolve())}]")

user_dict: dict = None
if isinstance(user_config, dict):
user_dict = user_config
user_dict = copy.deepcopy(user_config)
else:
if user_config is None:
user_config = str(
Path.home().joinpath(".specsanalyzer").joinpath("config.yaml"),
)
user_config = str(USER_CONFIG_PATH.joinpath("config_v1.yaml"))
if Path(user_config).exists():
user_dict = load_config(user_config)
if verbose:
print(f"User config loaded from: [{str(Path(user_config).resolve())}]")
logger.info(f"User config loaded from: [{str(Path(user_config).resolve())}]")

system_dict: dict = None
if isinstance(system_config, dict):
system_dict = system_config
system_dict = copy.deepcopy(system_config)
else:
if system_config is None:
if platform.system() in ["Linux", "Darwin"]:
system_config = str(
Path("/etc/").joinpath("specsanalyzer").joinpath("config.yaml"),
)
elif platform.system() == "Windows":
system_config = str(
Path(os.environ["ALLUSERSPROFILE"])
.joinpath("specsanalyzer")
.joinpath("config.yaml"),
)
system_config = str(SYSTEM_CONFIG_PATH.joinpath("config_v1.yaml"))
if Path(system_config).exists():
system_dict = load_config(system_config)
if verbose:
print(f"System config loaded from: [{str(Path(system_config).resolve())}]")
logger.info(f"System config loaded from: [{str(Path(system_config).resolve())}]")

if isinstance(default_config, dict):
default_dict = default_config
default_dict = copy.deepcopy(default_config)
else:
default_dict = load_config(default_config)
if verbose:
print(f"Default config loaded from: [{str(Path(default_config).resolve())}]")
logger.info(f"Default config loaded from: [{str(Path(default_config).resolve())}]")

if folder_dict is not None:
config_dict = complete_dictionary(
Expand Down Expand Up @@ -226,3 +235,85 @@ def complete_dictionary(dictionary: dict, base_dictionary: dict) -> dict:
dictionary[k] = v

return dictionary


def _parse_env_file(file_path: Path) -> dict:
"""Helper function to parse a .env file into a dictionary.

Args:
file_path (Path): Path to the .env file

Returns:
dict: Dictionary of environment variables from the file
"""
env_content = {}
if file_path.exists():
with open(file_path) as f:
for line in f:
line = line.strip()
if line and "=" in line:
key, val = line.split("=", 1)
env_content[key.strip()] = val.strip()
return env_content


def read_env_var(var_name: str) -> str | None:
"""Read an environment variable from multiple locations in order:
1. OS environment variables
2. .env file in current directory
3. .env file in user config directory
4. .env file in system config directory

Args:
var_name (str): Name of the environment variable to read

Returns:
str | None: Value of the environment variable or None if not found
"""
# 1. check OS environment variables
value = os.getenv(var_name)
if value is not None:
logger.debug(f"Found {var_name} in OS environment variables")
return value

# 2. check .env in current directory
local_vars = _parse_env_file(ENV_DIR)
if var_name in local_vars:
logger.debug(f"Found {var_name} in ./.env file")
return local_vars[var_name]

# 3. check .env in user config directory
user_vars = _parse_env_file(USER_CONFIG_PATH / ".env")
if var_name in user_vars:
logger.debug(f"Found {var_name} in user config .env file")
return user_vars[var_name]

# 4. check .env in system config directory
system_vars = _parse_env_file(SYSTEM_CONFIG_PATH / ".env")
if var_name in system_vars:
logger.debug(f"Found {var_name} in system config .env file")
return system_vars[var_name]

logger.debug(f"Environment variable {var_name} not found in any location")
return None


def save_env_var(var_name: str, value: str) -> None:
"""Save an environment variable to the .env file in the user config directory.
If the file exists, preserves other variables. If not, creates a new file.

Args:
var_name (str): Name of the environment variable to save
value (str): Value to save for the environment variable
"""
env_path = USER_CONFIG_PATH / ".env"
env_content = _parse_env_file(env_path)

# Update or add new variable
env_content[var_name] = value

# Write all variables back to file
with open(env_path, "w") as f:
for key, val in env_content.items():
f.write(f"{key}={val}\n")
logger.debug(f"Environment variable {var_name} saved to .env file")
7 changes: 6 additions & 1 deletion src/specsanalyzer/convert.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
"""Specsanalyzer image conversion module"""
from __future__ import annotations

import logging

import numpy as np
from scipy.ndimage import map_coordinates

# Configure logging
logger = logging.getLogger("specsanalyzer.specsscan")


def get_damatrix_from_calib2d(
lens_mode: str,
Expand Down Expand Up @@ -82,7 +87,7 @@ def get_damatrix_from_calib2d(

elif lens_mode in supported_space_modes:
# use the mode defaults
print("This is a spatial mode, using default " + lens_mode + " config")
logger.info("This is a spatial mode, using default " + lens_mode + " config")
rr_vec, da_matrix_full = get_rr_da(lens_mode, calib2d_dict)
a_inner = da_matrix_full[0][0]
da_matrix = da_matrix_full[1:][:]
Expand Down
Loading