Files
csaxs_bec/tests/tests_bec_ipython_client/test_angle_file.py
T
x01dcandClaude Opus 5.5 35ac94cfd5
CI for csaxs_bec / test (push) Successful in 3m7s
feat(flomni): tomo_type 4 -- angles read from a file
New angular sampling mode: tomo_angle_file points to a plain-text file
with one angle (0-360 deg) per line, optional 2nd column = sub-tomogram
number, '#' comments. Only the path is stored; the file is re-read at
every tomo_scan() start, resume and queued-job run, so an edited file
always takes effect on the next (re)start. The parsed angle list is
logged to scilog at scan start.

- OMNY_shared/angle_file.py: shared pure parser + summary, incl.
  warning for 180-deg-opposite/duplicate angles
- flomni: tomo_type 4 branch in tomo_scan()/tomo_scan_resume(), bad
  file fails before any acquisition, angles >180 not gated by
  tomo_angle_range, CLI prompt/display, PDF report
- tomo queue: path snapshotted per job; setter does not validate so
  stale paths never break non-type-4 jobs; reacquire bounds by file
- TomoParams widget: type 4 (flomni only) with Browse... file dialog,
  live content preview, validation blocking Submit/Add to queue
- webpage: show angle file in param tables

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:46:56 +02:00

108 lines
3.5 KiB
Python

"""Tests for the tomo_type-4 angle file parser (OMNY_shared/angle_file.py)."""
import pytest
from csaxs_bec.bec_ipython_client.plugins.OMNY_shared.angle_file import (
AngleFileError,
describe_opposite_pairs,
describe_summary,
read_angle_file,
summarize_angles,
)
def _write(tmp_path, text, name="angles.txt"):
path = tmp_path / name
path.write_text(text)
return str(path)
def test_one_column_defaults_to_subtomo_1(tmp_path):
path = _write(tmp_path, "0\n45.5\n360\n")
assert read_angle_file(path) == [(0.0, 1), (45.5, 1), (360.0, 1)]
def test_two_columns_whitespace_and_comma_separators(tmp_path):
path = _write(tmp_path, "0 1\n10\t1\n190.5, 2\n270,3\n")
assert read_angle_file(path) == [(0.0, 1), (10.0, 1), (190.5, 2), (270.0, 3)]
def test_comments_and_blank_lines_ignored_and_order_kept(tmp_path):
path = _write(tmp_path, "# header\n\n90 2 # trailing comment\n \n30 1\n")
assert read_angle_file(path) == [(90.0, 2), (30.0, 1)]
def test_tilde_expansion(tmp_path, monkeypatch):
monkeypatch.setenv("HOME", str(tmp_path))
_write(tmp_path, "12\n")
assert read_angle_file("~/angles.txt") == [(12.0, 1)]
@pytest.mark.parametrize(
"text, match",
[
("-0.1\n", "outside"),
("360.01\n", "outside"),
("abc\n", "invalid angle"),
("10 x\n", "invalid sub-tomogram"),
("10 0\n", ">= 1"),
("10 1 5\n", "expected"),
("# only comments\n\n", "no angles"),
],
)
def test_invalid_content_raises_with_reason(tmp_path, text, match):
path = _write(tmp_path, text)
with pytest.raises(AngleFileError, match=match):
read_angle_file(path)
def test_error_reports_line_number(tmp_path):
path = _write(tmp_path, "# c\n10\nbad\n")
with pytest.raises(AngleFileError, match="line 3"):
read_angle_file(path)
def test_missing_file_and_empty_path_raise(tmp_path):
with pytest.raises(AngleFileError, match="Cannot read"):
read_angle_file(str(tmp_path / "nope.txt"))
with pytest.raises(AngleFileError, match="No angle file"):
read_angle_file("")
def test_angle_file_error_is_a_value_error():
assert issubclass(AngleFileError, ValueError)
def test_summary_counts():
summary = summarize_angles([(0.0, 1), (45.0, 1), (100.0, 3)])
assert summary["n"] == 3
assert summary["min"] == 0.0
assert summary["max"] == 100.0
assert summary["n_subtomos"] == 2
assert summary["opposite_pairs"] == []
assert describe_summary(summary) == "3 angles, 0.00-100.00 deg, 2 sub-tomograms"
assert describe_opposite_pairs(summary) is None
def test_summary_detects_180_opposite_and_exact_duplicates():
entries = [(10.0, 1), (190.0, 1), (50.0, 1), (50.0, 2), (120.0, 1)]
pairs = summarize_angles(entries)["opposite_pairs"]
assert [(a, b) for a, b, _, _ in pairs] == [(0, 1), (2, 3)]
def test_summary_detects_pair_across_the_mod_180_wrap():
# 179.99 and 0.0 are 0.01 apart mod 180; so are 359.99 and 0.0
pairs = summarize_angles([(0.0, 1), (90.0, 1), (359.99, 1)])["opposite_pairs"]
assert [(a, b) for a, b, _, _ in pairs] == [(0, 2)]
def test_summary_no_false_positives_for_equally_spaced_360():
# 0, 7, 14, ... 353: 360/7 is not an integer, so no 180-degree pairs
entries = [(float(a), 1) for a in range(0, 360, 7)]
assert summarize_angles(entries)["opposite_pairs"] == []
def test_summary_flags_every_pair_for_equally_spaced_360_with_even_count():
entries = [(float(a), 1) for a in range(0, 360, 10)]
assert len(summarize_angles(entries)["opposite_pairs"]) == 18