Implement force calibration with scripting infrastructure - #170
Open
teaguesvendsen287 wants to merge 11 commits into
Open
Implement force calibration with scripting infrastructure#170teaguesvendsen287 wants to merge 11 commits into
teaguesvendsen287 wants to merge 11 commits into
Conversation
Phase 0 - Scripting infrastructure (merged from Fishel-lab-custom-motor-commands): - Add ScriptMoveErrorCommand to ipc_commands.py for motor error reporting - Add LinearMoveCommand, LinearJogCommand, LinearHomeCommand with wait_until_done + _script_move_pending + UpdateWaitingCommand pattern to Zaber LSQ motor - Add RotaryMoveCommand, RotaryJogCommand to Zaber NMS motor - Add FocusMoveCommand, FocusJogCommand to PI E-709 focus motor - Extend ScriptManager._execute_script_step() to support wait_until_done - Add ScriptManager.handle_script_move_error() for script error handling Force calibration package (magscope/force_calibration/): - model.py: ForceCalibrantModel (PCHIP interpolation), ForceRampProfile, ForceCalibrantError - commands.py: 7 IPC command dataclasses for load/unload/move/ramp/status - __init__.py: Lazy re-exports Force calibration UI (examples/motors/force_calibration.py): - ForceCalibrationControlPanel: calibrant loading, target force moves, A-B force ramps with constant pN/s via PCHIP correction, status/fault display - ForcePlot: reads ZaberLsqMotor buffer, converts position-force via shared module-level ForceCalibrantModel, plots actual vs target force Motor IPC handlers (examples/motors/zaber_lsq.py): - 7 force IPC handlers (load, unload, move-to-force, ramp, status) - Scriptable ForceMoveCommand and ForceRampCommand with wait_until_done - _execute_force_profile() / _dispatch_next_profile_segment() for multi-segment PCHIP-corrected moves with callback chaining - callback parameter on handle_linear_move for profile dispatch Main entry point (examples/main_fishel_lab.py): - Register ForceCalibrationControlPanel and ForcePlot in Motors tab Calibration data (examples/motors/force_calibrant.txt): - 1001-row two-column calibration file (position_mm force_pn)
…get force In _dispatch_next_profile_segment, negative velocities from �uild_force_ramp (for decreasing-force moves) were passed directly to handle_linear_move, where p.clip(speed, 0.001, ...) clamped them to 0.001 mm/s. Added �bs() to convert negative speeds to positive magnitudes since direction is already encoded in the target position.
- Switch ForcePlot from peak_unsorted() to peak_sorted() to eliminate stale buffer entries - Remove np.isfinite(force) filter so NaN values pass through to matplotlib (gaps in line) - Add epsilon tolerance to boundary checks to prevent float imprecision from rejecting edge values - Preserve existing plot data on empty update instead of clearing
Add QMessageBox warning in the Move to Force and Force Ramp controls when the entered force value is outside the bounds of the currently loaded force calibrant.
7jameslondon
force-pushed
the
main-fishel-lab-force-calibration
branch
from
May 27, 2026 15:21
65b705a to
51e9d27
Compare
- Remove section headers, keep horizontal dividers - Replace status section with centered force range readout - Reorder ramp buttons above A/B input fields - Center-align rows and resize input fields
- Fix handle_linear_move crash when speed_mm_s is None by preserving current speed - Use average ramp speed (derived from rate pN/s) for pre-position moves, ensuring consistent speed between A->B and B->A ramps - Warn user with Cancel/Proceed dialog when already at the target force position before executing a ramp - Add regression tests for force ramp pre-position speed behavior
Previously the force ramp was built as a list of discrete position-velocity segments (build_force_ramp) dispatched sequentially via move_absolute. This caused stop-start jolts between segments, poor dF/dt accuracy, and inconsistent final positioning. Replace with a single two-phase continuous velocity-mode ramp: - Pre-phase: move_velocity from current position toward start_mm, computing instantaneous velocity as v = rate / dF/dp - Ramp-phase: move_velocity through the whole calibration range, maintaining constant force rate via the same velocity formula - Lookahead: uses dynamic braking-distance estimate (v*dt + v²/2a + tol) to switch from move_velocity to move_absolute at the right moment - Initialization lookahead: if target is within braking distance at ramp start, skip velocity mode entirely and go directly to move_absolute Changes: - Add velocity_for_force_rate() to ForceCalibrantModel for computing instantaneous ramp velocity from the PCHIP derivative - Replace _pending_ramp_profile, _force_profile_segments, _force_profile_index, _force_profile_wait with clean _velocity_ramp_* state attributes - Replace _dispatch_pending_ramp / _execute_force_profile / _dispatch_next_profile_segment with _start_velocity_ramp() and inline fetch-loop logic - Read actual Zaber acceleration via SettingConstants.ACCEL in _refresh_metadata and use it for lookahead instead of the hardcoded RAMP_DECEL_MM_S2 guess - Add ACCELERATION_UNIT and POSITION_TOLERANCE module-level constants - Update handle_stop to clear _velocity_ramp_active - Bump magtrack dependency to >=0.7.7 (required for compatibility) - Update test stubs with ACCEL setting constant, ACCELERATION_UNIT, move_velocity/stop fakes, and velocity-mode ramp assertions
All four custom TimeSeriesPlotBase subclasses (ForcePlot, ZaberLsqPositionPlot, ZaberNmsPositionPlot, PiE709FocusPlot) overrode update() without checking self.parent.time_mode. They always produced datetime xdata, ignoring the relative time window and the relative formatter set by PlotWorker. With sharex=True between all subplots, this caused conflicting data types between the base X/Y/Z tracks plots and the custom plots, breaking axis labels in relative mode. Each custom plot now mirrors the pattern in TracksTimeSeriesPlot.update(): - In relative mode: windows data by relative_window_seconds, produces float-second xdata, sets xlim=(0, window) - In absolute mode: unchanged datetime behavior - Applies y-limits from limits dict in both modes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This branch also includes the changes made in PR #169 as I wanted to make custom commands in scripting to move to specific force values to be consistent with the other motor commands.
Force calibration package (magscope/force_calibration/):
Force calibration UI (examples/motors/force_calibration.py):
Motor IPC handlers (examples/motors/zaber_lsq.py):
Main entry point (examples/main_fishel_lab.py):
Calibration data (examples/motors/force_calibrant.txt):