Skip to content

Implement force calibration with scripting infrastructure - #170

Open
teaguesvendsen287 wants to merge 11 commits into
masterfrom
main-fishel-lab-force-calibration
Open

Implement force calibration with scripting infrastructure#170
teaguesvendsen287 wants to merge 11 commits into
masterfrom
main-fishel-lab-force-calibration

Conversation

@teaguesvendsen287

Copy link
Copy Markdown
Collaborator

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/):

  • 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)

teaguesvendsen287 and others added 5 commits May 22, 2026 15:26
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
7jameslondon force-pushed the main-fishel-lab-force-calibration branch from 65b705a to 51e9d27 Compare May 27, 2026 15:21
- 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants