Coverage for src/hallmd/utils.py: 89%
47 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-02-20 20:43 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-02-20 20:43 +0000
1"""Module to provide utilities for the `hallmd` package.
3Includes:
5- `load_device()` - Load a device configuration from the `hallmd.devices` directory.
6"""
7import json
8import os
9from importlib import resources
10from pathlib import Path
12import yaml
14__all__ = ['load_device']
16AVOGADRO_CONSTANT = 6.02214076e23 # Avogadro constant (mol^-1)
17FUNDAMENTAL_CHARGE = 1.602176634e-19 # Fundamental charge (C)
18BOLTZMANN_CONSTANT = 1.380649e-23 # Boltzmann constant (J/K)
19TORR_2_PA = 133.322 # Conversion factor from Torr to Pa
20MOLECULAR_WEIGHTS = {
21 'Xenon': 131.293,
22 'Argon': 39.948,
23 'Krypton': 83.798,
24 'Bismuth': 208.98,
25 'Hydrogen': 1.008,
26 'Mercury': 200.59
27}
30def _path_in_dict(value, data: dict) -> list:
31 """Recursively check if a value is in a dictionary and return the list of access keys to the value."""
32 if isinstance(data, dict):
33 for key, v in data.items():
34 path = _path_in_dict(value, v)
35 if path:
36 return [key] + path
37 elif data == value:
38 return [value] # found the value
39 return [] # value not found
42def load_device(device_name: str, device_file: str = 'device.yml', device_dir: str | Path = None) -> dict:
43 """Load a device configuration from the `device_dir` directory. The `device_file` must be located at
44 `device_dir/device_name/device_file`. All other files in the directory, if referenced in `device_file`, will
45 be converted to an absolute path.
47 !!! Example "Loading a device configuration"
48 Currently, the only provided device configuration is for the SPT-100 thruster.
49 ```python
50 from hallmd.utils import load_device
52 device = load_device('SPT-100')
53 ```
55 You may put custom configurations in the `hallmd.devices` directory or specify a custom directory with a custom
56 configuration file:
57 ```yaml
58 name: MyDevice
59 geometry:
60 channel_length: 1
61 inner_radius: 2
62 outer_radius: 3
63 magnetic_field:
64 file: bfield.csv
65 shielded: false
66 ```
68 :param device_name: name of the device configuration to load
69 :param device_file: name of the device configuration file (default: 'device.yml'). Only supported file types are
70 `.yml` and `.json`.
71 :param device_dir: directory containing the devices. If None, the `hallmd.devices` directory is used.
72 :return: dictionary containing the device configuration
73 """
74 device_dir = resources.files('hallmd.devices') if device_dir is None else Path(device_dir)
75 if not (device_dir / device_name).exists():
76 raise FileNotFoundError(f'Device directory "{device_name}" not found in the device folder.')
77 if not (device_dir / device_name / device_file).exists():
78 raise FileNotFoundError(f'Device configuration file "{device_file}" not found in the "{device_name}" '
79 f'directory. Please rename or specify the configuration file as "{device_file}".')
81 config_file = device_dir / device_name / device_file
82 with open(config_file, 'r', encoding='utf-8') as fd:
83 if config_file.suffix == '.yml':
84 config = yaml.safe_load(fd)
85 elif config_file.suffix == '.json':
86 config = json.load(fd)
87 else:
88 raise ValueError(f'Unsupported file type "{config_file.suffix}". Only .yml and .json files are supported.')
90 # Convert all relative paths to absolute paths
91 for root, _, files in os.walk(device_dir / device_name):
92 for file in files:
93 if file != device_file:
94 # Check if the posix file path from root is in the config (i.e. "./file.csv")
95 root_path = Path(root) / file # Path like "hallmd/devices/SPT-100/path/to/file.csv"
96 rel_path = root_path.relative_to(device_dir / device_name) # Just the path/to/file.csv part (relative)
97 dict_path = _path_in_dict(rel_path.as_posix(), config)
98 if len(dict_path) == 0:
99 # Check if the plain filename is in the config (i.e. file.csv); will only pick first match
100 dict_path = _path_in_dict(file, config)
102 if dict_path:
103 d = config # pointer to the nested location in config
104 for key in dict_path[:-2]:
105 d = config[key]
106 d[dict_path[-2]] = root_path.resolve().as_posix()
108 return config