Coverage for src/pytribeam/log.py: 94%
89 statements
« prev ^ index » next coverage.py v7.6.1, created at 2026-09-09 18:27 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2026-09-09 18:27 +0000
1#!/usr/bin/python3
2"""
3Experiment log-file utilities for `pytribeam`.
5This module provides helper functions for creating and updating `pytribeam`
6experiment log files. Logs are stored as HDF5 files using `h5py`, with dataset
7names, structured NumPy dtypes, and units defined by `pytribeam.constants.Constants`.
9The logging functions append timestamped rows to HDF5 datasets for experiment
10settings, stage position, laser power, and specimen current. These records are
11used to document experiment state before, during, and after automated
12TriBeam/FIB/laser operations.
14## Typical usage
16```python
17from pathlib import Path
19from pytribeam import log
20from pytribeam.constants import Constants
22log_path = Path("experiment_log.h5")
23config_path = Path("experiment.yml")
25log.create_file(log_path)
27log.experiment_settings(
28 slice_number=1,
29 step_number=1,
30 log_filepath=log_path,
31 yml_path=config_path,
32)
34log.position(
35 step_number=1,
36 step_name="mill",
37 slice_number=1,
38 log_filepath=log_path,
39 dataset_name=Constants.pre_position_dataset_name,
40 current_position=current_position,
41)
42```
44## Main entry points
46- `create_file`: create an HDF5 log file if it does not already exist.
47- `current_time`: return the current time as a `tbt.TimeStamp`.
48- `experiment_settings`: append the active experiment configuration to the log.
49- `position`: append a stage-position record to a step-specific dataset.
50- `laser_power`: append a laser-power measurement to a step-specific dataset.
51- `specimen_current`: append a specimen-current measurement to a step-specific
52 dataset.
53- `yml_from_log`: extract a saved YAML configuration from an existing log file.
55## Log-file layout
57Experiment settings are stored in a top-level dataset named by
58`Constants.settings_dataset_name`.
60Step-specific measurements are stored under groups named using the step number
61and step name:
63```text
64{step_number:02d}_{step_name}/{dataset_name}
65```
67For example:
69```text
70Experiment Settings
7101_mill/Position Before
7201_mill/Position After
7301_laser/Laser Power Before
7401_laser/Laser Power After
75```
77The exact dataset names and structured dtypes are defined in
78`pytribeam.constants.Constants`.
80## Timestamp convention
82Each logged row includes both a human-readable timestamp and a UNIX timestamp.
83Timestamps are generated by `current_time` at the time the row is written.
85## Units
87Datasets that store physical quantities include HDF5 attributes describing their
88units. Stage positions are recorded in millimeters and degrees, laser power is
89recorded in watts, and specimen current is recorded in nanoamperes.
91## Notes
93This module assumes that log files are valid HDF5 files. Use `create_file` to
94initialize new log files before appending records.
96<hr style="height: 12px; background-color: #333; border: none;">
97"""
99__all__ = [
100 "create_file",
101 "current_time",
102 "yml_from_log",
103 "experiment_settings",
104 "position",
105 "laser_power",
106 "specimen_current",
107]
109# Default python modules
110# from functools import singledispatch
111from pathlib import Path
112import datetime
114# Autoscript included modules
115import numpy as np
116import h5py
118# 3rd party module
120# Local scripts
121from pytribeam.constants import Constants
122import pytribeam.types as tbt
125def create_file(path: Path) -> bool:
126 """
127 Create a log file at the specified path.
129 This function creates a log file at the specified path if it does not already exist.
131 ## Parameters
133 - `path` (`Path`): The path where the log file should be created.
135 ## Returns
137 - `bool`: True if the log file is created successfully.
139 ## Raises
141 - `ValueError`: If the log file cannot be created.
142 """
143 if not path.is_file(): 143 ↛ 148line 143 didn't jump to line 148 because the condition on line 143 was always true
144 log = h5py.File(path, "w")
145 log.close()
146 if path.is_file(): 146 ↛ 148line 146 didn't jump to line 148 because the condition on line 146 was always true
147 print(f'Logfile created at "{path}".')
148 if path.is_file(): 148 ↛ 150line 148 didn't jump to line 150 because the condition on line 148 was always true
149 return True
150 raise ValueError(f'Unable to create log file at location "{path}".')
153def current_time() -> tbt.TimeStamp:
154 """
155 Get the current time as a timestamp.
157 This function returns the current time as a `TimeStamp` object, including both human-readable and UNIX time formats.
159 ## Returns
161 - `tbt.TimeStamp`: The current time as a `TimeStamp` object.
162 """
163 now = datetime.datetime.now()
164 human_readable = now.strftime("%m/%d/%Y %H:%M:%S")
165 unix_time = int(now.timestamp())
166 time = tbt.TimeStamp(human_readable=human_readable, unix=unix_time)
167 return time
170def yml_from_log(
171 log_path_h5: Path,
172 output_path_yml: Path,
173 row: int,
174 config_field: str = "Config File",
175) -> bool:
176 """
177 Extract YAML configuration from a log file and save it to an output path.
179 This function extracts the YAML configuration from a specified row in the log file and saves it to the output path.
181 ## Parameters
183 - `log_path_h5` (`Path`): The path to the log file.
184 - `output_path_yml` (`Path`): The path to save the extracted YAML configuration.
185 - `row` (`int`): The row number to extract the configuration from.
186 - `config_field` (`str, optional`): The field name for the configuration in the log file (default is "Config File").
188 ## Returns
190 - `bool`: True if the YAML configuration is extracted and saved successfully.
191 """
192 # TODO enforce file formats on inputs
193 with h5py.File(log_path_h5, "r") as file:
194 data = np.array(file[Constants.settings_dataset_name][:])
195 settings = data[row][Constants.settings_dtype.names.index(config_field)].decode(
196 "utf-8"
197 )
199 with open(output_path_yml, "w") as file:
200 file.write(settings)
202 return True
205def experiment_settings(
206 slice_number: int,
207 step_number: int,
208 log_filepath: Path,
209 yml_path: Path,
210) -> bool:
211 """
212 Log experiment settings to the log file.
214 This function logs the experiment settings from a YAML file to the log file.
216 ## Parameters
218 - `slice_number` (`int`): The slice number for the experiment.
219 - `step_number` (`int`): The step number for the experiment.
220 - `log_filepath` (`Path`): The path to the log file.
221 - `yml_path` (`Path`): The path to the YAML file containing the experiment settings.
223 ## Returns
225 - `bool`: True if the experiment settings are logged successfully.
226 """
227 dataset_name = Constants.settings_dataset_name
228 settings_dtype = Constants.settings_dtype
229 time = current_time()
231 with open(yml_path, "r") as yml_file:
232 yml_data = yml_file.read()
234 if not log_filepath.exists(): 234 ↛ 235line 234 didn't jump to line 235 because the condition on line 234 was never true
235 log_filepath.touch()
237 with h5py.File(log_filepath, "r+") as log:
238 if not dataset_name in log: 238 ↛ 245line 238 didn't jump to line 245 because the condition on line 238 was always true
239 settings = log.create_dataset(
240 dataset_name,
241 (0,),
242 settings_dtype,
243 maxshape=(None,),
244 )
245 dataset = log[dataset_name]
246 settings_data = np.array(
247 [
248 (
249 slice_number,
250 step_number,
251 yml_data,
252 time.human_readable,
253 time.unix,
254 )
255 ],
256 settings_dtype,
257 )
258 # add one row to table
259 dataset.resize(dataset.shape[0] + 1, axis=0)
260 dataset[-1:] = settings_data
262 return True
265def position(
266 step_number: int,
267 step_name: str,
268 slice_number: int,
269 log_filepath: Path,
270 dataset_name: str,
271 current_position: tbt.StagePositionUser,
272) -> bool:
273 """
274 Log the current position to the log file.
276 This function logs the current position of the stage to the log file.
278 ## Parameters
280 - `step_number` (`int`): The step number for the experiment.
281 - `step_name` (`str`): The name of the step.
282 - `slice_number` (`int`): The slice number for the experiment.
283 - `log_filepath` (`Path`): The path to the log file.
284 - `dataset_name` (`str`): The name of the dataset to log the position to.
285 - `current_position` (`tbt.StagePositionUser`): The current position of the stage.
287 ## Returns
289 - `bool`: True if the current position is logged successfully.
290 """
291 print("\tLogging current position...")
292 dataset_location = f"{step_number:02d}_{step_name}/{dataset_name}"
293 time = current_time()
295 with h5py.File(log_filepath, "r+") as file:
296 if not dataset_location in file:
297 position = file.create_dataset(
298 dataset_location,
299 (0,),
300 Constants.position_dtype,
301 maxshape=(None,),
302 )
303 position.attrs["X Units"] = np.string_("[mm]")
304 position.attrs["Y Units"] = np.string_("[mm]")
305 position.attrs["Z Units"] = np.string_("[mm]")
306 position.attrs["T Units"] = np.string_("[degrees]")
307 position.attrs["R Units"] = np.string_("[degrees]")
309 dataset = file[dataset_location]
310 position_data = np.array(
311 [
312 (
313 slice_number,
314 round(current_position.x_mm, 6),
315 round(current_position.y_mm, 6),
316 round(current_position.z_mm, 6),
317 round(current_position.t_deg, 6),
318 round(current_position.r_deg, 6),
319 time.human_readable,
320 time.unix,
321 )
322 ],
323 Constants.position_dtype,
324 )
325 # add one row to table
326 dataset.resize(dataset.shape[0] + 1, axis=0)
327 dataset[-1:] = position_data
329 return True
332def laser_power(
333 step_number: int,
334 step_name: str,
335 slice_number: int,
336 log_filepath: Path,
337 dataset_name: str,
338 power_w: float,
339) -> bool:
340 """
341 Log the laser power to the log file.
343 This function logs the laser power to the log file.
345 ## Parameters
347 - `step_number` (`int`): The step number for the experiment.
348 - `step_name` (`str`): The name of the step.
349 - `slice_number` (`int`): The slice number for the experiment.
350 - `log_filepath` (`Path`): The path to the log file.
351 - `dataset_name` (`str`): The name of the dataset to log the laser power to.
352 - `power_w` (`float`): The laser power in watts.
354 ## Returns
356 - `bool`: True if the laser power is logged successfully.
357 """
358 print("\tLogging laser power...")
359 dataset_location = f"{step_number:02d}_{step_name}/{dataset_name}"
360 time = current_time()
362 with h5py.File(log_filepath, "r+") as file:
363 if not dataset_location in file:
364 laser_power = file.create_dataset(
365 dataset_location,
366 (0,),
367 Constants.laser_power_dtype,
368 maxshape=(None,),
369 )
370 laser_power.attrs["Units"] = np.string_("[W]")
372 dataset = file[dataset_location]
373 laser_power_data = np.array(
374 [
375 (
376 slice_number,
377 round(power_w, 6),
378 time.human_readable,
379 time.unix,
380 )
381 ],
382 Constants.laser_power_dtype,
383 )
384 # add one row to table
385 dataset.resize(dataset.shape[0] + 1, axis=0)
386 dataset[-1:] = laser_power_data
388 return True
391def specimen_current(
392 step_number: int,
393 step_name: str,
394 slice_number: int,
395 log_filepath: Path,
396 dataset_name: str,
397 specimen_current_na: float,
398) -> bool:
399 """
400 Log the specimen current to the log file.
402 This function logs the specimen current to the log file.
404 ## Parameters
406 - `step_number` (`int`): The step number for the experiment.
407 - `step_name` (`str`): The name of the step.
408 - `slice_number` (`int`): The slice number for the experiment.
409 - `log_filepath` (`Path`): The path to the log file.
410 - `dataset_name` (`str`): The name of the dataset to log the specimen current to.
411 - `specimen_current_na` (`float`): The specimen current in nanoamperes.
413 ## Returns
415 - `bool`: True if the specimen current is logged successfully.
416 """
417 print("\tLogging sample current...")
418 dataset_location = f"{step_number:02d}_{step_name}/{dataset_name}"
419 time = current_time()
421 with h5py.File(log_filepath, "r+") as file:
422 if not dataset_location in file:
423 specimen_current = file.create_dataset(
424 dataset_location,
425 (0,),
426 Constants.specimen_current_dtype,
427 maxshape=(None,),
428 )
429 specimen_current.attrs["Units"] = np.string_("[nA]")
431 dataset = file[dataset_location]
432 specimen_current_data = np.array(
433 [
434 (
435 slice_number,
436 round(specimen_current_na, 6),
437 time.human_readable,
438 time.unix,
439 )
440 ],
441 Constants.specimen_current_dtype,
442 )
443 # add one row to table
444 dataset.resize(dataset.shape[0] + 1, axis=0)
445 dataset[-1:] = specimen_current_data
446 print("\tLogging sample current complete...")
448 return True