Merge remote-tracking branch 'origin/fixes/flomni4' into feat/flomni_tomo_from_angles_file
CI for csaxs_bec / test (push) Successful in 3m7s
CI for csaxs_bec / test (push) Successful in 3m7s
# Conflicts: # csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py # csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params.py
This commit is contained in:
@@ -1924,6 +1924,7 @@ class Flomni(
|
||||
self.tomo_angle_range = 180
|
||||
self.golden_projections_at_0_deg_for_damage_estimation = 0
|
||||
self.zero_deg_reference_at_each_subtomo = False
|
||||
self.tomo_type1_always_forward = False
|
||||
self.golden_ratio_bunch_size = 20
|
||||
self.tomo_angle_file = ""
|
||||
self.frames_per_trigger = 1
|
||||
@@ -2297,7 +2298,7 @@ class Flomni(
|
||||
def zero_deg_reference_at_each_subtomo(self):
|
||||
"""If True (tomo_type == 1 only), an additional projection at exactly
|
||||
0 degrees is acquired at the start of every odd (forward)
|
||||
sub-tomogram - i.e. every time the rotation passes back through
|
||||
sub-tomogram (every sub-tomogram if tomo_type1_always_forward) - i.e. every time the rotation passes back through
|
||||
0 degrees - and once more after the final (8th) sub-tomogram
|
||||
completes. Together with sub-tomogram 1's own first projection
|
||||
(which already lands exactly on 0 degrees), this gives a 0-degree
|
||||
@@ -2314,6 +2315,24 @@ class Flomni(
|
||||
def zero_deg_reference_at_each_subtomo(self, val: bool):
|
||||
self.client.set_global_var("zero_deg_reference_at_each_subtomo", val)
|
||||
|
||||
@property
|
||||
def tomo_type1_always_forward(self):
|
||||
"""If True (tomo_type == 1, tomo_angle_range == 180 only), every
|
||||
sub-tomogram sweeps forward (0 -> 180) instead of alternating
|
||||
forward/reverse. Each sub-tomogram covers the same angle set either
|
||||
way, so the combined grid is unchanged; the rotation just returns to
|
||||
~0 between sub-tomograms. Because every sub-tomogram then starts at
|
||||
0 degrees, zero_deg_reference_at_each_subtomo takes its reference
|
||||
shot before every sub-tomogram instead of every second one."""
|
||||
val = self.client.get_global_var("tomo_type1_always_forward")
|
||||
if val is None:
|
||||
return False
|
||||
return val
|
||||
|
||||
@tomo_type1_always_forward.setter
|
||||
def tomo_type1_always_forward(self, val: bool):
|
||||
self.client.set_global_var("tomo_type1_always_forward", bool(val))
|
||||
|
||||
golden_ratio_bunch_size = _GlobalVarParam(20)
|
||||
|
||||
@property
|
||||
@@ -2475,7 +2494,9 @@ class Flomni(
|
||||
f.write(" ".join(map(str, zeros)) + "\n")
|
||||
|
||||
@staticmethod
|
||||
def _subtomo_angle_plan(subtomo_number, tomo_angle_range, tomo_angle_stepsize, start_angle=None):
|
||||
def _subtomo_angle_plan(
|
||||
subtomo_number, tomo_angle_range, tomo_angle_stepsize, start_angle=None, always_forward=False
|
||||
):
|
||||
"""
|
||||
Pure angle-generation logic for one sub-tomogram of an equally
|
||||
spaced 8-sub-tomogram tomography scan (tomo_type == 1). No device
|
||||
@@ -2520,6 +2541,10 @@ class Flomni(
|
||||
scan would produce, at the same total projection count, with zero
|
||||
redundant measurements anywhere.
|
||||
|
||||
always_forward (bool): 180 mode only - sweep every sub-tomogram
|
||||
forward instead of alternating direction (same angle set per
|
||||
sub-tomogram, only the order changes). Ignored in 360 mode.
|
||||
|
||||
Returns:
|
||||
angles (np.ndarray): the N angles (degrees) for this sub-tomogram.
|
||||
subtomo_offset (int): index of this sub-tomogram's first angle
|
||||
@@ -2549,7 +2574,7 @@ class Flomni(
|
||||
|
||||
if tomo_angle_range == 180:
|
||||
base = 0.0
|
||||
forward = bool(subtomo_number % 2)
|
||||
forward = True if always_forward else bool(subtomo_number % 2)
|
||||
else:
|
||||
mod4 = subtomo_number % 4
|
||||
base = 0.0 if mod4 in (1, 0) else 180.0
|
||||
@@ -2630,7 +2655,10 @@ class Flomni(
|
||||
)
|
||||
subtomo_number = projection_number // N + 1
|
||||
angles, _, _, _ = self._subtomo_angle_plan(
|
||||
subtomo_number, self.tomo_angle_range, self.tomo_angle_stepsize
|
||||
subtomo_number,
|
||||
self.tomo_angle_range,
|
||||
self.tomo_angle_stepsize,
|
||||
always_forward=self.tomo_type1_always_forward,
|
||||
)
|
||||
return subtomo_number, float(angles[projection_number % N])
|
||||
|
||||
@@ -2646,7 +2674,11 @@ class Flomni(
|
||||
print(f"Sub tomo scan with start angle {start_angle} requested.")
|
||||
|
||||
angles, subtomo_offset, N, step = self._subtomo_angle_plan(
|
||||
subtomo_number, self.tomo_angle_range, self.tomo_angle_stepsize, start_angle=start_angle
|
||||
subtomo_number,
|
||||
self.tomo_angle_range,
|
||||
self.tomo_angle_stepsize,
|
||||
start_angle=start_angle,
|
||||
always_forward=self.tomo_type1_always_forward,
|
||||
)
|
||||
|
||||
for i, angle in enumerate(angles):
|
||||
@@ -2679,7 +2711,7 @@ class Flomni(
|
||||
shot: forcing that shot before a sub-tomogram that doesn't actually
|
||||
start near 0 would mean a large, wasted detour."""
|
||||
if self.tomo_angle_range == 180:
|
||||
return bool(subtomo_number % 2)
|
||||
return self.tomo_type1_always_forward or bool(subtomo_number % 2)
|
||||
return subtomo_number % 4 == 1
|
||||
|
||||
@staticmethod
|
||||
@@ -4145,8 +4177,15 @@ class Flomni(
|
||||
)
|
||||
if self.tomo_angle_range == 360:
|
||||
print("There are no duplicate angles.")
|
||||
if self.tomo_angle_range == 180:
|
||||
print(f"All sub-tomos forward (0->180) = {self.tomo_type1_always_forward}")
|
||||
zero_deg_where = (
|
||||
"every sub-tomo start + end"
|
||||
if self.tomo_angle_range == 180 and self.tomo_type1_always_forward
|
||||
else "odd sub-tomo start + end"
|
||||
)
|
||||
print(
|
||||
"0-deg reference shots (odd sub-tomo start + end) ="
|
||||
f"0-deg reference shots ({zero_deg_where}) ="
|
||||
f" {self.zero_deg_reference_at_each_subtomo}"
|
||||
)
|
||||
if self.tomo_type == 2:
|
||||
@@ -4301,9 +4340,18 @@ class Flomni(
|
||||
)
|
||||
if self.tomo_angle_range == 360:
|
||||
print("There are no duplicate angles.")
|
||||
if self.tomo_angle_range == 180:
|
||||
self.tomo_type1_always_forward = bool(
|
||||
self._get_val(
|
||||
"Run all sub-tomos forward (0->180) instead of alternating 1/0?",
|
||||
int(self.tomo_type1_always_forward),
|
||||
int,
|
||||
)
|
||||
)
|
||||
self.zero_deg_reference_at_each_subtomo = bool(
|
||||
self._get_val(
|
||||
"Take 0-deg reference shots (start of each odd sub-tomo + end) for"
|
||||
"Take 0-deg reference shots (start of each odd sub-tomo, or every"
|
||||
" sub-tomo if run forward, + end) for"
|
||||
" damage estimation 1/0?",
|
||||
int(self.zero_deg_reference_at_each_subtomo),
|
||||
int,
|
||||
@@ -4447,6 +4495,7 @@ class Flomni(
|
||||
"golden_max_number_of_projections",
|
||||
"golden_projections_at_0_deg_for_damage_estimation",
|
||||
"zero_deg_reference_at_each_subtomo",
|
||||
"tomo_type1_always_forward",
|
||||
"tomo_angle_file",
|
||||
"corridor_size",
|
||||
"at_each_angle_hook",
|
||||
|
||||
@@ -97,7 +97,11 @@ TOMO_TYPES = {
|
||||
# _job_tooltip() so a queued job's hover details don't show fields that are
|
||||
# meaningless for its own tomo_type (e.g. golden-ratio settings on a type-1
|
||||
# job), on both flomni and lamni.
|
||||
_TYPE1_ONLY_PARAMS = {"tomo_angle_range", "zero_deg_reference_at_each_subtomo"}
|
||||
_TYPE1_ONLY_PARAMS = {
|
||||
"tomo_angle_range",
|
||||
"zero_deg_reference_at_each_subtomo",
|
||||
"tomo_type1_always_forward",
|
||||
}
|
||||
_TYPE2_ONLY_PARAMS = {"golden_ratio_bunch_size"}
|
||||
_TYPE23_PARAMS = {
|
||||
"golden_max_number_of_projections",
|
||||
@@ -183,6 +187,7 @@ QUEUE_PARAM_NAMES = (
|
||||
"golden_max_number_of_projections",
|
||||
"golden_projections_at_0_deg_for_damage_estimation",
|
||||
"zero_deg_reference_at_each_subtomo",
|
||||
"tomo_type1_always_forward",
|
||||
"tomo_angle_file",
|
||||
"corridor_size",
|
||||
"at_each_angle_hook",
|
||||
@@ -208,6 +213,7 @@ DEFAULTS: dict[str, Any] = {
|
||||
"tomo_angle_range": 180,
|
||||
"tomo_angle_stepsize": 10.0,
|
||||
"zero_deg_reference_at_each_subtomo": False,
|
||||
"tomo_type1_always_forward": False,
|
||||
"golden_ratio_bunch_size": 20,
|
||||
"golden_max_number_of_projections": 1000.0,
|
||||
"golden_projections_at_0_deg_for_damage_estimation": 0,
|
||||
@@ -707,6 +713,9 @@ class OMNY_TomoParams(BECWidget, QWidget):
|
||||
self._pw["tomo_angle_range"].currentIndexChanged.connect(
|
||||
self._update_projection_preview
|
||||
)
|
||||
self._pw["tomo_angle_range"].currentIndexChanged.connect(
|
||||
self._update_always_forward_enabled
|
||||
)
|
||||
form.addRow("Angle range:", self._pw["tomo_angle_range"])
|
||||
|
||||
# requested total projections (user entry in edit mode)
|
||||
@@ -725,6 +734,8 @@ class OMNY_TomoParams(BECWidget, QWidget):
|
||||
|
||||
if self._profile["has_zero_deg_reference"]:
|
||||
self._add_bool(form, "zero_deg_reference_at_each_subtomo", "0° reference each sub-tomo")
|
||||
if self._profile.get("has_always_forward"):
|
||||
self._add_bool(form, "tomo_type1_always_forward", "Run all sub-tomos forward")
|
||||
return box
|
||||
|
||||
def _build_type23_section(self) -> QGroupBox:
|
||||
@@ -1316,8 +1327,10 @@ class OMNY_TomoParams(BECWidget, QWidget):
|
||||
|
||||
def _enter_edit_mode_with(self, params: dict[str, Any]) -> None:
|
||||
self._populate_fields(params)
|
||||
self._set_fields_enabled(True)
|
||||
# _edit_mode must be set first: _set_fields_enabled() consults it
|
||||
# (e.g. for the 'run all sub-tomos forward' checkbox).
|
||||
self._edit_mode = True
|
||||
self._set_fields_enabled(True)
|
||||
self._btn_edit.setVisible(False)
|
||||
self._btn_submit.setVisible(True)
|
||||
self._btn_add_edited_to_queue.setVisible(True)
|
||||
@@ -1500,6 +1513,14 @@ class OMNY_TomoParams(BECWidget, QWidget):
|
||||
self, "Added to queue", f"Added '{jobs[-1]['label']}' to the queue."
|
||||
)
|
||||
|
||||
def _update_always_forward_enabled(self, *_args) -> None:
|
||||
"""'Run all sub-tomos forward' only applies to the 180 degree range."""
|
||||
cb = self._pw.get("tomo_type1_always_forward")
|
||||
combo = self._pw.get("tomo_angle_range")
|
||||
if cb is None or combo is None:
|
||||
return
|
||||
cb.setEnabled(self._edit_mode and combo.currentData() == 180)
|
||||
|
||||
def _set_fields_enabled(self, enabled: bool) -> None:
|
||||
skip = {"_requested_total"} if not enabled else set()
|
||||
for key, widget in self._pw.items():
|
||||
@@ -1518,6 +1539,8 @@ class OMNY_TomoParams(BECWidget, QWidget):
|
||||
self._pw["_requested_total"].setEnabled(enabled)
|
||||
if self._sec_type4 is not None:
|
||||
self._btn_browse_angle_file.setEnabled(enabled)
|
||||
if enabled:
|
||||
self._update_always_forward_enabled()
|
||||
|
||||
# ── queue dialog ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -2675,6 +2698,7 @@ SETUP_PROFILES: dict[str, dict[str, Any]] = {
|
||||
# tomo_type 4 (angles from file) is flomni-only
|
||||
"tomo_types": (1, 2, 3, 4),
|
||||
"has_zero_deg_reference": True,
|
||||
"has_always_forward": True,
|
||||
"golden_bunch_size_min": 1,
|
||||
"fov_fields": [
|
||||
("fovx", "FOV x (µm)", 0.1, 220.0, 2),
|
||||
|
||||
@@ -5,13 +5,32 @@ import numpy as np
|
||||
from ophyd import Device, Component as Cpt, Kind, Signal
|
||||
from ophyd_devices import PreviewSignal
|
||||
import traceback
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
# The camera server passes "exposure" straight to V4L2 exposure_time_absolute,
|
||||
# which is in units of 100 us.
|
||||
EXPOSURE_UNIT_MS = 0.1
|
||||
# V4L2 auto_exposure menu values used by the camera server.
|
||||
AUTO_EXPOSURE_ON = 3 # aperture priority
|
||||
AUTO_EXPOSURE_OFF = 1 # manual
|
||||
SETTINGS_TIMEOUT_S = 3
|
||||
|
||||
|
||||
class WebcamViewer(Device):
|
||||
USER_ACCESS = ["start_live_mode", "stop_live_mode"]
|
||||
USER_ACCESS = [
|
||||
"start_live_mode",
|
||||
"stop_live_mode",
|
||||
"get_exposure_time",
|
||||
"set_exposure_time",
|
||||
"set_auto_exposure_enabled",
|
||||
"set_gain",
|
||||
"set_brightness",
|
||||
"refresh_settings",
|
||||
]
|
||||
preview = Cpt(PreviewSignal, ndim=2, num_rotation_90=0, transpose=False)
|
||||
live_mode_enabled = Cpt(
|
||||
Signal,
|
||||
@@ -20,6 +39,24 @@ class WebcamViewer(Device):
|
||||
doc="Enable or disable live mode.",
|
||||
kind=Kind.config,
|
||||
)
|
||||
exposure_time = Cpt(
|
||||
Signal,
|
||||
name="exposure_time",
|
||||
value=0.0,
|
||||
doc="Camera exposure time (ms). Only effective with auto exposure disabled.",
|
||||
kind=Kind.config,
|
||||
)
|
||||
auto_exposure_enabled = Cpt(
|
||||
Signal,
|
||||
name="auto_exposure_enabled",
|
||||
value=True,
|
||||
doc="Enable/disable camera auto exposure.",
|
||||
kind=Kind.config,
|
||||
)
|
||||
gain = Cpt(Signal, name="gain", value=0.0, doc="Camera gain.", kind=Kind.config)
|
||||
brightness = Cpt(
|
||||
Signal, name="brightness", value=0.0, doc="Camera brightness.", kind=Kind.config
|
||||
)
|
||||
|
||||
def __init__(self, url:str, name:str, num_rotation_90=0, transpose=False, **kwargs) -> None:
|
||||
super().__init__(name=name, **kwargs)
|
||||
@@ -31,7 +68,17 @@ class WebcamViewer(Device):
|
||||
self._live_mode_lock = threading.RLock()
|
||||
self.preview.num_rotation_90 = num_rotation_90
|
||||
self.preview.transpose = transpose
|
||||
parts = urlsplit(url)
|
||||
self._settings_url = f"{parts.scheme}://{parts.netloc}/camera/settings"
|
||||
self._refreshing = False
|
||||
self.live_mode_enabled.subscribe(self._on_live_mode_enabled_changed, run=False)
|
||||
self.exposure_time.subscribe(self._on_exposure_time_changed, run=False)
|
||||
self.auto_exposure_enabled.subscribe(self._on_auto_exposure_enabled_changed, run=False)
|
||||
self.gain.subscribe(self._on_gain_changed, run=False)
|
||||
self.brightness.subscribe(self._on_brightness_changed, run=False)
|
||||
# Load the camera's current settings in the background, so an unreachable
|
||||
# camera server cannot block the device from loading.
|
||||
threading.Thread(target=self._initial_refresh, daemon=True).start()
|
||||
|
||||
def start_live_mode(self) -> None:
|
||||
self.live_mode_enabled.put(True)
|
||||
@@ -39,6 +86,93 @@ class WebcamViewer(Device):
|
||||
def stop_live_mode(self) -> None:
|
||||
self.live_mode_enabled.put(False)
|
||||
|
||||
# ---- camera settings (HTTP /camera/settings on the camera server) ----
|
||||
|
||||
def _post_settings(self, payload: dict) -> None:
|
||||
"""POST settings to the camera server; log instead of raising on failure."""
|
||||
try:
|
||||
response = requests.post(
|
||||
self._settings_url, json=payload, timeout=SETTINGS_TIMEOUT_S
|
||||
)
|
||||
response.raise_for_status()
|
||||
applied = response.json().get("applied", {})
|
||||
bad = {k: v for k, v in applied.items() if isinstance(v, str)}
|
||||
if bad:
|
||||
logger.error(f"{self.name}: camera server rejected settings {bad}")
|
||||
except Exception: # pylint: disable=broad-except
|
||||
logger.error(
|
||||
f"{self.name}: failed to apply {payload} at {self._settings_url}: "
|
||||
f"{traceback.format_exc()}"
|
||||
)
|
||||
|
||||
def _on_exposure_time_changed(self, *args, value, **kwargs) -> None:
|
||||
if self._refreshing:
|
||||
return
|
||||
payload = {"exposure": float(value) / EXPOSURE_UNIT_MS}
|
||||
if self.auto_exposure_enabled.get():
|
||||
logger.warning(
|
||||
f"{self.name}: exposure_time set while auto exposure is enabled; "
|
||||
"it has no effect until set_auto_exposure_enabled(False)."
|
||||
)
|
||||
self._post_settings(payload)
|
||||
|
||||
def _on_auto_exposure_enabled_changed(self, *args, value, **kwargs) -> None:
|
||||
if self._refreshing:
|
||||
return
|
||||
self._post_settings({"auto_exposure": AUTO_EXPOSURE_ON if value else AUTO_EXPOSURE_OFF})
|
||||
|
||||
def _on_gain_changed(self, *args, value, **kwargs) -> None:
|
||||
if not self._refreshing:
|
||||
self._post_settings({"gain": float(value)})
|
||||
|
||||
def _on_brightness_changed(self, *args, value, **kwargs) -> None:
|
||||
if not self._refreshing:
|
||||
self._post_settings({"brightness": float(value)})
|
||||
|
||||
def get_exposure_time(self) -> float:
|
||||
"""Get the cached exposure time (ms). Use refresh_settings() to re-read the camera."""
|
||||
return float(self.exposure_time.get())
|
||||
|
||||
def set_exposure_time(self, value: float) -> None:
|
||||
"""Set the exposure time (ms). Disables auto exposure first, otherwise the camera ignores it."""
|
||||
if self.auto_exposure_enabled.get():
|
||||
self.auto_exposure_enabled.put(False)
|
||||
self.exposure_time.put(value)
|
||||
|
||||
def set_auto_exposure_enabled(self, enable: bool) -> None:
|
||||
self.auto_exposure_enabled.put(bool(enable))
|
||||
|
||||
def set_gain(self, value: float) -> None:
|
||||
self.gain.put(value)
|
||||
|
||||
def set_brightness(self, value: float) -> None:
|
||||
self.brightness.put(value)
|
||||
|
||||
def _initial_refresh(self) -> None:
|
||||
try:
|
||||
self.refresh_settings()
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
logger.warning(f"{self.name}: could not read camera settings from {self._settings_url}: {exc}")
|
||||
|
||||
def refresh_settings(self) -> dict:
|
||||
"""Read the current settings from the camera server into the Signals."""
|
||||
response = requests.get(self._settings_url, timeout=SETTINGS_TIMEOUT_S)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
self._refreshing = True
|
||||
try:
|
||||
if "exposure" in data:
|
||||
self.exposure_time.put(float(data["exposure"]) * EXPOSURE_UNIT_MS)
|
||||
if "auto_exposure" in data:
|
||||
self.auto_exposure_enabled.put(float(data["auto_exposure"]) >= 2.0)
|
||||
if "gain" in data:
|
||||
self.gain.put(float(data["gain"]))
|
||||
if "brightness" in data:
|
||||
self.brightness.put(float(data["brightness"]))
|
||||
finally:
|
||||
self._refreshing = False
|
||||
return data
|
||||
|
||||
def _on_live_mode_enabled_changed(self, *args, value, **kwargs) -> None:
|
||||
self._apply_live_mode(bool(value))
|
||||
|
||||
|
||||
@@ -382,6 +382,8 @@ See [Angles from a file](user.ptychography.flomni.angle_file) below for tomo_typ
|
||||
|
||||
For the "8 sub-tomograms" mode, `flomni.tomo_parameters()` also offers a `zero_deg_reference_at_each_subtomo` option: when enabled, an additional reference projection at exactly 0 degrees is acquired at the start of every odd (forward) sub-tomogram and once more after the final sub-tomogram, useful for tracking radiation damage across the full measurement.
|
||||
|
||||
By default odd sub-tomograms sweep 0→180° and even ones 180→0°, so the reference is taken before every second sub-tomogram. With the `tomo_type1_always_forward` option ("Run all sub-tomos forward", 180° range only), every sub-tomogram sweeps 0→180°; the rotation returns to 0° between sub-tomograms and a 0° reference is taken before each of the 8 (plus one at the end). The angle set of every sub-tomogram, and thus the combined angular grid, is unchanged.
|
||||
|
||||
`flomni.tomo_parameters()` also offers `stitch_x`/`stitch_y` (integers, default 0, up to 50 each) for a field of view larger than a single Fermat scan can cover: when nonzero, every projection angle is acquired as a `(2*stitch_x+1) × (2*stitch_y+1)` grid of separate Fermat scans instead of a single one, tiled around the normal alignment-corrected center and spaced by `fovx`/`fovy` minus `tomo_stitch_overlap` (µm, default 0.2) per step in x/y — stitching together an effective field of view of roughly `(2*stitch_x+1) × fovx` by `(2*stitch_y+1) × fovy`, with `tomo_stitch_overlap` controlling the deliberate overlap between adjacent tiles used for aligning them together during reconstruction. Stitching is forced to 0 (disabled) whenever `single_point_instead_of_fermat_scan` is enabled.
|
||||
|
||||
The parameters above can be used to __restart an interrupted acquisition__ manually, or - more conveniently - by running
|
||||
|
||||
@@ -151,6 +151,37 @@ def test_resume_round_trip(tomo_angle_range, stepsize):
|
||||
assert offset == i, f"n={n} i={i} range={tomo_angle_range} stepsize={stepsize}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("stepsize", STEPSIZES)
|
||||
def test_always_forward_180_mode(stepsize):
|
||||
"""always_forward: every sub-tomogram ascends, covers exactly the same
|
||||
angle set as the default alternating scheme, and resume round-trips."""
|
||||
for n in range(1, 9):
|
||||
default, _, N, _ = plan(n, 180, stepsize)
|
||||
fwd, _, _, _ = plan(n, 180, stepsize, always_forward=True)
|
||||
assert np.all(np.diff(fwd) > 0)
|
||||
assert np.allclose(np.sort(default), np.sort(fwd), atol=1e-9)
|
||||
for i in range(N):
|
||||
_, offset, _, _ = plan(n, 180, stepsize, start_angle=fwd[i], always_forward=True)
|
||||
assert offset == i
|
||||
|
||||
|
||||
def test_always_forward_ignored_in_360_mode():
|
||||
for n in range(1, 9):
|
||||
a, _, _, _ = plan(n, 360, 10.0)
|
||||
b, _, _, _ = plan(n, 360, 10.0, always_forward=True)
|
||||
assert np.array_equal(a, b)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("always_forward", [False, True])
|
||||
@pytest.mark.parametrize("n", range(1, 9))
|
||||
def test_subtomo_starts_near_zero_180(n, always_forward):
|
||||
flomni = Flomni.__new__(Flomni)
|
||||
flomni.client = types.SimpleNamespace(
|
||||
get_global_var=lambda name: {"tomo_angle_range": 180, "tomo_type1_always_forward": always_forward}.get(name)
|
||||
)
|
||||
assert flomni._subtomo_starts_near_zero(n) is (always_forward or bool(n % 2))
|
||||
|
||||
|
||||
def _actual_grid(stepsize):
|
||||
N = int(180.0 / stepsize)
|
||||
step = 180.0 / N
|
||||
|
||||
Reference in New Issue
Block a user