Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 9 additions & 2 deletions src/ibex_bluesky_core/callbacks/_file_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import logging
import os
from pathlib import Path
from stat import S_IRGRP, S_IROTH, S_IRUSR

from bluesky.callbacks import CallbackBase
from event_model.documents.event import Event
Expand Down Expand Up @@ -38,7 +39,9 @@
class HumanReadableFileCallback(CallbackBase):
"""Outputs bluesky runs to human-readable output files in the specified directory path."""

def __init__(self, fields: list[str], *, output_dir: Path | None, postfix: str = "") -> None:
def __init__(
self, fields: list[str], *, output_dir: Path | None = None, postfix: str = ""
) -> None:
"""Output human-readable output files of bluesky runs.

If fields are given, just output those, otherwise output all hinted signals.
Expand Down Expand Up @@ -67,6 +70,7 @@ def start(self, doc: RunStart) -> None:
self.current_start_document = doc[UID]

rb_num = _get_rb_num(doc)
rb_num_str = rb_num if rb_num == "Unknown RB" else f"RB{rb_num}"

# motors is a tuple, we need to convert to a list to join the two below
motors = list(doc.get(MOTORS, []))
Expand All @@ -75,7 +79,8 @@ def start(self, doc: RunStart) -> None:

self.filename = (
self.output_dir
/ f"{rb_num}"
/ rb_num_str
/ "bluesky_scans"
/ f"{get_instrument()}{'_' + '_'.join(motors) if motors else ''}_"
f"{formatted_time}Z{self.postfix}.txt"
)
Expand Down Expand Up @@ -150,4 +155,6 @@ def stop(self, doc: RunStop) -> RunStop | None:
"""Clear descriptors."""
logger.info("Stopping run, clearing descriptors, filename=%s", self.filename)
self.descriptors.clear()
if self.filename is not None:
os.chmod(self.filename, S_IRUSR | S_IRGRP | S_IROTH)
return super().stop(doc)
5 changes: 4 additions & 1 deletion src/ibex_bluesky_core/callbacks/_fitting.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import warnings
from itertools import zip_longest
from pathlib import Path
from stat import S_IRGRP, S_IROTH, S_IRUSR

import lmfit
import numpy as np
Expand Down Expand Up @@ -172,8 +173,9 @@ def start(self, doc: RunStart) -> None:
file = f"{get_instrument()}_{self.x}_{self.y}_{title_format_datetime}Z{self.postfix}.txt"

rb_num = _get_rb_num(doc)
rb_num_str = rb_num if rb_num == "Unknown RB" else f"RB{rb_num}"

self.filename = self.output_dir / f"{rb_num}" / file
self.filename = self.output_dir / f"{rb_num_str}" / "bluesky_scans" / file

def event(self, doc: Event) -> Event:
"""Start collecting, y, x, and yerr data.
Expand Down Expand Up @@ -237,6 +239,7 @@ def stop(self, doc: RunStop) -> None:
self.write_fields_table_uncertainty()

logger.info("Fitting information successfully written to: %s", self.filename.resolve())
os.chmod(self.filename, S_IRUSR | S_IRGRP | S_IROTH)

def write_fields_table(self) -> None:
"""Write collected run info to the fitting file."""
Expand Down
7 changes: 6 additions & 1 deletion src/ibex_bluesky_core/callbacks/_plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import os
import threading
from pathlib import Path
from stat import S_IRGRP, S_IROTH, S_IRUSR
from typing import Any

import matplotlib
Expand Down Expand Up @@ -227,9 +228,12 @@ def __init__(
self.filename = None

def start(self, doc: RunStart) -> None:
rb_num = _get_rb_num(doc)
rb_num_str = rb_num if rb_num == "Unknown RB" else f"RB{rb_num}"
self.filename = (
self.output_dir
/ f"{_get_rb_num(doc)}"
/ f"{rb_num_str}"
/ "bluesky_scans"
/ f"{get_instrument()}_{self.x}_{self.y}_{format_time(doc)}Z{self.postfix}.png"
)

Expand All @@ -245,3 +249,4 @@ def stop(self, doc: RunStop) -> None:

self.filename.parent.mkdir(parents=True, exist_ok=True)
self.ax.figure.savefig(self.filename, format="png") # pyright: ignore [reportAttributeAccessIssue]
os.chmod(self.filename, S_IRUSR | S_IRGRP | S_IROTH)
6 changes: 1 addition & 5 deletions src/ibex_bluesky_core/callbacks/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,7 @@ def get_instrument() -> str:

def get_default_output_path() -> Path:
output_dir_env = os.environ.get(OUTPUT_DIR_ENV_VAR)
return (
Path("//isis.cclrc.ac.uk/inst$") / node() / "user" / "bluesky_scans"
if output_dir_env is None
else Path(output_dir_env)
)
return Path("C:/Data") if output_dir_env is None else Path(output_dir_env)


def format_time(doc: Event | RunStart | RunStop) -> str:
Expand Down
53 changes: 50 additions & 3 deletions tests/callbacks/fitting/test_fit_logging_callback.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os
from pathlib import Path
from platform import node
from stat import S_IRGRP, S_IROTH, S_IRUSR
from unittest.mock import MagicMock, mock_open, patch

import pytest
Expand Down Expand Up @@ -30,6 +31,7 @@ def test_after_fitting_callback_writes_to_file_successfully_no_y_uncertainty(
with (
patch("ibex_bluesky_core.callbacks._fitting.open", m),
patch("ibex_bluesky_core.callbacks._fitting.os.makedirs"),
patch("os.chmod"),
):
with patch("time.time", MagicMock(return_value=time)):
RE(
Expand All @@ -39,7 +41,10 @@ def test_after_fitting_callback_writes_to_file_successfully_no_y_uncertainty(
)

assert m.call_args_list[0].args == (
filepath / "0" / f"{node()}_motor_invariant_2024-10-04_13-43-43Z{postfix}.txt",
(
filepath / "RB0" / "bluesky_scans" / f"{node()}_motor_invariant_2024-10-04_13-43-43"
f"Z{postfix}.txt"
),
"w",
) # type: ignore

Expand Down Expand Up @@ -68,6 +73,7 @@ def test_fitting_callback_handles_no_rb_number_save(
with (
patch("ibex_bluesky_core.callbacks._fitting.open", m),
patch("ibex_bluesky_core.callbacks._fitting.os.makedirs"),
patch("os.chmod"),
):
with patch("time.time", MagicMock(return_value=time)):
RE(
Expand All @@ -76,7 +82,12 @@ def test_fitting_callback_handles_no_rb_number_save(
)

assert m.call_args_list[0].args == (
filepath / "Unknown RB" / f"{node()}_motor_invariant_2024-10-04_13-43-43Z{postfix}.txt",
(
filepath
/ "Unknown RB"
/ "bluesky_scans"
/ f"{node()}_motor_invariant_2024-10-04_13-43-43Z{postfix}.txt"
),
"w",
) # type: ignore

Expand All @@ -100,6 +111,7 @@ def test_after_fitting_callback_writes_to_file_successfully_with_y_uncertainty(
with (
patch("ibex_bluesky_core.callbacks._fitting.open", m),
patch("ibex_bluesky_core.callbacks._fitting.os.makedirs"),
patch("os.chmod"),
):
with patch("time.time", MagicMock(return_value=time)):
RE(
Expand All @@ -109,7 +121,10 @@ def test_after_fitting_callback_writes_to_file_successfully_with_y_uncertainty(
)

assert m.call_args_list[0].args == (
filepath / "0" / f"{node()}_motor_invariant_2024-10-04_13-43-43Z{postfix}.txt",
(
filepath / "RB0" / "bluesky_scans" / f"{node()}_motor_invariant_2024-10-04_13-43-43"
f"Z{postfix}.txt"
),
"w",
) # type: ignore

Expand Down Expand Up @@ -203,3 +218,35 @@ def test_error_thrown_if_no_y_err_data_in_event(RE: run_engine.RunEngine):
}
}
)


def test_file_set_readonly_after_written(
RE: run_engine.RunEngine,
):
invariant = soft_signal_rw(float, 0.5, name="invariant")
mot = soft_signal_rw(float, name="motor")

filepath = Path("C:\\") / "instrument" / "var" / "logs"
postfix = "fit1"
m = mock_open()

lf = LiveFit(Linear.fit(), y="invariant", x="motor", update_every=50)
lfl = LiveFitLogger(lf, y="invariant", x="motor", postfix=postfix, output_dir=filepath)
with (
patch("ibex_bluesky_core.callbacks._fitting.open", m),
patch("ibex_bluesky_core.callbacks._fitting.os.makedirs"),
patch("os.chmod") as mock_chmod,
):
with patch("time.time", MagicMock(return_value=time)):
RE(
scan([invariant], mot, -1, 1, 3),
[lf, lfl], # pyright: ignore until https://github.com/bluesky/bluesky/issues/1938
)

fit_filepath = (
filepath
/ "Unknown RB"
/ "bluesky_scans"
/ f"{node()}_motor_invariant_2024-10-04_13-43-43Z{postfix}.txt"
)
mock_chmod.assert_called_with(fit_filepath, S_IRUSR | S_IRGRP | S_IROTH)
67 changes: 47 additions & 20 deletions tests/callbacks/test_plotting_callback.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from stat import S_IRGRP, S_IROTH, S_IRUSR
from typing import Any
from unittest.mock import MagicMock, patch

Expand Down Expand Up @@ -82,32 +83,58 @@ def test_errorbars_created_if_yerr_is_given():


def test_png_saved_on_run_stop():
ax = MagicMock(spec=Axes)
ax.figure = MagicMock(spec=Figure)
s = PlotPNGSaver(x="x", y="y", ax=ax, postfix="123", output_dir="")

s.start(
{
"uid": "0",
RB: 1234, # pyright: ignore reportArgumentType
"time": 123456789,
}
)

s.stop(
{
"time": 234567891,
"uid": "2",
"exit_status": "success",
"run_start": "",
}
)
with patch("os.chmod"):
ax = MagicMock(spec=Axes)
ax.figure = MagicMock(spec=Figure)
s = PlotPNGSaver(x="x", y="y", ax=ax, postfix="123", output_dir="")

s.start(
{
"uid": "0",
RB: 1234, # pyright: ignore reportArgumentType
"time": 123456789,
}
)

s.stop(
{
"time": 234567891,
"uid": "2",
"exit_status": "success",
"run_start": "",
}
)

assert ax.figure.savefig.call_count == 1
assert ax.figure.savefig.call_args.kwargs["format"] == "png"
assert "x_y_1973-11-29_21-33-09Z123.png" in ax.figure.savefig.call_args.args[0].name


def test_file_readonly_on_stop():
with patch("os.chmod") as mock_chmod:
ax = MagicMock(spec=Axes)
ax.figure = MagicMock(spec=Figure)
s = PlotPNGSaver(x="x", y="y", ax=ax, postfix="123", output_dir="")

s.start(
{
"uid": "0",
RB: 1234, # pyright: ignore reportArgumentType
"time": 123456789,
}
)
directory = s.filename
s.stop(
{
"time": 234567891,
"uid": "2",
"exit_status": "success",
"run_start": "",
}
)
mock_chmod.assert_called_with(directory, S_IRUSR | S_IRGRP | S_IROTH)


def test_errorbars_not_created_if_no_yerr():
_, ax = plt.subplots()
ax.errorbar = MagicMock()
Expand Down
2 changes: 1 addition & 1 deletion tests/callbacks/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ def test_default_output_location_with_env_var():
@pytest.mark.skipif(os.name != "nt", reason="Windows only")
def test_default_output_location_without_env_var():
with patch("ibex_bluesky_core.callbacks._utils.os.environ.get", return_value=None):
assert str(get_default_output_path()).startswith(r"\\isis.cclrc.ac.uk")
assert str(get_default_output_path()) == r"C:\Data"
46 changes: 40 additions & 6 deletions tests/callbacks/test_write_log_callback.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from pathlib import Path
from platform import node
from stat import S_IRGRP, S_IROTH, S_IRUSR
from unittest.mock import call, mock_open, patch

import pytest
Expand Down Expand Up @@ -32,7 +33,8 @@ def test_header_data_all_available_on_start(cb):
cb.start(run_start)
result = (
save_path
/ f"{run_start.get('rb_number', None)}"
/ f"RB{run_start.get('rb_number', None)}"
/ "bluesky_scans"
/ f"{node()}_block_2024-10-04_13-43-43Z.txt"
)

Expand All @@ -54,11 +56,13 @@ def test_no_rb_number_folder(cb):
patch("ibex_bluesky_core.callbacks._file_logger.os.makedirs") as mock_mkdir,
):
cb.start(run_start)
result = save_path / "Unknown RB" / f"{node()}_block_2024-10-04_13-43-43Z.txt"
result = (
save_path / "Unknown RB" / "bluesky_scans" / f"{node()}_block_2024-10-04_13-43-43Z.txt"
)
assert mock_mkdir.called

mock_file.assert_called_with(result, "a", newline="\n", encoding="utf-8")
# time should have been renamed to start_time and converted to human readable
# time should have been renamed to start_time and converted to human-readable
writelines_call_args = mock_file().writelines.call_args[0][0]
assert "start_time: 2024-10-04_13-43-43\n" in writelines_call_args
assert f"uid: {uid}\n" in writelines_call_args
Expand All @@ -75,7 +79,7 @@ def test_no_motors_doesnt_append_to_filename(cb):
patch("ibex_bluesky_core.callbacks._file_logger.os.makedirs") as mock_mkdir,
):
cb.start(run_start)
result = save_path / "Unknown RB" / f"{node()}_2024-10-04_13-43-43Z.txt"
result = save_path / "Unknown RB" / "bluesky_scans" / f"{node()}_2024-10-04_13-43-43Z.txt"
assert mock_mkdir.called

mock_file.assert_called_with(result, "a", newline="\n", encoding="utf-8")
Expand Down Expand Up @@ -202,7 +206,37 @@ def test_event_called_before_filename_specified_does_nothing():

def test_stop_clears_descriptors(cb):
cb.descriptors["test"] = EventDescriptor(uid="test", run_start="", time=0.1, data_keys={})

cb.stop(RunStop(uid="test", run_start="", time=0.1, exit_status="success"))
with patch("os.chmod"):
cb.stop(RunStop(uid="test", run_start="", time=0.1, exit_status="success"))

assert not cb.descriptors


def test_file_set_readonly_when_finished(cb):
time = 1728049423.5860472
start_uid = "test123start"
stop_uid = "test123stop"
scan_id = 1234
run_start = RunStart(
time=time,
uid=start_uid,
scan_id=scan_id,
rb_number="0",
detectors=["dae"],
motors=("block",),
)
run_stop = RunStop(time=time, run_start=start_uid, uid=stop_uid, exit_status="success")
with (
patch("ibex_bluesky_core.callbacks._file_logger.open", mock_open()),
patch("ibex_bluesky_core.callbacks._file_logger.os.makedirs"),
patch("ibex_bluesky_core.callbacks._file_logger.os.chmod") as mock_chmod,
):
cb.start(run_start)
result = (
save_path
/ f"RB{run_start.get('rb_number', None)}"
/ "bluesky_scans"
/ f"{node()}_block_2024-10-04_13-43-43Z.txt"
)
cb.stop(run_stop)
mock_chmod.assert_called_with(result, S_IRUSR | S_IRGRP | S_IROTH)