Kstars

adaptivefocus.cpp
1/*
2 SPDX-FileCopyrightText: 2023 John Evans <john.e.evans.email@googlemail.com>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6
7#include "adaptivefocus.h"
8#include <kstars_debug.h>
9#include "kstars.h"
10#include "Options.h"
11
12namespace Ekos
13{
14
15AdaptiveFocus::AdaptiveFocus(Focus *_focus) : m_focus(_focus)
16{
17 if (m_focus == nullptr)
18 qCDebug(KSTARS_EKOS_FOCUS) << "AdaptiveFocus constructed with null focus ptr";
19}
20
21AdaptiveFocus::~AdaptiveFocus()
22{
23}
24
25// Start an Adaptive Focus run
26void AdaptiveFocus::runAdaptiveFocus(const int currentPosition, const QString &filter)
27{
28 if (!m_focus)
29 {
30 qCDebug(KSTARS_EKOS_FOCUS) << "runAdaptiveFocus called but focus ptr is null. Ignoring.";
31 adaptiveFocusAdmin(currentPosition, false, false);
32 return;
33 }
34
35 if (!m_focus->m_FilterManager)
36 {
37 qCDebug(KSTARS_EKOS_FOCUS) << "runAdaptiveFocus called but focus filterManager is null. Ignoring.";
38 adaptiveFocusAdmin(currentPosition, false, false);
39 return;
40 }
41
42 if (!m_focus->m_OpsFocusSettings->focusAdaptive->isChecked())
43 {
44 qCDebug(KSTARS_EKOS_FOCUS) << "runAdaptiveFocus called but focusAdaptive->isChecked is false. Ignoring.";
45 adaptiveFocusAdmin(currentPosition, false, false);
46 return;
47 }
48
49 if (inAdaptiveFocus())
50 {
51 qCDebug(KSTARS_EKOS_FOCUS) << "runAdaptiveFocus called whilst already inAdaptiveFocus. Ignoring.";
52 adaptiveFocusAdmin(currentPosition, false, false);
53 return;
54 }
55
56 if (m_focus->inAutoFocus || m_focus->inFocusLoop || m_focus->inAdjustFocus || m_focus->inBuildOffsets)
57 {
58 qCDebug(KSTARS_EKOS_FOCUS) << "adaptiveFocus called whilst other focus activity in progress. Ignoring.";
59 adaptiveFocusAdmin(currentPosition, false, false);
60 return;
61 }
62
63 setInAdaptiveFocus(true);
64 m_ThisAdaptiveFocusStartPos = currentPosition;
65
66 // Get the reference data to be used by Adaptive Focus
67 int refPosition;
68 QString adaptiveFilter = getAdaptiveFilter(filter);
69
70 // If the filter has been changed since the last Adaptive Focus iteration, reset
71 if (filter != m_LastAdaptiveFilter)
72 {
73 resetAdaptiveFocusCounters();
74 m_LastAdaptiveFilter = filter;
75 }
76 if (!m_focus->m_FilterManager->getFilterAbsoluteFocusDetails(adaptiveFilter, refPosition,
77 m_focus->m_LastSourceAutofocusTemperature,
78 m_focus->m_LastSourceAutofocusAlt))
79 {
80 qCDebug(KSTARS_EKOS_FOCUS) << "runAdaptiveFocus unable to get last Autofocus details. Ignoring.";
81 adaptiveFocusAdmin(currentPosition, false, false);
82 return;
83 }
84
85 // If we are using a lock filter then adjust the reference position by the offset
86 refPosition += getAdaptiveFilterOffset(filter, adaptiveFilter);
87
88 // Find out if there is anything to do for temperature, firstly do we have a valid temperature source
89 double currentTemp = INVALID_VALUE;
90 double tempTicksDelta = 0.0, tempTicksDeltaLast = 0.0;
91 if (m_focus->currentTemperatureSourceElement && m_focus->m_LastSourceAutofocusTemperature != INVALID_VALUE)
92 {
93 if (m_LastAdaptiveFocusTemperature == INVALID_VALUE)
94 // 1st Adaptive Focus run so no previous results
95 m_LastAdaptiveFocusTemperature = m_focus->m_LastSourceAutofocusTemperature;
96
97 currentTemp = m_focus->currentTemperatureSourceElement->value;
98
99 // Calculate the deltas since the last Autofocus and Last Adaptive Focus
100 const double tempDelta = currentTemp - m_focus->m_LastSourceAutofocusTemperature;
101 const double tempDeltaLast = m_LastAdaptiveFocusTemperature - m_focus->m_LastSourceAutofocusTemperature;
102
103 // Scale the temperature delta to number of ticks
104 const double ticksPerTemp = m_focus->m_FilterManager->getFilterTicksPerTemp(adaptiveFilter);
105 tempTicksDelta = ticksPerTemp * tempDelta;
106 tempTicksDeltaLast = ticksPerTemp * tempDeltaLast;
107 }
108
109 // Now check for altitude
110 double currentAlt = INVALID_VALUE;
111 double altTicksDelta = 0.0, altTicksDeltaLast = 0.0;
112 bool altDimension = false;
113 if (m_focus->m_LastSourceAutofocusAlt != INVALID_VALUE)
114 {
115 if (m_LastAdaptiveFocusAlt == INVALID_VALUE)
116 // 1st Adaptive Focus run so no previous results
117 m_LastAdaptiveFocusAlt = m_focus->m_LastSourceAutofocusAlt;
118
119 currentAlt = m_focus->mountAlt;
120
121 const double altDelta = currentAlt - m_focus->m_LastSourceAutofocusAlt;
122 const double altDeltaLast = m_LastAdaptiveFocusAlt - m_focus->m_LastSourceAutofocusAlt;
123
124 // Scale the altitude delta to number of ticks
125 const double ticksPerAlt = m_focus->m_FilterManager->getFilterTicksPerAlt(adaptiveFilter);
126 altDimension = (abs(ticksPerAlt) > 0.001);
127 altTicksDelta = ticksPerAlt * altDelta;
128 altTicksDeltaLast = ticksPerAlt * altDeltaLast;
129 }
130
131 // proposedPosition is where focuser should move; proposedPositionLast is where focuser should have moved in last Adaptive iterative
132 int proposedPosition = refPosition + static_cast<int>(round(tempTicksDelta + altTicksDelta));
133 int proposedPositionLast = refPosition + static_cast<int>(round(tempTicksDeltaLast + altTicksDeltaLast));
134 int proposedMove = proposedPosition - currentPosition;
135
136 // We have the total movement, now work out the split by Temp, Alt and Pos Error
137 m_ThisAdaptiveFocusTempTicks = tempTicksDelta - tempTicksDeltaLast;
138 m_ThisAdaptiveFocusAltTicks = altTicksDelta - altTicksDeltaLast;
139
140 // If everything is going to plan the currentPosition will equal proposedPositionLast. If the focuser hasn't moved exactly to the
141 // requested position, e.g. its 1 or 2 ticks away then we need to account for this Positioning Error. It will have been reported
142 // on the last Adaptive run but we now need to reverse that positioning error out in the accounting for this run
143 m_LastAdaptiveFocusPosErrorReversal = proposedPositionLast - currentPosition;
144
145 // There could be a rounding error, where, for example, a small change in temp/alt (e.g. 0.1 ticks) results in a 1 tick move
146 m_ThisAdaptiveFocusRoundingError = proposedMove - m_LastAdaptiveFocusPosErrorReversal -
147 static_cast<int>(round(m_ThisAdaptiveFocusTempTicks + m_ThisAdaptiveFocusAltTicks));
148
149 // Check movement is above user defined minimum
150 if (abs(proposedMove) < m_focus->m_OpsFocusSettings->focusAdaptiveMinMove->value())
151 {
152 m_focus->appendLogText(i18n("Adaptive Focus: No movement (below threshold)"));
153 adaptiveFocusAdmin(currentPosition, true, false);
154 }
155 else
156 {
157 // Now do some checks that the movement is permitted
158 if (abs(m_focus->initialFocuserAbsPosition - proposedPosition) > m_focus->m_OpsFocusMechanics->focusMaxTravel->value())
159 {
160 // We are about to move the focuser beyond focus max travel so don't
161 // Suspend adaptive focusing, user can always re-enable, if required
162 m_focus->m_OpsFocusSettings->focusAdaptive->setChecked(false);
163 m_focus->appendLogText(i18n("Adaptive Focus suspended. Total movement would exceed Max Travel limit"));
164 adaptiveFocusAdmin(currentPosition, false, false);
165 }
166 else if (abs(m_AdaptiveTotalMove + proposedMove) > m_focus->m_OpsFocusSettings->focusAdaptiveMaxMove->value())
167 {
168 // We are about to move the focuser beyond adaptive focus max move so don't
169 // Suspend adaptive focusing. User can always re-enable, if required
170 m_focus->m_OpsFocusSettings->focusAdaptive->setChecked(false);
171 m_focus->appendLogText(i18n("Adaptive Focus suspended. Total movement would exceed adaptive limit"));
172 adaptiveFocusAdmin(currentPosition, false, false);
173 }
174 else
175 {
176 // For most folks, most of the time there won't be alt data or positioning errors, so don't continually report 0
177 QString tempStr = QString("%1").arg(m_ThisAdaptiveFocusTempTicks, 0, 'f', 1);
178 QString altStr = QString("%1").arg(m_ThisAdaptiveFocusAltTicks, 0, 'f', 1);
179 QString text = i18n("Adaptive Focus: Moving from %1 to %2 (TempΔ %3", currentPosition, proposedPosition, tempStr);
180 if (altDimension)
181 text += i18n("; AltΔ %1", altStr);
182 text += (m_LastAdaptiveFocusPosErrorReversal == 0) ? ")" : i18n("; Pos Error %1)", m_LastAdaptiveFocusPosErrorReversal);
183 m_focus->appendLogText(text);
184
185 // Go ahead and try to move the focuser. Admin tasks will be completed when the focuser move completes
186 if (m_focus->changeFocus(proposedMove))
187 {
188 // All good so update variables used after focuser move completes (see adaptiveFocusAdmin())
189 m_ThisAdaptiveFocusTemperature = currentTemp;
190 m_ThisAdaptiveFocusAlt = currentAlt;
191 m_AdaptiveFocusPositionReq = proposedPosition;
192 m_AdaptiveTotalMove += proposedMove;
193 }
194 else
195 {
196 // Problem moving the focuser
197 m_focus->appendLogText(i18n("Adaptive Focus unable to move focuser"));
198 adaptiveFocusAdmin(currentPosition, false, false);
199 }
200 }
201 }
202}
203
204// When adaptiveFocus succeeds the focuser is moved and admin tasks are performed to inform other modules that adaptiveFocus is complete
205void AdaptiveFocus::adaptiveFocusAdmin(const int currentPosition, const bool success, const bool focuserMoved)
206{
207 // Reset member variable
208 setInAdaptiveFocus(false);
209
210 int thisPosError = 0;
211 if (focuserMoved)
212 {
213 // Signal Capture that we are done - honour the focuser settle time after movement.
214 QTimer::singleShot(m_focus->m_OpsFocusMechanics->focusSettleTime->value() * 1000, m_focus, [ &, success]()
215 {
216 emit m_focus->focusAdaptiveComplete(success, m_focus->opticalTrain());
217 });
218
219 // Check whether the focuser moved to the requested position or whether we have a positioning error (1 or 2 ticks for example)
220 // If there is a positioning error update the total ticks to include the error.
221 if (m_AdaptiveFocusPositionReq != INVALID_VALUE)
222 {
223 thisPosError = currentPosition - m_AdaptiveFocusPositionReq;
224 m_AdaptiveTotalMove += thisPosError;
225 }
226 }
227 else
228 emit m_focus->focusAdaptiveComplete(success, m_focus->opticalTrain());
229
230 // Signal Analyze if success both for focuser moves and zero moves
231 bool check = true;
232 if (success)
233 {
234 // Note we send Analyze the active filter, not the Adaptive Filter (i.e. not the lock filter)
235 int totalTicks = static_cast<int>(round(m_ThisAdaptiveFocusTempTicks + m_ThisAdaptiveFocusAltTicks)) +
236 m_LastAdaptiveFocusPosErrorReversal + m_ThisAdaptiveFocusRoundingError + thisPosError;
237
238 emit m_focus->adaptiveFocusComplete(m_focus->filter(), m_ThisAdaptiveFocusTemperature, m_ThisAdaptiveFocusTempTicks,
239 m_ThisAdaptiveFocusAlt, m_ThisAdaptiveFocusAltTicks, m_LastAdaptiveFocusPosErrorReversal,
240 thisPosError, totalTicks, currentPosition, focuserMoved);
241
242 // Check that totalTicks movement is above minimum
243 if (totalTicks < m_focus->m_OpsFocusSettings->focusAdaptiveMinMove->value())
244 totalTicks = 0;
245 // Perform an accounting check that the numbers add up
246 check = (m_ThisAdaptiveFocusStartPos + totalTicks == currentPosition);
247 }
248
249 if (success && focuserMoved)
250 {
251 // Reset member variables in prep for the next Adaptive Focus run
252 m_LastAdaptiveFocusTemperature = m_ThisAdaptiveFocusTemperature;
253 m_LastAdaptiveFocusAlt = m_ThisAdaptiveFocusAlt;
254 }
255
256 // Log a debug for each Adaptive Focus.
257 qCDebug(KSTARS_EKOS_FOCUS) << "Adaptive Focus Stats: Filter:" << m_LastAdaptiveFilter
258 << ", Adaptive Filter:" << getAdaptiveFilter(m_LastAdaptiveFilter)
259 << ", Min Move:" << m_focus->m_OpsFocusSettings->focusAdaptiveMinMove->value()
260 << ", success:" << (success ? "Yes" : "No")
261 << ", focuserMoved:" << (focuserMoved ? "Yes" : "No")
262 << ", Temp:" << m_ThisAdaptiveFocusTemperature
263 << ", Temp ticks:" << m_ThisAdaptiveFocusTempTicks
264 << ", Alt:" << m_ThisAdaptiveFocusAlt
265 << ", Alt ticks:" << m_ThisAdaptiveFocusAltTicks
266 << ", Last Pos Error reversal:" << m_LastAdaptiveFocusPosErrorReversal
267 << ", Rounding Error:" << m_ThisAdaptiveFocusRoundingError
268 << ", New Pos Error:" << thisPosError
269 << ", Starting Pos:" << m_ThisAdaptiveFocusStartPos
270 << ", Current Position:" << currentPosition
271 << ", Accounting Check:" << (check ? "Passed" : "Failed");
272}
273
274// Get the filter to use for Adaptive Focus
275QString AdaptiveFocus::getAdaptiveFilter(const QString filter)
276{
277 if (!m_focus->m_FilterManager)
278 return QString();
279
280 // If the active filter has a lock filter use that
281 QString adaptiveFilter = filter;
282 QString lockFilter = m_focus->m_FilterManager->getFilterLock(filter);
283 if (lockFilter != NULL_FILTER)
284 adaptiveFilter = lockFilter;
285
286 return adaptiveFilter;
287}
288
289// Calc the filter offset between the active filter and the adaptive filter (either active filter or lock filter)
290int AdaptiveFocus::getAdaptiveFilterOffset(const QString &activeFilter, const QString &adaptiveFilter)
291{
292 int offset = 0;
293 if (!m_focus->m_FilterManager)
294 return offset;
295
296 if (activeFilter != adaptiveFilter)
297 {
298 int activeOffset = m_focus->m_FilterManager->getFilterOffset(activeFilter);
299 int adaptiveOffset = m_focus->m_FilterManager->getFilterOffset(adaptiveFilter);
300 if (activeOffset != INVALID_VALUE && adaptiveOffset != INVALID_VALUE)
301 offset = activeOffset - adaptiveOffset;
302 else
303 qCDebug(KSTARS_EKOS_FOCUS) << "getAdaptiveFilterOffset unable to calculate filter offsets";
304 }
305 return offset;
306}
307
308// Reset the variables used by Adaptive Focus
309void AdaptiveFocus::resetAdaptiveFocusCounters()
310{
311 m_LastAdaptiveFilter = NULL_FILTER;
312 m_LastAdaptiveFocusTemperature = INVALID_VALUE;
313 m_LastAdaptiveFocusAlt = INVALID_VALUE;
314 m_AdaptiveTotalMove = 0;
315}
316
317// Function to set inAdaptiveFocus
318void AdaptiveFocus::setInAdaptiveFocus(bool value)
319{
320 m_inAdaptiveFocus = value;
321}
322
323// Change the start position of an autofocus run based Adaptive Focus settings
324// The start position uses the last successful AF run for the active filter and adapts that position
325// based on the temperature and altitude delta between now and when the last successful AF run happened
326int AdaptiveFocus::adaptStartPosition(int currentPosition, QString &AFfilter)
327{
328 // If the active filter has no lock then the AF run will happen on this filter so get the start point
329 // Otherwise get the lock filter on which the AF run will happen and get the start point of this filter
330 // An exception is when the BuildOffsets utility is being used as this ignores the lock filter
331 if (!m_focus->m_FilterManager)
332 return currentPosition;
333
334 QString filterText;
335 QString lockFilter = m_focus->m_FilterManager->getFilterLock(AFfilter);
336 if (m_focus->inBuildOffsets || lockFilter == NULL_FILTER || lockFilter == AFfilter)
337 filterText = AFfilter;
338 else
339 {
340 filterText = AFfilter + " locked to " + lockFilter;
341 AFfilter = lockFilter;
342 }
343
344 if (!m_focus->m_OpsFocusSettings->focusAdaptStart->isChecked())
345 // Adapt start option disabled
346 return currentPosition;
347
348 if (m_focus->m_FocusAlgorithm != Focus::FOCUS_LINEAR1PASS)
349 // Only enabled for LINEAR 1 PASS
350 return currentPosition;
351
352 // Start with the last AF run result for the active filter
353 int lastPos;
354 double lastTemp, lastAlt;
355 if(!m_focus->m_FilterManager->getFilterAbsoluteFocusDetails(AFfilter, lastPos, lastTemp, lastAlt))
356 // Unable to get the last AF run information for the filter so just use the currentPosition
357 return currentPosition;
358
359 // Only proceed if we have a sensible lastPos
360 if (lastPos <= 0)
361 return currentPosition;
362
363 // Do some checks on the lastPos
364 int minTravelLimit = qMax(0.0, currentPosition - m_focus->m_OpsFocusMechanics->focusMaxTravel->value());
365 int maxTravelLimit = qMin(m_focus->absMotionMax, currentPosition + m_focus->m_OpsFocusMechanics->focusMaxTravel->value());
366 if (lastPos < minTravelLimit || lastPos > maxTravelLimit)
367 {
368 // Looks like there is a potentially dodgy lastPos so just use currentPosition
369 m_focus->appendLogText(i18n("Adaptive start point, last AF solution outside Max Travel, ignoring"));
370 return currentPosition;
371 }
372
373 // Adjust temperature
374 double ticksTemp = 0.0;
375 double tempDelta = 0.0;
376 if (!m_focus->currentTemperatureSourceElement)
377 m_focus->appendLogText(i18n("Adaptive start point, no temperature source available"));
378 else if (lastTemp == INVALID_VALUE)
379 m_focus->appendLogText(i18n("Adaptive start point, no temperature for last AF solution"));
380 else
381 {
382 double currentTemp = m_focus->currentTemperatureSourceElement->value;
383 tempDelta = currentTemp - lastTemp;
384 if (abs(tempDelta) > 30)
385 // Sanity check on the temperature delta
386 m_focus->appendLogText(i18n("Adaptive start point, very large temperature delta, ignoring"));
387 else
388 ticksTemp = tempDelta * m_focus->m_FilterManager->getFilterTicksPerTemp(AFfilter);
389 }
390
391 // Adjust altitude
392 double ticksAlt = 0.0;
393 double currentAlt = m_focus->mountAlt;
394 double altDelta = currentAlt - lastAlt;
395
396 // Sanity check on the altitude delta
397 if (lastAlt == INVALID_VALUE)
398 m_focus->appendLogText(i18n("Adaptive start point, no alt recorded for last AF solution"));
399 else if (abs(altDelta) > 90.0)
400 m_focus->appendLogText(i18n("Adaptive start point, very large altitude delta, ignoring"));
401 else
402 ticksAlt = altDelta * m_focus->m_FilterManager->getFilterTicksPerAlt(AFfilter);
403
404 // We have all the elements to adjust the AF start position so final checks before the move
405 const int ticksTotal = static_cast<int> (round(ticksTemp + ticksAlt));
406 int targetPos = lastPos + ticksTotal;
407 if (targetPos < minTravelLimit || targetPos > maxTravelLimit)
408 {
409 // targetPos is outside Max Travel
410 m_focus->appendLogText(i18n("Adaptive start point, target position is outside Max Travel, ignoring"));
411 return currentPosition;
412 }
413
414 if (abs(targetPos - currentPosition) > m_focus->m_OpsFocusSettings->focusAdaptiveMaxMove->value())
415 {
416 // Disallow excessive movement.
417 // No need to check minimum movement
418 m_focus->appendLogText(i18n("Adaptive start point [%1] excessive move disallowed", filterText));
419 qCDebug(KSTARS_EKOS_FOCUS) << "Adaptive start point: " << filterText
420 << " startPosition: " << currentPosition
421 << " Last filter position: " << lastPos
422 << " Temp delta: " << tempDelta << " Temp ticks: " << ticksTemp
423 << " Alt delta: " << altDelta << " Alt ticks: " << ticksAlt
424 << " Target position: " << targetPos
425 << " Exceeds max allowed move: " << m_focus->m_OpsFocusSettings->focusAdaptiveMaxMove->value()
426 << " Using startPosition.";
427 return currentPosition;
428 }
429 else
430 {
431 // All good so report the move
432 m_focus->appendLogText(i18n("Adapting start point [%1] from %2 to %3", filterText, currentPosition, targetPos));
433 qCDebug(KSTARS_EKOS_FOCUS) << "Adaptive start point: " << filterText
434 << " startPosition: " << currentPosition
435 << " Last filter position: " << lastPos
436 << " Temp delta: " << tempDelta << " Temp ticks: " << ticksTemp
437 << " Alt delta: " << altDelta << " Alt ticks: " << ticksAlt
438 << " Target position: " << targetPos;
439 return targetPos;
440 }
441}
442
443}
QString i18n(const char *text, const TYPE &arg...)
Ekos is an advanced Astrophotography tool for Linux.
Definition align.cpp:83
QString arg(Args &&... args) const const
QFuture< void > filter(QThreadPool *pool, Sequence &sequence, KeepFunctor &&filterFunction)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 4 2024 16:38:42 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.