From c01f94850edef4e8c2f4f0ddf1285d998e3669aa Mon Sep 17 00:00:00 2001 From: Christoffer Fremling Date: Thu, 29 Oct 2026 16:08:58 +0200 Subject: [PATCH] slicecamd: fine-acquire correction budget A target that kept landing just outside the goal was corrected again and again until the sequencer's acquisition timeout. Two slicecamd.cfg keys: FINE_ACQUIRE_MAX_ADJUSTMENTS= (default 3, 0 = previous behaviour): at most corrections are commanded in one run, and the -th decides. It is sent only if the offset it removes is within FINE_ACQUIRE_RELAXED_FACTOR x goal, and the run converges once that move has executed and settled; otherwise the run fails at once without sending it. A correction acamd does not confirm within FINE_ACQUIRE_MOVE_TIMEOUT also fails a budgeted run. Nothing is sampled or evaluated once no correction can follow. FINE_ACQUIRE_RELAXED_FACTOR= (default 2.0, at least 1). Also: a single frame inside the goal after a correction converges the run; FINE_ACQUIRE_SETTLE_SEC=1 (was 3.0) now that the settle starts when the move has executed; every lock writes the same "fine acquisition converged" line, with how it locked appended. Co-Authored-By: Claude Opus 5.5 --- Config/slicecamd.cfg.in | 37 +++++++-- slicecamd/slicecam_interface.cpp | 137 ++++++++++++++++++++++++++++++- slicecamd/slicecam_interface.h | 3 + 3 files changed, 166 insertions(+), 11 deletions(-) diff --git a/Config/slicecamd.cfg.in b/Config/slicecamd.cfg.in index c481b4a3..6bd4d003 100644 --- a/Config/slicecamd.cfg.in +++ b/Config/slicecamd.cfg.in @@ -92,17 +92,38 @@ FINE_ACQUIRE_MIN_SAMPLES=3 FINE_ACQUIRE_SETTLE_FRAMES=0 # FINE_ACQUIRE_SETTLE_SEC= -# Time-based settle applied after each move, in addition to SETTLE_FRAMES, -# to let the TCS physically finish the move before sampling resumes. This matters -# when autoexpose shortens the exposure (e.g. bright targets): the frame-count -# settle then elapses too quickly in wall-clock. Set to 0 to disable. -# -FINE_ACQUIRE_SETTLE_SEC=3.0 +# Pause after each correction, before the next exposure starts. The frame +# loop already waits for acamd to report the move made, which is only after the +# TCS has answered it (#479), so no frame can overlap the move. acamd usually +# reports 1 s after the answer, but sooner if anything requests its status in +# that second; this pause guarantees the settle either way. Set to 0 to disable. +# +FINE_ACQUIRE_SETTLE_SEC=1 + +# FINE_ACQUIRE_MAX_ADJUSTMENTS= +# Correction budget. At most corrections are commanded in one run, and the +# -th decides. It is sent only if the offset it removes is within +# FINE_ACQUIRE_RELAXED_FACTOR x goal; the run then converges once that move has +# executed and settled ("converged (FINAL CORRECTION)"). If the offset is +# larger, the run fails at once without sending it ("fine acquisition FAILED: +# correction of would be the last ..."). A correction acamd does not +# confirm within FINE_ACQUIRE_MOVE_TIMEOUT also fails a budgeted run. The +# sequencer reports a failure as a failed acquisition. Nothing is sampled or +# evaluated once no correction can follow. Needs FINE_ACQUIRE_MOVE_TIMEOUT > 0. +# 0 disables (correct until the goal or the sequencer timeout). +FINE_ACQUIRE_MAX_ADJUSTMENTS=3 + +# FINE_ACQUIRE_RELAXED_FACTOR= +# Largest offset, as a multiple of the 0.4" fine-acquire goal, that the budget's +# last correction may remove: factor 2.0 sends it, and then converges, only if +# that offset is <= 0.8". Must be >= 1 (1 means the last correction is never sent). +FINE_ACQUIRE_RELAXED_FACTOR=2.0 # FINE_ACQUIRE_MOVE_TIMEOUT= # Max time to wait for acamd to report a requested goal change applied -# before the settle begins. On timeout the settle proceeds anyway. Set to 0 -# to disable the wait. +# before the settle begins. On timeout the settle proceeds anyway, except under +# a correction budget, where the run fails. Set to 0 to disable the wait (not +# allowed with FINE_ACQUIRE_MAX_ADJUSTMENTS > 0). # FINE_ACQUIRE_MOVE_TIMEOUT=60.0 diff --git a/slicecamd/slicecam_interface.cpp b/slicecamd/slicecam_interface.cpp index 0ee2da88..edc2420f 100644 --- a/slicecamd/slicecam_interface.cpp +++ b/slicecamd/slicecam_interface.cpp @@ -169,6 +169,7 @@ namespace Slicecam { this->fineacquire_state.reset(); this->fineacq_total_dra = 0.0; // reset per-run ACAM->slit residual accumulators this->fineacq_total_ddec = 0.0; + this->fineacq_adjustments = 0; // relaxed-accept counter, per run // snapshot this run's goal (DB target) coords from the TARGETINFO published message; NAN if no target // has been published (manual runs log nan). Frozen for the run via the is_fineacquire_running handoff. @@ -432,10 +433,14 @@ namespace Slicecam { if ( this->fineacq_total_dra != 0.0 || this->fineacq_total_ddec != 0.0 ) { const double r1 = std::hypot( offsets.first, offsets.second ) * 3600.0; if ( r1 <= this->fineacquire_state.goal_arcsec ) { + // the same converged line as every lock (log readers key on it) std::ostringstream oss; - oss << "fine acquisition converged: single-frame offset r=" << r1 - << " arcsec <= goal=" << this->fineacquire_state.goal_arcsec - << " arcsec (n=" << n << ")"; + oss << "fine acquisition converged: offset dRA=" << offsets.first * 3600.0 + << " dDEC=" << offsets.second * 3600.0 + << " arcsec (r=" << r1 + << " arcsec, n=1 scatter=(0,0) arcsec)" + << " goal=" << this->fineacquire_state.goal_arcsec << " arcsec" + << "; accepted on a single frame after a correction"; logwrite( function, oss.str() ); std::ostringstream acqmodel; @@ -578,6 +583,30 @@ namespace Slicecam { } } + // THE BUDGET'S LAST CORRECTION (FINE_ACQUIRE_MAX_ADJUSTMENTS): the run can + // lock after it only if the offset it would remove is within + // FINE_ACQUIRE_RELAXED_FACTOR x goal, and that offset is known now. When it + // is larger the run fails here, without sending a correction that no lock + // can follow (the sequencer reports a failed acquisition; the robot retries). + // + if ( this->fineacquire_state.max_adjustments > 0 + && this->fineacq_adjustments + 1 >= this->fineacquire_state.max_adjustments + && !( offset_arcsec <= this->fineacquire_state.relaxed_factor + * this->fineacquire_state.goal_arcsec ) ) { + std::ostringstream oss; + oss << "fine acquisition FAILED: correction " << this->fineacq_adjustments + 1 + << " of " << this->fineacquire_state.max_adjustments << " would be the last and r=" + << offset_arcsec << " arcsec > relaxed goal=" + << this->fineacquire_state.relaxed_factor * this->fineacquire_state.goal_arcsec + << " arcsec, so no lock can follow; not sent"; + logwrite( function, oss.str() ); + this->is_fineacquire_locked.store( false, std::memory_order_release ); + this->is_fineacquire_running.store( false, std::memory_order_release ); + this->fineacquire_state.reset(); + this->publish_status(); + return; + } + // select gain: use gain_large when offset is well above the goal threshold // const double effective_gain = ( offset_arcsec > this->fineacquire_state.gain_threshold_arcsec ) @@ -608,6 +637,7 @@ namespace Slicecam { this->publish_status(); return; } + this->fineacq_adjustments++; // one more commanded correction this run (relaxed accept) // while guiding the correction only re-points acamd's goal; wait until // acamd reports the goal change applied before settling @@ -633,6 +663,20 @@ namespace Slicecam { if ( !this->is_fineacquire_running.load(std::memory_order_acquire) ) return; if ( !applied ) { std::ostringstream w; + if ( this->fineacquire_state.max_adjustments > 0 ) { + // A budgeted run decides on the corrections it has commanded, so it + // cannot go on from one that never executed: later frames would + // still show the old position, and the shift may land mid-exposure. + w << "fine acquisition FAILED: correction " << this->fineacq_adjustments << " of " + << this->fineacquire_state.max_adjustments << " was not confirmed applied by acamd within " + << this->fineacquire_state.move_timeout_sec << " s"; + logwrite( function, w.str() ); + this->is_fineacquire_locked.store( false, std::memory_order_release ); + this->is_fineacquire_running.store( false, std::memory_order_release ); + this->fineacquire_state.reset(); + this->publish_status(); + return; + } w << "WARNING goal change not applied within " << this->fineacquire_state.move_timeout_sec << " s; proceeding"; logwrite( function, w.str() ); @@ -646,11 +690,55 @@ namespace Slicecam { if ( this->fineacquire_state.settle_sec > 0.0 ) { std::this_thread::sleep_for( std::chrono::duration( this->fineacquire_state.settle_sec ) ); } + // a stop that arrived during the settle ends this run here: nothing below + // (totals, the budget's decision, a published lock) may act for it + if ( !this->should_framegrab_run.load(std::memory_order_acquire) || + !this->is_fineacquire_running.load(std::memory_order_acquire) ) return; // accumulate the applied correction (arcsec). Summed over the run this is the // total ACAM->slit residual that acam-acquire left behind (the [ACQMODEL] line). this->fineacq_total_dra += cmd_dra * 3600.0; this->fineacq_total_ddec += cmd_ddec * 3600.0; + // The budget's last correction decides. Once FINE_ACQUIRE_MAX_ADJUSTMENTS + // corrections have been commanded, nothing further is sampled or evaluated. + // This last one was only sent because the offset it removes is within + // FINE_ACQUIRE_RELAXED_FACTOR x goal (checked before the send), and it has + // executed and settled, so the run converges now. 0 disables the budget + // (correct until the goal or the sequencer timeout). + // + if ( this->fineacquire_state.max_adjustments > 0 + && this->fineacq_adjustments >= this->fineacquire_state.max_adjustments ) { + std::ostringstream oss; + // the same converged line as every lock (log readers key on it); the + // offset is the one this last correction was sent for, and the goal the + // relaxed one it was accepted against + oss << "fine acquisition converged: offset dRA=" << med_dra * 3600.0 + << " dDEC=" << med_ddec * 3600.0 + << " arcsec (r=" << offset_arcsec + << " arcsec, n=" << n + << " scatter=(" << sig_dra << "," << sig_ddec << ") arcsec)" + << " goal=" << this->fineacquire_state.relaxed_factor * this->fineacquire_state.goal_arcsec + << " arcsec; accepted on the final correction (" << this->fineacq_adjustments + << " of " << this->fineacquire_state.max_adjustments << ") without a verification frame"; + logwrite( function, oss.str() ); + + std::ostringstream acqmodel; + acqmodel << "[ACQMODEL] acam2slit dRA=" << this->fineacq_total_dra + << " dDEC=" << this->fineacq_total_ddec << " arcsec" + << " GOALRA=" << this->fineacq_goal_ra + << " GOALDEC=" << this->fineacq_goal_dec + << " CASANGLE=" << this->telem.angle_scope + << " n=" << n + << " cam=" << which; + logwrite( function, acqmodel.str() ); + + this->is_fineacquire_locked.store( true, std::memory_order_release ); + this->is_fineacquire_running.store( false, std::memory_order_release ); + this->fineacquire_state.reset(); + this->publish_status(); + return; + } + // reset samples and discard settle_count frames for telescope settling this->fineacquire_state.reset(); this->fineacquire_state.settle_frames = this->fineacquire_state.settle_count; @@ -1492,6 +1580,40 @@ namespace Slicecam { applied++; } else + if ( config.param[entry] == "FINE_ACQUIRE_MAX_ADJUSTMENTS" ) { + try { this->fineacquire_state.max_adjustments = std::stoi( config.arg[entry] ); } + catch ( const std::exception &e ) { + message.str(""); message << "ERROR invalid FINE_ACQUIRE_MAX_ADJUSTMENTS " + << config.arg[entry] << ": " << e.what(); + logwrite( function, message.str() ); + return ERROR; + } + if ( this->fineacquire_state.max_adjustments < 0 ) { + logwrite( function, "ERROR FINE_ACQUIRE_MAX_ADJUSTMENTS must be >= 0 (0 disables the budget)" ); + return ERROR; + } + message.str(""); message << "SLICECAMD:config:" << config.param[entry] << "=" << config.arg[entry]; + logwrite( function, message.str() ); + applied++; + } + else + if ( config.param[entry] == "FINE_ACQUIRE_RELAXED_FACTOR" ) { + try { this->fineacquire_state.relaxed_factor = std::stod( config.arg[entry] ); } + catch ( const std::exception &e ) { + message.str(""); message << "ERROR invalid FINE_ACQUIRE_RELAXED_FACTOR " + << config.arg[entry] << ": " << e.what(); + logwrite( function, message.str() ); + return ERROR; + } + if ( !( this->fineacquire_state.relaxed_factor >= 1.0 ) ) { // also refuses NaN + logwrite( function, "ERROR FINE_ACQUIRE_RELAXED_FACTOR must be a number >= 1" ); + return ERROR; + } + message.str(""); message << "SLICECAMD:config:" << config.param[entry] << "=" << config.arg[entry]; + logwrite( function, message.str() ); + applied++; + } + else if ( config.param[entry] == "FINE_ACQUIRE_MOVE_TIMEOUT" ) { try { this->fineacquire_state.move_timeout_sec = std::stod( config.arg[entry] ); } catch ( const std::exception &e ) { @@ -1721,6 +1843,15 @@ namespace Slicecam { } } + // a correction budget decides on the moves it commands, so it needs the + // confirmation that each one executed (the acamd move wait) + // + if ( this->fineacquire_state.max_adjustments > 0 && + !( this->fineacquire_state.move_timeout_sec > 0.0 ) ) { + logwrite( function, "ERROR FINE_ACQUIRE_MAX_ADJUSTMENTS > 0 needs FINE_ACQUIRE_MOVE_TIMEOUT > 0" ); + return ERROR; + } + // min_snr, when set, must be at least 2 // NAN (unset) disables SNR override // diff --git a/slicecamd/slicecam_interface.h b/slicecamd/slicecam_interface.h index b3e018c6..3a5eb9eb 100644 --- a/slicecamd/slicecam_interface.h +++ b/slicecamd/slicecam_interface.h @@ -95,6 +95,8 @@ namespace Slicecam { int settle_frames = 0; ///< countdown of frames to discard while telescope settles int settle_count = 2; ///< configured: frames to discard after each move double settle_sec = 3.0; ///< time-based settle (sec) after each move; 0 disables (needed when autoexpose shortens frames so the frame-count settle is too brief in wall-clock) + int max_adjustments = 0; ///< correction budget: at most this many corrections per run; the last is sent only if the offset it removes is <= relaxed_factor x goal_arcsec, and the run converges once it has executed, else fails; 0 disables + double relaxed_factor = 2.0; ///< correction budget: multiple of goal_arcsec the last correction may remove double move_timeout_sec = 60.0; ///< max wait (sec) for acamd to report the goal change applied; 0 disables the wait int consecutive_centroid_failures = 0; ///< counts consecutive centroid failures // exposure compensation (shared by the reactive trim and, later, autoexpose) @@ -150,6 +152,7 @@ namespace Slicecam { // the ACAM->slit pointing offset vs geometry over time. Touched only from the // single framegrab thread, so plain doubles are sufficient. double fineacq_total_dra = 0.0; ///< sum of applied dRA corrections this run [arcsec] + int fineacq_adjustments = 0; ///< corrections commanded this run (correction budget counter; survives FineAcqState::reset()) double fineacq_total_ddec = 0.0; ///< sum of applied dDEC corrections this run [arcsec] double fineacq_goal_ra = NAN; ///< per-run snapshot of goal RA [deg], taken at fineacquire start, logged at lock double fineacq_goal_dec = NAN; ///< per-run snapshot of goal DEC [deg], taken at fineacquire start, logged at lock