[education/kstars] kstars: AI Guide: apply a trained model's recorded settings on load instead of rejecting them
Jasem Mutlaq <[email protected]> Tue, 4 Aug 2026 16:00:48 +0000 (UTC)
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit 21b6aa2f7a2f03384641e08f0da407ba2c29a4d7 by Jasem Mutlaq. Committed on 04/08/2026 at 16:00. Pushed by mutlaqja into branch 'master'. AI Guide: apply a trained model's recorded settings on load instead of rejecting them Previously, loading a weights file whose fingerprint (guide exposure, binning, RA/DEC proportional+integral gain, min/max pulse, hysteresis) didn't match the current live settings was rejected outright, aborting guiding with an error telling the user to go match the settings by hand across several panels, or retrain. Adds a shared applyFingerprintToOptions() (new mount_guider.cpp) that instead writes every field present in the fingerprint directly to Options, returning a summary of what changed. WormGearGuider/HarmonicGuider/ DirectDriveGuider's loadWeights() call this instead of the old validateFingerprint()/fpDoubleClose() reject-on-mismatch logic, which is removed from all three (DirectDriveGuider's version only checked 4 of the 11 fields; unified to the same full set for all three, since AIGuideProtocol::buildFingerprint() always writes all of them regardless of mount type). gmath.cpp's two load call sites (session start, and the live reload-weights path) log what was applied via a new fingerprintApplied() accessor. fingerprintError() now only reports genuine failures (missing/corrupt file, wrong mount class) -- a fingerprint mismatch is no longer one of them. Exposure/binning changes take effect on the next guide session start, not retroactively into a capture already in flight, since that is a camera reconfiguration rather than a plain settings flag. Co-Authored-By: Claude Sonnet 5 <[email protected]> M +1 -0 kstars/CMakeLists.txt M +19 -61 kstars/ekos/guide/internalguide/direct_drive_guider.cpp M +0 -5 kstars/ekos/guide/internalguide/direct_drive_guider.h M +15 -6 kstars/ekos/guide/internalguide/gmath.cpp M +19 -64 kstars/ekos/guide/internalguide/harmonic_guider.cpp M +0 -1 kstars/ekos/guide/internalguide/harmonic_guider.h A +85 -0 kstars/ekos/guide/internalguide/mount_guider.cpp [License: GPL(v2.0+)] M +27 -2 kstars/ekos/guide/internalguide/mount_guider.h M +21 -66 kstars/ekos/guide/internalguide/worm_gear_guider.cpp M +0 -1 kstars/ekos/guide/internalguide/worm_gear_guider.h https://invent.kde.org/education/kstars/-/commit/21b6aa2f7a2f03384641e08f0da407ba2c29a4d7 diff --git a/kstars/CMakeLists.txt b/kstars/CMakeLists.txt index 5cd95925c7..7a446b1a42 100644 --- a/kstars/CMakeLists.txt +++ b/kstars/CMakeLists.txt @@ -403,6 +403,7 @@ if (INDI_FOUND) # Internal Guide ekos/guide/internalguide/assistant_stats.cpp ekos/guide/internalguide/gmath.cpp + ekos/guide/internalguide/mount_guider.cpp ekos/guide/internalguide/worm_gear_guider.cpp ekos/guide/internalguide/direct_drive_guider.cpp ekos/guide/internalguide/harmonic_guider.cpp diff --git a/kstars/ekos/guide/internalguide/direct_drive_guider.cpp b/kstars/ekos/guide/internalguide/direct_drive_guider.cpp index a64d4575a1..9ce9d392c6 100644 --- a/kstars/ekos/guide/internalguide/direct_drive_guider.cpp +++ b/kstars/ekos/guide/internalguide/direct_drive_guider.cpp @@ -16,87 +16,45 @@ #include <cmath> #include <algorithm> -namespace -{ -bool fpDoubleClose(double a, double b, double tol = 1e-4) -{ - return std::abs(a - b) <= tol; -} -} // namespace - -// --------------------------------------------------------------------------- -// validateFingerprint — reject weights recorded under different equipment -// --------------------------------------------------------------------------- -bool DirectDriveGuider::validateFingerprint(const QJsonObject &fp) -{ - m_FingerprintError.clear(); - if (fp.isEmpty()) - return true; - - QStringList mismatches; - - const struct - { - const char *key; - double current; - double tol; - } checks[] = - { - { "guide_exposure_s", Options::guideExposure(), 0.05 }, - { "ra_proportional_gain", Options::rAProportionalGain(), 1e-4 }, - { "dec_proportional_gain", Options::dECProportionalGain(), 1e-4 }, - }; - - bool ok = true; - for (const auto &c : checks) - { - if (fp.contains(c.key) && !fpDoubleClose(fp[c.key].toDouble(), c.current, c.tol)) - { - qCWarning(KSTARS_EKOS_GUIDE) << "AI weights rejected:" << c.key << "recorded" - << fp[c.key].toDouble() << "current" << c.current; - mismatches << QString("%1: weights %2, current %3") - .arg(c.key).arg(fp[c.key].toDouble()).arg(c.current); - ok = false; - } - } - - if (fp.contains("guide_binning") && fp["guide_binning"].toString() != Options::guideBinning()) - { - qCWarning(KSTARS_EKOS_GUIDE) << "AI weights rejected: guide_binning recorded" - << fp["guide_binning"].toString() << "current" << Options::guideBinning(); - mismatches << QString("guide_binning: weights %1, current %2") - .arg(fp["guide_binning"].toString(), Options::guideBinning()); - ok = false; - } - - m_FingerprintError = mismatches.join("\n"); - return ok; -} - // --------------------------------------------------------------------------- // loadWeights // --------------------------------------------------------------------------- bool DirectDriveGuider::loadWeights(const QString &weightsPath) { m_weightsLoaded = false; + m_FingerprintError.clear(); + m_FingerprintApplied.clear(); QFile file(weightsPath); if (!file.open(QIODevice::ReadOnly)) + { + m_FingerprintError = "weights file could not be opened"; return false; + } QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); if (doc.isNull() || !doc.isObject()) + { + m_FingerprintError = "weights file is not valid JSON"; return false; + } QJsonObject root = doc.object(); if (root["mount_type"].toString() != "DIRECT_DRIVE") + { + m_FingerprintError = "weights were trained for a different mount class"; return false; + } - // Reject weights recorded at a different binning / exposure / gain, so a model whose - // coefficients are in one rig's pixels/second is not silently applied to another. - if (!validateFingerprint(root["model_fingerprint"].toObject())) - return false; + // Reinstate the settings this model was trained under (exposure, binning, gains, …) + // rather than requiring the user to have already matched them by hand. + const QStringList changes = applyFingerprintToOptions(root["model_fingerprint"].toObject()); + if (!changes.isEmpty()) + { + m_FingerprintApplied = changes.join("\n"); + qCInfo(KSTARS_EKOS_GUIDE) << "AI weights: applied recorded settings -" << changes.join("; "); + } // Parameters block written by train_direct_drive.py QJsonObject params = root["parameters"].toObject(); diff --git a/kstars/ekos/guide/internalguide/direct_drive_guider.h b/kstars/ekos/guide/internalguide/direct_drive_guider.h index 6bf42f0306..b59f1a323c 100644 --- a/kstars/ekos/guide/internalguide/direct_drive_guider.h +++ b/kstars/ekos/guide/internalguide/direct_drive_guider.h @@ -66,9 +66,4 @@ class DirectDriveGuider : public MountSpecificGuider // --- Physics helpers --- double physicsRA (double alt_deg) const; double physicsDEC(double alt_deg, double q_deg) const; - - // Rejects weights whose recorded equipment fingerprint (exposure, binning, gains, …) - // does not match the current session, so a model trained at a different binning / - // pixel-scale is not silently applied. Mirrors WormGearGuider / HarmonicGuider. - bool validateFingerprint(const QJsonObject &fp); }; diff --git a/kstars/ekos/guide/internalguide/gmath.cpp b/kstars/ekos/guide/internalguide/gmath.cpp index 30869c119b..ae30e4882a 100644 --- a/kstars/ekos/guide/internalguide/gmath.cpp +++ b/kstars/ekos/guide/internalguide/gmath.cpp @@ -273,6 +273,12 @@ void cgmath::start() m_AIGuider = MountGuiderFactory::createFromWeights(weightsPath); if (m_AIGuider && m_AIGuider->loadWeights(weightsPath)) { + if (!m_AIGuider->fingerprintApplied().isEmpty()) + { + qCInfo(KSTARS_EKOS_GUIDE) << "[AI GUIDER] Applied recorded settings:" << m_AIGuider->fingerprintApplied(); + emit newLog(i18n("AI Guider: applied the trained model's recorded settings (%1).", + m_AIGuider->fingerprintApplied())); + } if (useAIAlgorithm) { qCWarning(KSTARS_EKOS_GUIDE) << "======================================================="; @@ -290,16 +296,16 @@ void cgmath::start() } else { - qCWarning(KSTARS_EKOS_GUIDE) << ">>> AI GUIDER FAILED TO LOAD WEIGHTS OR FINGERPRINT MISMATCH:" << weightsPath; + const QString reason = m_AIGuider ? m_AIGuider->fingerprintError() : QString(); + qCWarning(KSTARS_EKOS_GUIDE) << ">>> AI GUIDER FAILED TO LOAD WEIGHTS:" << weightsPath << reason; if (useAIAlgorithm) { - const QString reason = m_AIGuider ? m_AIGuider->fingerprintError() : QString(); const QString detail = reason.isEmpty() - ? i18n("The weights file could not be read or does not match this mount type.") - : i18n("Settings that do not match the weights:\n%1", reason); - emit newLog(i18n("AI Guider failed to load weights or fingerprint mismatched. Guiding aborted.")); + ? i18n("The weights file could not be read.") + : reason; + emit newLog(i18n("AI Guider failed to load weights. Guiding aborted.")); KSNotification::error( - i18n("AI Guider failed to load weights!\n\n%1\n\nChange the settings back to match, re-run the Guide AI Assistant with your current settings, or switch the Guide Algorithm to a standard mode. Guiding has been aborted.", + i18n("AI Guider failed to load weights!\n\n%1\n\nRe-run the Guide AI Assistant, load a different weights file, or switch the Guide Algorithm to a standard mode. Guiding has been aborted.", detail), i18n("AI Guider Error")); } @@ -368,6 +374,9 @@ bool cgmath::reloadAIWeights() } m_AIGuider = std::move(candidate); + if (!m_AIGuider->fingerprintApplied().isEmpty()) + emit newLog(i18n("AI Guider: applied the trained model's recorded settings (%1).", + m_AIGuider->fingerprintApplied())); // A plain (non-forced) reset is enough: HarmonicGuider::resetSession() already forces a // full reset itself when the new weights describe a different PE period than the currently // active static Kalman state, and otherwise only clears transient tracking state while diff --git a/kstars/ekos/guide/internalguide/harmonic_guider.cpp b/kstars/ekos/guide/internalguide/harmonic_guider.cpp index ff152ee62b..3e328699c3 100644 --- a/kstars/ekos/guide/internalguide/harmonic_guider.cpp +++ b/kstars/ekos/guide/internalguide/harmonic_guider.cpp @@ -19,14 +19,6 @@ #include <cmath> #include <algorithm> -namespace -{ -bool fpDoubleClose(double a, double b, double tol = 1e-4) -{ - return std::abs(a - b) <= tol; -} -} // namespace - // ── Static member initialization ────────────────────────────────────────────── Eigen::Matrix<double, HarmonicGuider::N_STATES, 1> HarmonicGuider::m_x { Eigen::Matrix<double, N_STATES, 1>::Zero() }; @@ -54,79 +46,42 @@ HarmonicGuider::HarmonicGuider() m_qb2.setZero(); } -bool HarmonicGuider::validateFingerprint(const QJsonObject &fp) -{ - m_FingerprintError.clear(); - if (fp.isEmpty()) - return true; - - QStringList mismatches; - - const struct - { - const char *key; - double current; - double tol; - } checks[] = - { - { "guide_exposure_s", Options::guideExposure(), 0.05 }, - { "ra_proportional_gain", Options::rAProportionalGain(), 1e-4 }, - { "dec_proportional_gain", Options::dECProportionalGain(), 1e-4 }, - { "ra_integral_gain", Options::rAIntegralGain(), 1e-4 }, - { "dec_integral_gain", Options::dECIntegralGain(), 1e-4 }, - { "ra_min_pulse_arcsec", Options::rAMinimumPulseArcSec(), 1e-4 }, - { "dec_min_pulse_arcsec", Options::dECMinimumPulseArcSec(), 1e-4 }, - { "ra_max_pulse_arcsec", static_cast<double>(Options::rAMaximumPulseArcSec()), 1e-4 }, - { "dec_max_pulse_arcsec", static_cast<double>(Options::dECMaximumPulseArcSec()), 1e-4 }, - { "ra_hysteresis", Options::rAHysteresis(), 1e-4 }, - { "dec_hysteresis", Options::dECHysteresis(), 1e-4 }, - }; - - bool ok = true; - for (const auto &c : checks) - { - if (fp.contains(c.key) && !fpDoubleClose(fp[c.key].toDouble(), c.current, c.tol)) - { - qCWarning(KSTARS_EKOS_GUIDE) << "AI weights rejected:" << c.key << "recorded" - << fp[c.key].toDouble() << "current" << c.current; - mismatches << QString("%1: weights %2, current %3") - .arg(c.key).arg(fp[c.key].toDouble()).arg(c.current); - ok = false; - } - } - - if (fp.contains("guide_binning") && fp["guide_binning"].toString() != Options::guideBinning()) - { - qCWarning(KSTARS_EKOS_GUIDE) << "AI weights rejected: guide_binning recorded" - << fp["guide_binning"].toString() << "current" << Options::guideBinning(); - mismatches << QString("guide_binning: weights %1, current %2") - .arg(fp["guide_binning"].toString(), Options::guideBinning()); - ok = false; - } - - m_FingerprintError = mismatches.join("\n"); - return ok; -} - bool HarmonicGuider::loadWeights(const QString &weightsPath) { m_weightsLoaded = false; + m_FingerprintError.clear(); + m_FingerprintApplied.clear(); QFile file(weightsPath); if (!file.open(QIODevice::ReadOnly)) + { + m_FingerprintError = "weights file could not be opened"; return false; + } QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); if (doc.isNull() || !doc.isObject()) + { + m_FingerprintError = "weights file is not valid JSON"; return false; + } QJsonObject root = doc.object(); if (root["mount_type"].toString() != "HARMONIC_DRIVE") + { + m_FingerprintError = "weights were trained for a different mount class"; return false; + } - if (!validateFingerprint(root["model_fingerprint"].toObject())) - return false; + // Reinstate the settings this model was trained under (exposure, binning, gains, …) + // rather than requiring the user to have already matched them by hand. + const QStringList changes = applyFingerprintToOptions(root["model_fingerprint"].toObject()); + if (!changes.isEmpty()) + { + m_FingerprintApplied = changes.join("\n"); + qCInfo(KSTARS_EKOS_GUIDE) << "AI weights: applied recorded settings -" << changes.join("; "); + } QJsonObject phys = root["physical"].toObject(); m_kappa_ra = phys["kappa_ra"].toDouble(0.2); diff --git a/kstars/ekos/guide/internalguide/harmonic_guider.h b/kstars/ekos/guide/internalguide/harmonic_guider.h index a766783caf..ddf04144c3 100644 --- a/kstars/ekos/guide/internalguide/harmonic_guider.h +++ b/kstars/ekos/guide/internalguide/harmonic_guider.h @@ -146,7 +146,6 @@ class HarmonicGuider : public MountSpecificGuider static constexpr int INNOV_WINDOW = 20; // ── Helpers ────────────────────────────────────────────────────────── - bool validateFingerprint(const QJsonObject &fp); void buildF(Eigen::Matrix<double, N_STATES, N_STATES> &F, double dt) const; Eigen::Matrix<double, N_STATES, N_STATES> computeQ(double snr, double snr_delta, double innov_ra, double innov_dec, double dt) const; diff --git a/kstars/ekos/guide/internalguide/mount_guider.cpp b/kstars/ekos/guide/internalguide/mount_guider.cpp new file mode 100644 index 0000000000..f85db6d74b --- /dev/null +++ b/kstars/ekos/guide/internalguide/mount_guider.cpp @@ -0,0 +1,85 @@ +/* + * mount_guider.cpp — shared helpers for MountSpecificGuider implementations + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "mount_guider.h" + +#include "Options.h" + +#include <QJsonObject> +#include <cmath> + +QStringList applyFingerprintToOptions(const QJsonObject &fp) +{ + QStringList changes; + if (fp.isEmpty()) + return changes; + + const struct + { + const char *key; + double current; + double tol; + void (*setter)(double); + } doubleFields[] = + { + { "guide_exposure_s", Options::guideExposure(), 0.05, &Options::setGuideExposure }, + { "ra_proportional_gain", Options::rAProportionalGain(), 1e-4, &Options::setRAProportionalGain }, + { "dec_proportional_gain", Options::dECProportionalGain(), 1e-4, &Options::setDECProportionalGain }, + { "ra_integral_gain", Options::rAIntegralGain(), 1e-4, &Options::setRAIntegralGain }, + { "dec_integral_gain", Options::dECIntegralGain(), 1e-4, &Options::setDECIntegralGain }, + { "ra_min_pulse_arcsec", Options::rAMinimumPulseArcSec(), 1e-4, &Options::setRAMinimumPulseArcSec }, + { "dec_min_pulse_arcsec", Options::dECMinimumPulseArcSec(), 1e-4, &Options::setDECMinimumPulseArcSec }, + { "ra_hysteresis", Options::rAHysteresis(), 1e-4, &Options::setRAHysteresis }, + { "dec_hysteresis", Options::dECHysteresis(), 1e-4, &Options::setDECHysteresis }, + }; + + for (const auto &f : doubleFields) + { + if (!fp.contains(f.key)) + continue; + const double recorded = fp[f.key].toDouble(); + if (std::abs(recorded - f.current) > f.tol) + { + changes << QString("%1: %2 -> %3").arg(f.key).arg(f.current, 0, 'g', 6).arg(recorded, 0, 'g', 6); + f.setter(recorded); + } + } + + if (fp.contains("guide_binning")) + { + const QString recorded = fp["guide_binning"].toString(); + if (!recorded.isEmpty() && recorded != Options::guideBinning()) + { + changes << QString("guide_binning: %1 -> %2").arg(Options::guideBinning(), recorded); + Options::setGuideBinning(recorded); + } + } + + const struct + { + const char *key; + uint current; + void (*setter)(uint); + } uintFields[] = + { + { "ra_max_pulse_arcsec", Options::rAMaximumPulseArcSec(), &Options::setRAMaximumPulseArcSec }, + { "dec_max_pulse_arcsec", Options::dECMaximumPulseArcSec(), &Options::setDECMaximumPulseArcSec }, + }; + + for (const auto &f : uintFields) + { + if (!fp.contains(f.key)) + continue; + const uint recorded = static_cast<uint>(std::lround(fp[f.key].toDouble())); + if (recorded != f.current) + { + changes << QString("%1: %2 -> %3").arg(f.key).arg(f.current).arg(recorded); + f.setter(recorded); + } + } + + return changes; +} diff --git a/kstars/ekos/guide/internalguide/mount_guider.h b/kstars/ekos/guide/internalguide/mount_guider.h index f51c7f6ccf..4850fbdbc8 100644 --- a/kstars/ekos/guide/internalguide/mount_guider.h +++ b/kstars/ekos/guide/internalguide/mount_guider.h @@ -13,8 +13,22 @@ */ #include <QString> +#include <QStringList> +#include <QJsonObject> #include <cstdint> +/** + * @brief Applies every field present in a training fingerprint (guide exposure, binning, + * RA/DEC proportional+integral gain, min/max pulse, hysteresis) directly to the live + * Options, so a trained model's recorded settings are reinstated automatically rather + * than requiring the user to go match them by hand across several settings panels. + * Exposure/binning changes take effect on the next guide session start, not retroactively + * mid-capture. + * @return A human-readable "key: old -> new" line per field that actually changed value + * (empty if the fingerprint was empty or every field already matched). + */ +QStringList applyFingerprintToOptions(const QJsonObject &fingerprint); + // --------------------------------------------------------------------------- // Input frame data — populated by InternalGuider::processGuiding() each frame // --------------------------------------------------------------------------- @@ -139,14 +153,25 @@ class MountSpecificGuider } /** - * @brief Human-readable list of fingerprint mismatches from the last loadWeights() - * failure, e.g. "guide_exposure_s: weights 0.5, current 1". Empty when none. + * @brief Human-readable description of why the last loadWeights() call failed + * (missing/corrupt file, or a mount_type mismatch). Empty on success; a + * fingerprint mismatch is no longer a failure reason, see fingerprintApplied(). */ QString fingerprintError() const { return m_FingerprintError; } + /** + * @brief Human-readable "key: old -> new" summary of the settings applyFingerprintToOptions() + * changed on the last successful loadWeights() call. Empty if nothing needed changing. + */ + QString fingerprintApplied() const + { + return m_FingerprintApplied; + } + protected: QString m_FingerprintError; + QString m_FingerprintApplied; }; diff --git a/kstars/ekos/guide/internalguide/worm_gear_guider.cpp b/kstars/ekos/guide/internalguide/worm_gear_guider.cpp index 83f39bd519..ee56afa9e8 100644 --- a/kstars/ekos/guide/internalguide/worm_gear_guider.cpp +++ b/kstars/ekos/guide/internalguide/worm_gear_guider.cpp @@ -17,14 +17,6 @@ #include <cmath> #include <algorithm> -namespace -{ -bool fpDoubleClose(double a, double b, double tol = 1e-4) -{ - return std::abs(a - b) <= tol; -} -} // namespace - // ── Static member initialization ────────────────────────────────────────────── Eigen::Vector4d WormGearGuider::m_rls_theta { Eigen::Vector4d::Zero() }; Eigen::Matrix4d WormGearGuider::m_rls_P { Eigen::Matrix4d::Identity() * 100.0 }; @@ -43,81 +35,44 @@ WormGearGuider::WormGearGuider() m_innovDec.clear(); } -bool WormGearGuider::validateFingerprint(const QJsonObject &fp) -{ - m_FingerprintError.clear(); - if (fp.isEmpty()) - return true; - - QStringList mismatches; - - const struct - { - const char *key; - double current; - double tol; - } checks[] = - { - { "guide_exposure_s", Options::guideExposure(), 0.05 }, - { "ra_proportional_gain", Options::rAProportionalGain(), 1e-4 }, - { "dec_proportional_gain", Options::dECProportionalGain(), 1e-4 }, - { "ra_integral_gain", Options::rAIntegralGain(), 1e-4 }, - { "dec_integral_gain", Options::dECIntegralGain(), 1e-4 }, - { "ra_min_pulse_arcsec", Options::rAMinimumPulseArcSec(), 1e-4 }, - { "dec_min_pulse_arcsec", Options::dECMinimumPulseArcSec(), 1e-4 }, - { "ra_max_pulse_arcsec", static_cast<double>(Options::rAMaximumPulseArcSec()), 1e-4 }, - { "dec_max_pulse_arcsec", static_cast<double>(Options::dECMaximumPulseArcSec()), 1e-4 }, - { "ra_hysteresis", Options::rAHysteresis(), 1e-4 }, - { "dec_hysteresis", Options::dECHysteresis(), 1e-4 }, - }; - - bool ok = true; - for (const auto &c : checks) - { - if (fp.contains(c.key) && !fpDoubleClose(fp[c.key].toDouble(), c.current, c.tol)) - { - qCWarning(KSTARS_EKOS_GUIDE) << "AI weights rejected:" << c.key << "recorded" - << fp[c.key].toDouble() << "current" << c.current; - mismatches << QString("%1: weights %2, current %3") - .arg(c.key).arg(fp[c.key].toDouble()).arg(c.current); - ok = false; - } - } - - if (fp.contains("guide_binning") && fp["guide_binning"].toString() != Options::guideBinning()) - { - qCWarning(KSTARS_EKOS_GUIDE) << "AI weights rejected: guide_binning recorded" - << fp["guide_binning"].toString() << "current" << Options::guideBinning(); - mismatches << QString("guide_binning: weights %1, current %2") - .arg(fp["guide_binning"].toString(), Options::guideBinning()); - ok = false; - } - - // Pulse algorithm is Standard (0) in training fingerprint; runtime uses AI — skip. - - m_FingerprintError = mismatches.join("\n"); - return ok; -} - bool WormGearGuider::loadWeights(const QString &weightsPath) { m_weightsLoaded = false; + m_FingerprintError.clear(); + m_FingerprintApplied.clear(); QFile file(weightsPath); if (!file.open(QIODevice::ReadOnly)) + { + m_FingerprintError = "weights file could not be opened"; return false; + } QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); if (doc.isNull() || !doc.isObject()) + { + m_FingerprintError = "weights file is not valid JSON"; return false; + } QJsonObject root = doc.object(); if (root["mount_type"].toString() != "WORM_GEAR") + { + m_FingerprintError = "weights were trained for a different mount class"; return false; + } - if (!validateFingerprint(root["model_fingerprint"].toObject())) - return false; + // Reinstate the settings this model was trained under (exposure, binning, gains, …), + // rather than requiring the user to have already matched them by hand. Pulse algorithm + // is Standard (0) in the training fingerprint but runtime uses AI, so it is not one of + // the fields applyFingerprintToOptions() touches. + const QStringList changes = applyFingerprintToOptions(root["model_fingerprint"].toObject()); + if (!changes.isEmpty()) + { + m_FingerprintApplied = changes.join("\n"); + qCInfo(KSTARS_EKOS_GUIDE) << "AI weights: applied recorded settings -" << changes.join("; "); + } QJsonObject phys = root["physics"].toObject(); m_pe_amplitude = phys["pe_amplitude"].toDouble(1.0); diff --git a/kstars/ekos/guide/internalguide/worm_gear_guider.h b/kstars/ekos/guide/internalguide/worm_gear_guider.h index 3df147cb97..bf6f4bf8c7 100644 --- a/kstars/ekos/guide/internalguide/worm_gear_guider.h +++ b/kstars/ekos/guide/internalguide/worm_gear_guider.h @@ -109,7 +109,6 @@ class WormGearGuider : public MountSpecificGuider static constexpr int INNOV_WINDOW = 20; // ── Helpers ─────────────────────────────────────────────────────────── - bool validateFingerprint(const QJsonObject &fp); double physicsRA(double t_sec, double altitude_deg) const; double physicsDEC(double altitude_deg, double parallactic_angle_deg) const; std::array<float, 2> runMLP(float altitude, float snr, float last_ra_pulse, float last_dec_pulse, float dt,