From ca85bc4aa32cc96734e8b5a875046ebc21996055 Mon Sep 17 00:00:00 2001 From: Christoffer Fremling Date: Tue, 16 Jun 2026 17:05:59 -0700 Subject: [PATCH 1/3] slicecamd: log structured [ACQMODEL] line at fine-acquire lock Emit one structured line at fine-acquire convergence with the total ACAM->slit residual (sum of the corrections applied during the run) paired with the pointing geometry: cass rotator angle, altitude (from airmass), azimuth, and telescope RA/DEC (TCS telemetry). Why: the ACAM astrometric acquire leaves the target a few arcsec off the slit, and fine-acquire removes that residual every time. Logging the residual vs geometry, run after run, lets us build/refine a geometric (flexure) model of the ACAM->slit offset over time so acam-acquire can pre-compensate it -- fewer fine-acquire iterations and fewer non-converging acquisitions. Today this signal is only recoverable by scraping per-correction "requested offsets" lines (which are unit-ambiguous and interleaved with manual put-on-slit). No behavior change: pure logging plus two per-run accumulators reset at run start. HA is derived offline from TELRA + the line timestamp, so no site ephemeris is added to the daemon. Co-Authored-By: Claude Opus 4.8 (1M context) --- slicecamd/slicecam_interface.cpp | 31 +++++++++++++++++++++++++++++++ slicecamd/slicecam_interface.h | 7 +++++++ 2 files changed, 38 insertions(+) diff --git a/slicecamd/slicecam_interface.cpp b/slicecamd/slicecam_interface.cpp index 01604a4b..ceb4c2a4 100644 --- a/slicecamd/slicecam_interface.cpp +++ b/slicecamd/slicecam_interface.cpp @@ -163,6 +163,8 @@ namespace Slicecam { // start the state machine this->fineacquire_state.reset(); + this->fineacq_total_dra = 0.0; // reset per-run ACAM->slit residual accumulators + this->fineacq_total_ddec = 0.0; this->is_fineacquire_locked.store(false, std::memory_order_release); this->is_fineacquire_running.store(true, std::memory_order_release); this->is_autoexpose_running.store(false, std::memory_order_release); // fineacquire supersedes auto-exposure @@ -461,6 +463,30 @@ namespace Slicecam { << " scatter=(" << sig_dra << "," << sig_ddec << ") arcsec)" << " goal=" << this->fineacquire_state.goal_arcsec << " arcsec"; logwrite( function, oss.str() ); + + // One structured per-run line for building an ACAM->slit geometric (flexure) + // model over time. fineacq_total_{dra,ddec} is the total correction applied this + // run = the ACAM->slit residual that acam-acquire left behind. We pair it with the + // pointing geometry (cass rotator angle, altitude, azimuth) and the telescope + // RA/DEC at lock (TCS telemetry, i.e. the database-fed on-target/goal position). + // ALT is derived from AIRMASS (site-independent); HA is derived offline from + // TELRA + this line's timestamp, so no site ephemeris lives in this daemon. + const double am = this->telem.airmass; + const double alt = ( std::isfinite(am) && am >= 1.0 ) + ? 90.0 - std::acos( 1.0 / am ) * 180.0 / PI : NAN; + std::ostringstream acqmodel; + acqmodel << "[ACQMODEL] acam2slit dRA=" << this->fineacq_total_dra + << " dDEC=" << this->fineacq_total_ddec << " arcsec" + << " CASANGLE="<< this->telem.angle_scope + << " ALT=" << alt + << " AZ=" << this->telem.az + << " AIRMASS=" << am + << " TELRA=" << this->telem.ra_scope_h + << " TELDEC=" << this->telem.dec_scope_d + << " 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(); @@ -542,6 +568,11 @@ namespace Slicecam { 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; + // reset samples and discard settle_count frames for telescope settling this->fineacquire_state.reset(); this->fineacquire_state.settle_frames = this->fineacquire_state.settle_count; diff --git a/slicecamd/slicecam_interface.h b/slicecamd/slicecam_interface.h index f8d3920f..0308cb1d 100644 --- a/slicecamd/slicecam_interface.h +++ b/slicecamd/slicecam_interface.h @@ -143,6 +143,13 @@ namespace Slicecam { FineAcqState fineacquire_state; + // Per-run accumulators for the ACAM->slit residual, summed over a fine-acquire + // run and logged once at lock (the [ACQMODEL] line) to build a flexure model of + // 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] + double fineacq_total_ddec = 0.0; ///< sum of applied dDEC corrections this run [arcsec] + /// per-frame auto-exposure runtime (ACAM-window pre-tuning). Brightness is /// sampled over a window of frames; a high percentile (near-max) is used /// because telescope motion only smears light out (lowering brightness), From 1346d71986847a251da2afa7d731a5cac9b3011a Mon Sep 17 00:00:00 2001 From: Christoffer Fremling Date: Tue, 16 Jun 2026 18:31:35 -0700 Subject: [PATCH 2/3] acqmodel log: record GOAL (database target) coords, not telescope RA/DEC The [ACQMODEL] line previously logged the telescope's actual RA/DEC (TELRA/ TELDEC from TCS telemetry). That is the OUTPUT of acam-acquire and drifts as fine-acquire applies offsets, so it cannot be used to fit the SCOPE->ACAM geometry. Log the target's GOAL coordinates instead -- the INPUT to the transform -- sourced from the database by the sequencer. - sequencerd: do_slicecam_fineacquire passes the DB target coords on the fineacquire start command ("... start goal "). - slicecamd: fineacquire parses/strips the optional "goal ", stores it, and the [ACQMODEL] line now reports GOALRA/GOALDEC + CASANGLE (actual rotator angle) + total ACAM->slit residual. Dropped TELRA/TELDEC and the ALT/AZ/AIRMASS fields -- altitude/hour-angle are derived offline from GOALRA/GOALDEC + the line timestamp. Manual "fineacquire start" without goal logs GOALRA/GOALDEC as nan. Still pure logging plus per-run accumulators; no control-path change. Co-Authored-By: Claude Opus 4.8 (1M context) --- sequencerd/sequence_acquisition.cpp | 10 +++++- slicecamd/slicecam_interface.cpp | 49 +++++++++++++++++------------ slicecamd/slicecam_interface.h | 2 ++ 3 files changed, 40 insertions(+), 21 deletions(-) diff --git a/sequencerd/sequence_acquisition.cpp b/sequencerd/sequence_acquisition.cpp index 057d2063..5fc7cdd7 100644 --- a/sequencerd/sequence_acquisition.cpp +++ b/sequencerd/sequence_acquisition.cpp @@ -158,8 +158,16 @@ namespace Sequencer { ScopedState wait_state(wait_state_manager, Sequencer::SEQ_WAIT_FINEACQUIRE); + // Pass the database target (goal) coordinates so slicecamd logs them with the per-run + // ACAM->slit residual for the geometry model. The goal is the INPUT to the SCOPE->ACAM + // transform; the telescope's actual RA/DEC is the output and drifts as fine-acquire + // applies offsets, so it cannot be used to fit the geometry. + const std::string startcmd = SLICECAMD_FINEACQUIRE + " start goal " + + std::to_string( radec_to_decimal( this->target.ra_hms ) * TO_DEGREES ) + " " + + std::to_string( radec_to_decimal( this->target.dec_dms ) ); + std::string reply; - if (this->slicecamd.command( SLICECAMD_FINEACQUIRE+" start", reply ) != NO_ERROR + if (this->slicecamd.command( startcmd, reply ) != NO_ERROR || reply.find("DONE") == std::string::npos ) { logwrite( function, "ERROR starting slicecam fine acquisition: no confirmation (reply=\""+reply+"\")" ); return ERROR; diff --git a/slicecamd/slicecam_interface.cpp b/slicecamd/slicecam_interface.cpp index ceb4c2a4..11313ab3 100644 --- a/slicecamd/slicecam_interface.cpp +++ b/slicecamd/slicecam_interface.cpp @@ -33,7 +33,7 @@ namespace Slicecam { // Help if ( args == "?" || args == "help" ) { retstring = SLICECAMD_FINEACQUIRE; - retstring.append( " stop | start [ { L | R } ] | [ status ]\n" ); + retstring.append( " stop | start [ goal ] [ { L | R } ] | [ status ]\n" ); retstring.append( " start or stop fine target acquisition.\n" ); retstring.append( " aimpoint is optional and uses configuration by default, but\n" ); retstring.append( " if specified must contain both L or R to specify which camera,\n" ); @@ -116,6 +116,20 @@ namespace Slicecam { } } + // optional "goal ": the database target (goal) coordinates, i.e. the + // INPUT to the SCOPE->ACAM transform. Stripped here, stored, and logged at lock for the + // ACAM->slit geometry model. Absent (e.g. manual runs) -> logged as nan. + this->fineacq_goal_ra = NAN; this->fineacq_goal_dec = NAN; + for ( size_t i=0; i+2 < tokens.size(); ++i ) { + if ( tokens[i] == "goal" ) { + try { this->fineacq_goal_ra = std::stod( tokens.at(i+1) ); + this->fineacq_goal_dec = std::stod( tokens.at(i+2) ); } + catch ( const std::exception & ) { this->fineacq_goal_ra = this->fineacq_goal_dec = NAN; } + tokens.erase( tokens.begin()+i, tokens.begin()+i+3 ); + break; + } + } + // are optional but if specified then require all three if ( tokens.size() != 1 && tokens.size() != 4 ) { logwrite(function, "ERROR expected stop | start [ { L | R } ]"); @@ -464,27 +478,22 @@ namespace Slicecam { << " goal=" << this->fineacquire_state.goal_arcsec << " arcsec"; logwrite( function, oss.str() ); - // One structured per-run line for building an ACAM->slit geometric (flexure) - // model over time. fineacq_total_{dra,ddec} is the total correction applied this - // run = the ACAM->slit residual that acam-acquire left behind. We pair it with the - // pointing geometry (cass rotator angle, altitude, azimuth) and the telescope - // RA/DEC at lock (TCS telemetry, i.e. the database-fed on-target/goal position). - // ALT is derived from AIRMASS (site-independent); HA is derived offline from - // TELRA + this line's timestamp, so no site ephemeris lives in this daemon. - const double am = this->telem.airmass; - const double alt = ( std::isfinite(am) && am >= 1.0 ) - ? 90.0 - std::acos( 1.0 / am ) * 180.0 / PI : NAN; + // One structured per-run line for building an ACAM->slit geometric (flexure) model + // over time. fineacq_total_{dra,ddec} is the total correction applied this run = the + // ACAM->slit residual that acam-acquire left behind. We log it against the GOAL + // (database target) coordinates -- the INPUT to the SCOPE->ACAM transform -- plus the + // cassegrain angle. Altitude/hour-angle are derived offline from GOALRA/GOALDEC + this + // line's timestamp. We deliberately do NOT log the telescope's actual RA/DEC: that is + // the transform's OUTPUT and drifts as fine-acquire applies offsets, so it cannot be + // used to fit the geometry. std::ostringstream acqmodel; acqmodel << "[ACQMODEL] acam2slit dRA=" << this->fineacq_total_dra - << " dDEC=" << this->fineacq_total_ddec << " arcsec" - << " CASANGLE="<< this->telem.angle_scope - << " ALT=" << alt - << " AZ=" << this->telem.az - << " AIRMASS=" << am - << " TELRA=" << this->telem.ra_scope_h - << " TELDEC=" << this->telem.dec_scope_d - << " n=" << n - << " cam=" << which; + << " 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 ); diff --git a/slicecamd/slicecam_interface.h b/slicecamd/slicecam_interface.h index 0308cb1d..c5e33bcd 100644 --- a/slicecamd/slicecam_interface.h +++ b/slicecamd/slicecam_interface.h @@ -149,6 +149,8 @@ namespace Slicecam { // single framegrab thread, so plain doubles are sufficient. double fineacq_total_dra = 0.0; ///< sum of applied dRA corrections this run [arcsec] double fineacq_total_ddec = 0.0; ///< sum of applied dDEC corrections this run [arcsec] + double fineacq_goal_ra = NAN; ///< database target (goal) RA [deg], passed at start, logged at lock + double fineacq_goal_dec = NAN; ///< database target (goal) DEC [deg], passed at start, logged at lock /// per-frame auto-exposure runtime (ACAM-window pre-tuning). Brightness is /// sampled over a window of frames; a high percentile (near-max) is used From 84bd0d395f5e85bf3743ee3c3d0763924fbfe46e Mon Sep 17 00:00:00 2001 From: David Hale Date: Tue, 23 Jun 2026 11:45:12 -0700 Subject: [PATCH 3/3] source [ACQMODEL] goal coords from Topic::TARGETINFO instead of the fineacquire command --- sequencerd/sequence_acquisition.cpp | 10 +----- slicecamd/slicecam_interface.cpp | 49 ++++++++++++++++++++--------- slicecamd/slicecam_interface.h | 15 +++++++-- slicecamd/slicecamd.cpp | 3 +- 4 files changed, 49 insertions(+), 28 deletions(-) diff --git a/sequencerd/sequence_acquisition.cpp b/sequencerd/sequence_acquisition.cpp index 5fc7cdd7..057d2063 100644 --- a/sequencerd/sequence_acquisition.cpp +++ b/sequencerd/sequence_acquisition.cpp @@ -158,16 +158,8 @@ namespace Sequencer { ScopedState wait_state(wait_state_manager, Sequencer::SEQ_WAIT_FINEACQUIRE); - // Pass the database target (goal) coordinates so slicecamd logs them with the per-run - // ACAM->slit residual for the geometry model. The goal is the INPUT to the SCOPE->ACAM - // transform; the telescope's actual RA/DEC is the output and drifts as fine-acquire - // applies offsets, so it cannot be used to fit the geometry. - const std::string startcmd = SLICECAMD_FINEACQUIRE + " start goal " - + std::to_string( radec_to_decimal( this->target.ra_hms ) * TO_DEGREES ) + " " - + std::to_string( radec_to_decimal( this->target.dec_dms ) ); - std::string reply; - if (this->slicecamd.command( startcmd, reply ) != NO_ERROR + if (this->slicecamd.command( SLICECAMD_FINEACQUIRE+" start", reply ) != NO_ERROR || reply.find("DONE") == std::string::npos ) { logwrite( function, "ERROR starting slicecam fine acquisition: no confirmation (reply=\""+reply+"\")" ); return ERROR; diff --git a/slicecamd/slicecam_interface.cpp b/slicecamd/slicecam_interface.cpp index 11313ab3..0c8b19e8 100644 --- a/slicecamd/slicecam_interface.cpp +++ b/slicecamd/slicecam_interface.cpp @@ -33,7 +33,7 @@ namespace Slicecam { // Help if ( args == "?" || args == "help" ) { retstring = SLICECAMD_FINEACQUIRE; - retstring.append( " stop | start [ goal ] [ { L | R } ] | [ status ]\n" ); + retstring.append( " stop | start [ { L | R } ] | [ status ]\n" ); retstring.append( " start or stop fine target acquisition.\n" ); retstring.append( " aimpoint is optional and uses configuration by default, but\n" ); retstring.append( " if specified must contain both L or R to specify which camera,\n" ); @@ -116,20 +116,6 @@ namespace Slicecam { } } - // optional "goal ": the database target (goal) coordinates, i.e. the - // INPUT to the SCOPE->ACAM transform. Stripped here, stored, and logged at lock for the - // ACAM->slit geometry model. Absent (e.g. manual runs) -> logged as nan. - this->fineacq_goal_ra = NAN; this->fineacq_goal_dec = NAN; - for ( size_t i=0; i+2 < tokens.size(); ++i ) { - if ( tokens[i] == "goal" ) { - try { this->fineacq_goal_ra = std::stod( tokens.at(i+1) ); - this->fineacq_goal_dec = std::stod( tokens.at(i+2) ); } - catch ( const std::exception & ) { this->fineacq_goal_ra = this->fineacq_goal_dec = NAN; } - tokens.erase( tokens.begin()+i, tokens.begin()+i+3 ); - break; - } - } - // are optional but if specified then require all three if ( tokens.size() != 1 && tokens.size() != 4 ) { logwrite(function, "ERROR expected stop | start [ { L | R } ]"); @@ -179,6 +165,12 @@ 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; + + // 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. + this->fineacq_goal_ra = this->targetinfo_ra_deg.load(); + this->fineacq_goal_dec = this->targetinfo_dec_deg.load(); + this->is_fineacquire_locked.store(false, std::memory_order_release); this->is_fineacquire_running.store(true, std::memory_order_release); this->is_autoexpose_running.store(false, std::memory_order_release); // fineacquire supersedes auto-exposure @@ -1000,6 +992,32 @@ namespace Slicecam { /***** Slicecam::Interface::handletopic_tcsd ********************************/ + /***** Slicecam::Interface::handletopic_targetinfo **************************/ + /** + * @brief what to do when the topic is Topic::TARGETINFO + * @details This receives target RA/DEC and stores them in the class + * as decimal degrees. + * @param[in] jmessage_in subscribed-received JSON message + * + */ + void Interface::handletopic_targetinfo( const nlohmann::json &jmessage ) { + std::string ra_hms, dec_dms; + + Common::extract_telemetry_value( jmessage, Key::TargetInfo::RA, ra_hms ); + Common::extract_telemetry_value( jmessage, Key::TargetInfo::DECL, dec_dms ); + + try { + this->targetinfo_ra_deg.store( radec_to_decimal( ra_hms ) * TO_DEGREES ); + this->targetinfo_dec_deg.store( radec_to_decimal( dec_dms ) ); + } + catch( const std::exception &e ) { + this->targetinfo_ra_deg.store(NAN); + this->targetinfo_dec_deg.store(NAN); + } + } + /***** Slicecam::Interface::handletopic_targetinfo **************************/ + + /***** Slicecam::Interface::publish_status **********************************/ /** * @brief publishes my important status on change @@ -2227,6 +2245,7 @@ namespace Slicecam { this->is_fineacquire_running.store( false, std::memory_order_release ); this->is_fineacquire_locked.store( false, std::memory_order_release ); + this->publish_status(); this->cv.notify_all(); // send notification that the loop has stopped diff --git a/slicecamd/slicecam_interface.h b/slicecamd/slicecam_interface.h index c5e33bcd..0cf27085 100644 --- a/slicecamd/slicecam_interface.h +++ b/slicecamd/slicecam_interface.h @@ -149,8 +149,8 @@ namespace Slicecam { // single framegrab thread, so plain doubles are sufficient. double fineacq_total_dra = 0.0; ///< sum of applied dRA corrections this run [arcsec] double fineacq_total_ddec = 0.0; ///< sum of applied dDEC corrections this run [arcsec] - double fineacq_goal_ra = NAN; ///< database target (goal) RA [deg], passed at start, logged at lock - double fineacq_goal_dec = NAN; ///< database target (goal) DEC [deg], passed at start, logged at lock + 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 /// per-frame auto-exposure runtime (ACAM-window pre-tuning). Brightness is /// sampled over a window of frames; a high percentile (near-max) is used @@ -193,6 +193,12 @@ namespace Slicecam { std::atomic last_acam_pubtime{0}; ///< pubtime (us) of latest received acamd status + // Latest target (goal) coords published on Topic::TARGETINFO + // NAN until a TARGETINFO arrives, so manual runs with no sequencer target log nan. + // + std::atomic targetinfo_ra_deg{NAN}; ///< latest goal RA [deg] from TARGETINFO + std::atomic targetinfo_dec_deg{NAN}; ///< latest goal DEC [deg] from TARGETINFO + /// Max acceptable age (us) for cached ACAM status used by fineacquire. static constexpr int64_t ACAM_STATUS_MAX_AGE_US = 10'000'000; @@ -267,7 +273,9 @@ namespace Slicecam { { Topic::TCSD, std::function( [this](const nlohmann::json &msg) { handletopic_tcsd(msg); } ) }, { Topic::SLITD, std::function( - [this](const nlohmann::json &msg) { handletopic_slitd(msg); } ) } + [this](const nlohmann::json &msg) { handletopic_slitd(msg); } ) }, + { Topic::TARGETINFO, std::function( + [this](const nlohmann::json &msg) { handletopic_targetinfo(msg); } ) } }; } @@ -307,6 +315,7 @@ namespace Slicecam { void handletopic_acamd( const nlohmann::json &jmessage ); void handletopic_slitd( const nlohmann::json &jmessage ); void handletopic_tcsd( const nlohmann::json &jmessage ); + void handletopic_targetinfo( const nlohmann::json &jmessage ); void publish_status(bool force=false); void publish_snapshot(); void publish_temperature(); ///< publish only the andor temperatures on Topic::SLICECAMD (periodic) diff --git a/slicecamd/slicecamd.cpp b/slicecamd/slicecamd.cpp index e4aa0006..cd507431 100644 --- a/slicecamd/slicecamd.cpp +++ b/slicecamd/slicecamd.cpp @@ -148,7 +148,8 @@ int main(int argc, char **argv) { // if ( slicecamd.interface.init_pubsub( { Topic::SLITD, Topic::ACAMD, - Topic::TCSD }) == ERROR ) { + Topic::TCSD, + Topic::TARGETINFO }) == ERROR ) { logwrite(function, "ERROR initializing publisher-subscriber handler"); slicecamd.exit_cleanly(); }