kiwi-scan 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. kiwi_scan/__init__.py +192 -0
  2. kiwi_scan/actuator/factory.py +69 -0
  3. kiwi_scan/actuator/multi.py +116 -0
  4. kiwi_scan/actuator/single.py +216 -0
  5. kiwi_scan/actuator_concrete/single_epics.py +512 -0
  6. kiwi_scan/actuator_concrete/single_simulation.py +377 -0
  7. kiwi_scan/actuator_concrete/undulator.py +119 -0
  8. kiwi_scan/actuator_runner.py +613 -0
  9. kiwi_scan/dataloader.py +95 -0
  10. kiwi_scan/datamodels.py +355 -0
  11. kiwi_scan/epics_wrapper.py +243 -0
  12. kiwi_scan/manifestfiles.py +118 -0
  13. kiwi_scan/manifestwriter.py +537 -0
  14. kiwi_scan/metadata_loader.py +150 -0
  15. kiwi_scan/monitor/__init__.py +0 -0
  16. kiwi_scan/monitor/base.py +26 -0
  17. kiwi_scan/monitor/factory.py +51 -0
  18. kiwi_scan/monitor_concrete/__init__.py +0 -0
  19. kiwi_scan/monitor_concrete/print.py +186 -0
  20. kiwi_scan/monitor_concrete/queue_plotter.py +138 -0
  21. kiwi_scan/plotter.py +259 -0
  22. kiwi_scan/plugin/__init__.py +0 -0
  23. kiwi_scan/plugin/base.py +160 -0
  24. kiwi_scan/plugin/registry.py +33 -0
  25. kiwi_scan/plugin_concrete/__init__.py +0 -0
  26. kiwi_scan/plugin_concrete/jog_pid.py +165 -0
  27. kiwi_scan/plugin_concrete/logging.py +58 -0
  28. kiwi_scan/postmortem.py +77 -0
  29. kiwi_scan/scan/__init__.py +0 -0
  30. kiwi_scan/scan/common.py +1215 -0
  31. kiwi_scan/scan/metadata_monitor.py +258 -0
  32. kiwi_scan/scan/range_exit_detector.py +57 -0
  33. kiwi_scan/scan/registry.py +131 -0
  34. kiwi_scan/scan/scan_abs.py +98 -0
  35. kiwi_scan/scan/subscription_manager.py +511 -0
  36. kiwi_scan/scan/sync_controller.py +123 -0
  37. kiwi_scan/scan/tools.py +130 -0
  38. kiwi_scan/scan/trigger_manager.py +149 -0
  39. kiwi_scan/scan_concrete/__init__.py +0 -0
  40. kiwi_scan/scan_concrete/approach.py +53 -0
  41. kiwi_scan/scan_concrete/cm.py +206 -0
  42. kiwi_scan/scan_concrete/linear.py +114 -0
  43. kiwi_scan/scan_concrete/poll.py +177 -0
  44. kiwi_scan/scan_runner.py +316 -0
  45. kiwi_scan/scanplotter_cli.py +184 -0
  46. kiwi_scan/stats.py +162 -0
  47. kiwi_scan/test_support.py +198 -0
  48. kiwi_scan/yaml_loader.py +114 -0
  49. kiwi_scan-0.2.0.dist-info/LICENSE +21 -0
  50. kiwi_scan-0.2.0.dist-info/METADATA +813 -0
  51. kiwi_scan-0.2.0.dist-info/RECORD +54 -0
  52. kiwi_scan-0.2.0.dist-info/WHEEL +5 -0
  53. kiwi_scan-0.2.0.dist-info/entry_points.txt +5 -0
  54. kiwi_scan-0.2.0.dist-info/top_level.txt +1 -0
kiwi_scan/__init__.py ADDED
@@ -0,0 +1,192 @@
1
+ # SPDX-FileCopyrightText: 2026 Helmholtz-Zentrum Berlin für Materialien und Energie GmbH
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """
5
+ This module provides the startup helpers for the import of scans and plugins.
6
+
7
+ Loading order:
8
+ 1. Built-in
9
+ 2. Optional
10
+
11
+ __all__:
12
+ - Plugin classes register themselves via decorators at import time.
13
+ - If a module is not imported, its class is not registered.
14
+ - Debug logging here helps diagnose missing registrations
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import hashlib
20
+ import importlib
21
+ import importlib.util
22
+ import logging
23
+ import os
24
+ import pkgutil
25
+ import sys
26
+ from pathlib import Path
27
+ from typing import Set
28
+ from kiwi_scan.scan.registry import load_all_scan_types
29
+ from importlib.metadata import version, PackageNotFoundError
30
+
31
+ __all__ = ["__version__", "load_all_plugins", "load_all_scan_types"]
32
+ try:
33
+ __version__ = version("kiwi-scan")
34
+ except PackageNotFoundError:
35
+ # fallback for editable installs / no metadata
36
+ __version__ = "0+unknown"
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+ PLUGIN_ENVVAR = "KIWI_SCAN_PLUGIN_PATH"
41
+
42
+
43
+ def _module_suffix_for_path(pyfile: Path) -> str:
44
+ """
45
+ Avoids module-name collisions when two different plugin directories
46
+ contain files with the same basename.
47
+ """
48
+ resolved = str(pyfile.resolve())
49
+ digest = hashlib.sha1(resolved.encode("utf-8")).hexdigest()[:12]
50
+ return f"{pyfile.stem}_{digest}"
51
+
52
+
53
+ def _import_plugin_file(pyfile: Path, raise_on_error: bool = False) -> None:
54
+ """
55
+ Import plugin module.
56
+ """
57
+ pyfile = pyfile.resolve()
58
+ module_name = f"kiwi_scan_ext_{_module_suffix_for_path(pyfile)}"
59
+
60
+ try:
61
+ logger.debug("Importing plugin file: %s as %s", pyfile, module_name)
62
+
63
+ spec = importlib.util.spec_from_file_location(module_name, pyfile)
64
+ if spec is None or spec.loader is None:
65
+ raise ImportError(f"Could not create import spec for {pyfile}")
66
+
67
+ module = importlib.util.module_from_spec(spec)
68
+ sys.modules[module_name] = module
69
+ spec.loader.exec_module(module)
70
+
71
+ logger.debug("Imported plugin file successfully: %s", pyfile)
72
+
73
+ except Exception:
74
+ logger.exception("Failed to import plugin file %s", pyfile)
75
+ if raise_on_error:
76
+ raise
77
+
78
+
79
+ def _import_package_modules(package_name: str, raise_on_error: bool = False) -> None:
80
+ """
81
+ Import all modules provided by kiwi scan
82
+ """
83
+ try:
84
+ package = importlib.import_module(package_name)
85
+ except Exception:
86
+ logger.exception("Failed to import package %s", package_name)
87
+ if raise_on_error:
88
+ raise
89
+ return
90
+
91
+ package_path = getattr(package, "__path__", None)
92
+ if not package_path:
93
+ logger.debug("Package %s has no __path__; nothing to scan", package_name)
94
+ return
95
+
96
+ modules = sorted(pkgutil.iter_modules(package_path), key=lambda m: m.name)
97
+ logger.debug("Scanning built-in plugin package %s (%d modules)", package_name, len(modules))
98
+
99
+ for module_info in modules:
100
+ if module_info.name.startswith("_"):
101
+ logger.debug("Skipping private module %s.%s", package_name, module_info.name)
102
+ continue
103
+
104
+ fullname = f"{package_name}.{module_info.name}"
105
+ try:
106
+ logger.debug("Importing built-in plugin module %s", fullname)
107
+ importlib.import_module(fullname)
108
+ logger.debug("Imported built-in plugin module %s", fullname)
109
+ except Exception:
110
+ logger.exception("Failed to import built-in plugin module %s", fullname)
111
+ if raise_on_error:
112
+ raise
113
+
114
+
115
+ def load_all_plugins(raise_on_error: bool = False) -> None:
116
+ """
117
+ Import all plugin modules.
118
+ """
119
+ package_root = __package__ or __name__
120
+ builtin_package = f"{package_root}.plugin_concrete"
121
+
122
+ raw = os.environ.get(PLUGIN_ENVVAR, "")
123
+
124
+ logger.debug("Starting plugin loading")
125
+ logger.debug("Builtin plugin package: %s", builtin_package)
126
+ logger.debug("%s=%r", PLUGIN_ENVVAR, raw)
127
+
128
+ # Always load the plugins that ship with kiwi_scan.
129
+ _import_package_modules(builtin_package, raise_on_error=raise_on_error)
130
+
131
+ # Then load any external plugins configured by the user.
132
+ if not raw.strip():
133
+ logger.debug("%s not set; built-in plugins loaded only", PLUGIN_ENVVAR)
134
+ else:
135
+ seen: Set[Path] = set()
136
+
137
+ for entry in raw.split(os.pathsep):
138
+ entry = entry.strip()
139
+ if not entry:
140
+ continue
141
+
142
+ p = Path(entry).expanduser().resolve()
143
+ logger.debug("Considering plugin path entry: %s", p)
144
+
145
+ if not p.exists():
146
+ logger.warning("Plugin path does not exist: %s", p)
147
+ continue
148
+
149
+ if p.is_file():
150
+ if p.suffix != ".py":
151
+ logger.debug("Skipping non-Python file: %s", p)
152
+ continue
153
+
154
+ if p in seen:
155
+ logger.debug("Skipping duplicate plugin file: %s", p)
156
+ continue
157
+
158
+ _import_plugin_file(p, raise_on_error=raise_on_error)
159
+ seen.add(p)
160
+ continue
161
+
162
+ if p.is_dir():
163
+ pyfiles = sorted(
164
+ candidate
165
+ for candidate in p.glob("*.py")
166
+ if not candidate.name.startswith("_")
167
+ )
168
+ logger.debug("Scanning plugin directory %s (%d files)", p, pyfiles.__len__())
169
+
170
+ for pyfile in pyfiles:
171
+ pyfile = pyfile.resolve()
172
+ if pyfile in seen:
173
+ logger.debug("Skipping already imported plugin file: %s", pyfile)
174
+ continue
175
+
176
+ _import_plugin_file(pyfile, raise_on_error=raise_on_error)
177
+ seen.add(pyfile)
178
+
179
+ continue
180
+
181
+ logger.warning("Plugin path is neither a file nor directory: %s", p)
182
+
183
+ try:
184
+ from kiwi_scan.plugin.registry import PLUGIN_REGISTRY
185
+
186
+ logger.debug(
187
+ "Plugin registry now contains %d entries: %s",
188
+ len(PLUGIN_REGISTRY),
189
+ sorted(PLUGIN_REGISTRY.keys()),
190
+ )
191
+ except Exception:
192
+ logger.exception("Could not inspect plugin registry after loading")
@@ -0,0 +1,69 @@
1
+ # SPDX-FileCopyrightText: 2026 Helmholtz-Zentrum Berlin für Materialien und Energie GmbH
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ from typing import Type, Dict, Optional
5
+
6
+ from kiwi_scan.datamodels import ActuatorConfig
7
+ from kiwi_scan.actuator_concrete.single_epics import EpicsActuator
8
+ from kiwi_scan.actuator_concrete.single_simulation import SimulatedActuator
9
+ from kiwi_scan.actuator.single import AbstractActuator
10
+
11
+
12
+ class ActuatorFactory:
13
+ """
14
+ Factory for creating actuator instances based on configuration.
15
+
16
+ Usage:
17
+ # simple flag-based simulation
18
+ actuator = ActuatorFactory.create(config, use_simulation=True)
19
+
20
+ # config-based (EPICS)
21
+ actuator = ActuatorFactory.create(config)
22
+ """
23
+ # Registry mapping type keys to actuator classes
24
+ _registry: Dict[str, Type[AbstractActuator]] = {
25
+ 'epics': EpicsActuator,
26
+ 'sim': SimulatedActuator,
27
+ }
28
+
29
+ @classmethod
30
+ def create(
31
+ cls,
32
+ config: ActuatorConfig,
33
+ use_simulation: Optional[bool] = None,
34
+ actuator_type: Optional[str] = None,
35
+ ) -> AbstractActuator:
36
+ """
37
+ Create an actuator based on:
38
+ - explicit `use_simulation` flag (overrides everything)
39
+ - `actuator_type` argument ('epics' or 'sim')
40
+ - `config.actuator_type` attribute, if defined in your datamodel
41
+
42
+ Falls back to EpicsActuator.
43
+ """
44
+ # Determine type key
45
+ if use_simulation is True:
46
+ key = 'sim'
47
+ elif use_simulation is False:
48
+ key = 'epics'
49
+ else:
50
+ # try explicit override
51
+ key = actuator_type or getattr(config, 'actuator_type', None)
52
+ # default if missing
53
+ key = key or 'epics'
54
+
55
+ try:
56
+ actuator_cls = cls._registry[key]
57
+ except KeyError:
58
+ raise ValueError(f"Unknown actuator type '{key}'. "
59
+ f"Supported types: {list(cls._registry)}")
60
+
61
+ return actuator_cls(config)
62
+
63
+
64
+ def create_actuator(config: ActuatorConfig) -> AbstractActuator:
65
+ key = config.type # “epics” or “sim”
66
+ cls = ActuatorFactory._registry.get(key)
67
+ if cls is None:
68
+ raise ValueError(f"Unknown type {key}")
69
+ return cls(config)
@@ -0,0 +1,116 @@
1
+ # SPDX-FileCopyrightText: 2026 Helmholtz-Zentrum Berlin für Materialien und Energie GmbH
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ from typing import Sequence, Optional, Any
5
+ import time
6
+ from kiwi_scan.actuator.single import AbstractActuator
7
+ from kiwi_scan.datamodels import ActuatorConfig
8
+
9
+ import logging
10
+ import time
11
+ from typing import Sequence, Optional, Any
12
+
13
+ import numpy as np
14
+ from kiwi_scan.actuator.single import AbstractActuator
15
+ from kiwi_scan.datamodels import ActuatorConfig
16
+ from kiwi_scan.epics_wrapper import EpicsPV
17
+
18
+ class MultiActuator(AbstractActuator):
19
+ """
20
+ A composite actuator that lets you jog N child actuators in one call.
21
+ """
22
+ def __init__(self, actuators: Sequence[AbstractActuator], config: ActuatorConfig):
23
+ super().__init__(config)
24
+ self._axes = list(actuators)
25
+
26
+ @property
27
+ def pvname(self) -> Sequence[str]:
28
+ return [ax.pvname for ax in self._axes]
29
+
30
+ @property
31
+ def rbv(self) -> Sequence[Any]:
32
+ return [ax.rbv for ax in self._axes]
33
+
34
+ @rbv.setter
35
+ def rbv(self, values: Sequence[Any]) -> None:
36
+ if len(values) != len(self._axes):
37
+ raise ValueError(f"Expected {len(self._axes)} values, got {len(values)}")
38
+ for ax, v in zip(self._axes, values):
39
+ ax.rbv = v
40
+
41
+ @property
42
+ def cmdv(self) -> Sequence[Any]:
43
+ return [ax.cmdv for ax in self._axes]
44
+
45
+ @cmdv.setter
46
+ def cmdv(self, values: Sequence[Any]) -> None:
47
+ if len(values) != len(self._axes):
48
+ raise ValueError(f"Expected {len(self._axes)} values, got {len(values)}")
49
+ for ax, v in zip(self._axes, values):
50
+ ax.cmdv = v
51
+
52
+ # Implemented abstract methods:
53
+ def set_velocity(self, velocities: Sequence[float]) -> None:
54
+ if len(velocities) != len(self._axes):
55
+ raise ValueError(f"Need {len(self._axes)} velocities, got {len(velocities)}")
56
+ for ax, v in zip(self._axes, velocities):
57
+ ax.set_velocity(v)
58
+
59
+ def get_velocity(self) -> Sequence[Optional[float]]:
60
+ return [ax.get_velocity() for ax in self._axes]
61
+
62
+ def move(self, positions: Sequence[float]) -> None:
63
+ # non-blocking multi-axis move
64
+ self.run_move(positions, sync=False)
65
+
66
+ def rel_move(self, deltas: Sequence[float]) -> None:
67
+ """Non-blocking multi-axis relative move."""
68
+ self.run_rel_move(deltas, sync=False)
69
+
70
+ def run_move(self, positions: Sequence[float], sync: bool = True) -> None:
71
+ if len(positions) != len(self._axes):
72
+ raise ValueError(f"Expected {len(self._axes)} positions, got {len(positions)}")
73
+ # issue each child move without blocking
74
+ for ax, pos in zip(self._axes, positions):
75
+ ax.run_move(pos, sync=False)
76
+ if sync:
77
+ # wait until all axes are ready
78
+ while not self.is_ready():
79
+ time.sleep(0.01)
80
+
81
+ def run_rel_move(self, deltas: Sequence[float], sync: bool = True) -> None:
82
+ if len(deltas) != len(self._axes):
83
+ raise ValueError(f"Expected {len(self._axes)} deltas, got {len(deltas)}")
84
+ # issue each child relative move without blocking
85
+ for ax, d in zip(self._axes, deltas):
86
+ ax.run_rel_move(float(d), sync=False)
87
+ if sync:
88
+ while not self.is_ready():
89
+ time.sleep(0.01)
90
+
91
+ def jog(self, velocities: Sequence[float], sync: bool = True) -> None:
92
+ if len(velocities) != len(self._axes):
93
+ raise ValueError(f"Need {len(self._axes)} velocities, got {len(velocities)}")
94
+ for ax, v in zip(self._axes, velocities):
95
+ ax.jog(v, sync=False)
96
+ if sync:
97
+ while not self.is_ready():
98
+ time.sleep(0.01)
99
+
100
+ def stop(self) -> None:
101
+ for ax in self._axes:
102
+ ax.stop()
103
+
104
+ def is_ready(self) -> bool:
105
+ # wait for all single actuator conditions
106
+ return all(ax.is_ready() for ax in self._axes)
107
+
108
+ def is_in_position(self, targets: Sequence[float], in_position_band: Optional[float] = None) -> bool:
109
+ band = in_position_band if in_position_band is not None else self.in_position_band
110
+ return all(ax.is_in_position(t, band) for ax, t in zip(self._axes, targets))
111
+
112
+ def wait_until_done(self, positions: Optional[Sequence[float]] = None) -> None:
113
+ # wait until all axes have stopped, single actuators provide in position band etc.
114
+ while not self.is_ready():
115
+ time.sleep(0.01)
116
+
@@ -0,0 +1,216 @@
1
+ # SPDX-FileCopyrightText: 2026 Helmholtz-Zentrum Berlin für Materialien und Energie GmbH
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ from abc import ABC, abstractmethod
5
+ from dataclasses import dataclass
6
+ from typing import Optional, Any, Callable, Dict, Mapping
7
+ import threading
8
+ import time
9
+ import logging
10
+
11
+ from kiwi_scan.datamodels import ActuatorConfig
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class PvEvent:
16
+ """
17
+ Representation of one PV update.
18
+ This decouples from the raw pyepics callback kwargs.
19
+ """
20
+ pvname: str
21
+ value: Any
22
+ source: Optional[str] = None
23
+
24
+ # Time
25
+ timestamp: Optional[float] = None # seconds since epoch (float)
26
+ posixseconds: Optional[int] = None
27
+ nanoseconds: Optional[int] = None
28
+
29
+ # Alarm / status
30
+ severity: Optional[int] = None
31
+ status: Optional[int] = None
32
+
33
+ # Anything else provided by the backend
34
+ raw: Mapping[str, Any] = None
35
+
36
+
37
+ MonitorCallback = Callable[[PvEvent], None]
38
+
39
+
40
+ class AbstractActuator(ABC):
41
+ """
42
+ Defines the interface for an actuator.
43
+ """
44
+
45
+ def __init__(self, config: ActuatorConfig):
46
+ self.config = config
47
+ # Expose common config parameters
48
+ self.backlash: float = config.backlash
49
+ self.in_position_band: float = config.in_position_band
50
+ self.dwell_time: float = config.dwell_time
51
+ self.ready_value: Any = config.ready_value
52
+ self.startup_timeout: float = config.startup_timeout
53
+ self.q_delay: float = config.queueing_delay
54
+
55
+ # --- Monitoring -----------------------
56
+ self._monitors: Dict[str, Any] = {} # pvname -> backend monitor object (e.g. EpicsPV)
57
+ self._monitor_callbacks: Dict[str, list[MonitorCallback]] = {}
58
+ self._last_events: Dict[str, PvEvent] = {}
59
+ self._monitor_lock = threading.RLock()
60
+
61
+ # --------------------- monitor/event helpers ----------------------------
62
+
63
+ def supports_monitors(self) -> bool:
64
+ """Override in backends that can subscribe to PV updates (e.g. EPICS)."""
65
+ return False
66
+
67
+ def add_monitor(self, pvname: str, user_callback: Optional[MonitorCallback] = None, **kwargs) -> Any:
68
+ """
69
+ Subscribe to PV updates. Concrete backends (EpicsActuator) should override.
70
+ Returns a backend-specific monitor handle/object (e.g. EpicsPV).
71
+ """
72
+ raise NotImplementedError(f"{self.__class__.__name__} does not support PV monitors")
73
+
74
+ def remove_monitor(self, pvname: str) -> None:
75
+ """
76
+ Remove/close a monitor subscription for pvname if supported by backend.
77
+ Concrete backends should override if they can actually clear CA subscriptions.
78
+ """
79
+ with self._monitor_lock:
80
+ self._monitors.pop(pvname, None)
81
+ self._monitor_callbacks.pop(pvname, None)
82
+ self._last_events.pop(pvname, None)
83
+
84
+ def clear_monitors(self) -> None:
85
+ """Remove all monitors."""
86
+ with self._monitor_lock:
87
+ pvs = list(self._monitors.keys())
88
+ for pv in pvs:
89
+ self.remove_monitor(pv)
90
+
91
+ def on_pv_event(self, pvname: str, cb: MonitorCallback) -> None:
92
+ """Register an additional listener for pvname (even if monitor already exists)."""
93
+ with self._monitor_lock:
94
+ self._monitor_callbacks.setdefault(pvname, []).append(cb)
95
+
96
+ def get_last_event(self, pvname: str) -> Optional[PvEvent]:
97
+ with self._monitor_lock:
98
+ return self._last_events.get(pvname)
99
+
100
+ def _dispatch_pv_update(self, pvname: str, value: Any, **kw: Any) -> PvEvent:
101
+ """
102
+ Backend-facing entry point: call this from EPICS callbacks.
103
+ Stores last event and notifies listeners.
104
+ """
105
+ ev = PvEvent(
106
+ pvname=pvname,
107
+ value=value,
108
+ timestamp=kw.get("timestamp", None),
109
+ posixseconds=kw.get("posixseconds", None),
110
+ nanoseconds=kw.get("nanoseconds", None),
111
+ severity=kw.get("severity", None),
112
+ status=kw.get("status", None),
113
+ source=kw.get("source", None),
114
+ raw=dict(kw) if kw else {},
115
+ )
116
+
117
+ with self._monitor_lock:
118
+ self._last_events[pvname] = ev
119
+ listeners = list(self._monitor_callbacks.get(pvname, []))
120
+
121
+ # Call listeners outside the lock
122
+ for cb in listeners:
123
+ try:
124
+ cb(ev)
125
+ except Exception:
126
+ # Keep it non-fatal: monitor callbacks must never kill control logic
127
+ logging.exception(f"Monitor callback failed for {pvname}")
128
+
129
+ return ev
130
+ @property
131
+ @abstractmethod
132
+ def pvname(self) -> str:
133
+ """The primary PV name for the actuator."""
134
+ pass
135
+
136
+ @property
137
+ @abstractmethod
138
+ def rbv(self) -> Optional[Any]:
139
+ """Read-back value shortcut property."""
140
+ pass
141
+
142
+ @rbv.setter
143
+ @abstractmethod
144
+ def rbv(self, value: Any) -> None:
145
+ """Read-back value shortcut property setter."""
146
+ pass
147
+
148
+ @property
149
+ @abstractmethod
150
+ def cmdv(self) -> Optional[Any]:
151
+ """Commanded value shortcut property."""
152
+ pass
153
+
154
+ @cmdv.setter
155
+ @abstractmethod
156
+ def cmdv(self, value: Any) -> None:
157
+ """Commanded value shortcut property setter."""
158
+ pass
159
+
160
+ @abstractmethod
161
+ def set_velocity(self, velocity: float) -> None:
162
+ """Set the actuator velocity (for move and jog)."""
163
+ pass
164
+
165
+ @abstractmethod
166
+ def get_velocity(self) -> Optional[float]:
167
+ """Get the current actuator velocity."""
168
+ pass
169
+
170
+ @abstractmethod
171
+ def move(self, position: float) -> None:
172
+ """Issue a move command without waiting."""
173
+ pass
174
+
175
+ @abstractmethod
176
+ def rel_move(self, delta: float) -> None:
177
+ """Issue a *relative* move command without waiting (incremental move)."""
178
+ pass
179
+
180
+ @abstractmethod
181
+ def run_move(self, position: float, sync: bool = True) -> None:
182
+ """Move the actuator, optionally waiting until completion."""
183
+ pass
184
+
185
+ @abstractmethod
186
+ def run_rel_move(self, delta: float, sync: bool = True) -> None:
187
+ """Relative move, optionally waiting until completion."""
188
+ pass
189
+
190
+ @abstractmethod
191
+ def jog(self, velocity: float, sync: bool = True) -> None:
192
+ """Continuous jog at given velocity."""
193
+ pass
194
+
195
+ @abstractmethod
196
+ def is_ready(self) -> bool:
197
+ """Return True if actuator is ready (not is_movingy())."""
198
+ pass
199
+
200
+ def is_moving(self) -> bool:
201
+ return not self.is_ready()
202
+
203
+ @abstractmethod
204
+ def is_in_position(self, target: float, in_position_band: float) -> bool:
205
+ """Check if readback is within band of target."""
206
+ pass
207
+
208
+ @abstractmethod
209
+ def wait_until_done(self, position: float) -> None:
210
+ """Block until actuator has reached target."""
211
+ pass
212
+
213
+ @abstractmethod
214
+ def stop(self) -> None:
215
+ """Stop motion immediately."""
216
+ pass