Kstars

calibration.cpp
1/*
2 SPDX-FileCopyrightText: 2020 Hy Murveit <hy@murveit.com>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6
7#include "calibration.h"
8#include "Options.h"
9#include "ekos_guide_debug.h"
10#include <QDateTime>
11#include "indi/indimount.h"
12
13#define CAL_VERSION "Cal v1.1"
14#define CAL_VERSION_1_0 "Cal v1.0"
15
16Calibration::Calibration()
17{
18 ROT_Z = GuiderUtils::Matrix(0);
19}
20
21// Set angle
22void Calibration::setAngle(double rotationAngle)
23{
24 angle = rotationAngle;
25 // Matrix is set a priori to NEGATIVE angle, because we want a CCW rotation in the
26 // left hand system of the sensor coordinate system.
27 // Rotation is used in guidestars::computeStarDrift() and guidestars::getDrift()
28 ROT_Z = GuiderUtils::RotateZ(-M_PI * angle / 180.0);
29}
30
31void Calibration::setParameters(double ccd_pix_width, double ccd_pix_height,
32 double focalLengthMm,
33 int binX, int binY,
34 ISD::Mount::PierSide currentPierSide,
35 const dms &mountRa, const dms &mountDec)
36{
37 focalMm = focalLengthMm;
38 ccd_pixel_width = ccd_pix_width;
39 ccd_pixel_height = ccd_pix_height;
40 calibrationRA = mountRa;
41 calibrationDEC = mountDec;
42 subBinX = binX;
43 subBinY = binY;
44 subBinXused = subBinX;
45 subBinYused = subBinY;
46 calibrationPierSide = currentPierSide;
47}
48
49void Calibration::setBinningUsed(int x, int y)
50{
51 subBinXused = x;
52 subBinYused = y;
53}
54
55void Calibration::setRaPulseMsPerArcsecond(double rate)
56{
57 raPulseMsPerArcsecond = rate;
58}
59
60void Calibration::setDecPulseMsPerArcsecond(double rate)
61{
62 decPulseMsPerArcsecond = rate;
63}
64
65GuiderUtils::Vector Calibration::convertToArcseconds(const GuiderUtils::Vector &input) const
66{
67 GuiderUtils::Vector arcseconds;
68 arcseconds.x = input.x * xArcsecondsPerPixel();
69 arcseconds.y = input.y * yArcsecondsPerPixel();
70 return arcseconds;
71}
72
73GuiderUtils::Vector Calibration::convertToPixels(const GuiderUtils::Vector &input) const
74{
75 GuiderUtils::Vector arcseconds;
76 arcseconds.x = input.x / xArcsecondsPerPixel();
77 arcseconds.y = input.y / yArcsecondsPerPixel();
78 return arcseconds;
79}
80
81void Calibration::convertToPixels(double xArcseconds, double yArcseconds,
82 double *xPixel, double *yPixel) const
83{
84 *xPixel = xArcseconds / xArcsecondsPerPixel();
85 *yPixel = yArcseconds / yArcsecondsPerPixel();
86}
87
88GuiderUtils::Vector Calibration::rotateToRaDec(const GuiderUtils::Vector &input) const
89{
90 GuiderUtils::Vector in;
91 in.x = input.x;
92 in.y = input.y;
93 return (in * ROT_Z);
94}
95
96void Calibration::rotateToRaDec(double dx, double dy,
97 double *ra, double *dec) const
98{
99 GuiderUtils::Vector input;
100 input.x = dx;
101 input.y = dy;
102 GuiderUtils::Vector out = rotateToRaDec(input);
103 *ra = out.x;
104 *dec = out.y;
105}
106
107double Calibration::binFactor() const
108{
109 return static_cast<double>(subBinXused) / static_cast<double>(subBinX);
110}
111
112double Calibration::inverseBinFactor() const
113{
114 return 1.0 / binFactor();
115}
116
117double Calibration::xArcsecondsPerPixel() const
118{
119 // arcs = 3600*180/pi * (pix*ccd_pix_sz) / focal_len
120 return binFactor() * (206264.806 * ccd_pixel_width * subBinX) / focalMm;
121}
122
123double Calibration::yArcsecondsPerPixel() const
124{
125 return binFactor() * (206264.806 * ccd_pixel_height * subBinY) / focalMm;
126}
127
128double Calibration::xPixelsPerArcsecond() const
129{
130 return inverseBinFactor() * (focalMm / (206264.806 * ccd_pixel_width * subBinX));
131}
132
133double Calibration::yPixelsPerArcsecond() const
134{
135 return inverseBinFactor() * (focalMm / (206264.806 * ccd_pixel_height * subBinY));
136}
137
138double Calibration::raPulseMillisecondsPerArcsecond() const
139{
140 return raPulseMsPerArcsecond;
141}
142
143double Calibration::decPulseMillisecondsPerArcsecond() const
144{
145 return decPulseMsPerArcsecond;
146}
147
148double Calibration::calculateRotation(double x, double y)
149{
150 double phi;
151
152 //if( (!GuiderUtils::Vector(delta_x, delta_y, 0)) < 2.5 )
153 // JM 2015-12-10: Lower threshold to 1 pixel
154 if ((!GuiderUtils::Vector(x, y, 0)) < 1)
155 return -1;
156
157 // 90 or 270 degrees
158 if (fabs(x) < fabs(y) / 1000000.0)
159 {
160 phi = y > 0 ? 90.0 : 270;
161 }
162 else
163 {
164 phi = 180.0 / M_PI * atan2(y, x);
165 if (phi < 0)
166 phi += 360.0;
167 }
168
169 return phi;
170}
171
172bool Calibration::calculate1D(double start_x, double start_y,
173 double end_x, double end_y, int RATotalPulse)
174{
175 return calculate1D(end_x - start_x, end_y - start_y, RATotalPulse);
176}
177
178bool Calibration::calculate1D(double dx, double dy, int RATotalPulse)
179{
180 const double arcSecondsX = dx * xArcsecondsPerPixel();
181 const double arcSecondsY = dy * yArcsecondsPerPixel();
182 const double arcSeconds = std::hypot(arcSecondsX, arcSecondsY);
183 if (arcSeconds < .1 || RATotalPulse <= 0)
184 {
185 qCDebug(KSTARS_EKOS_GUIDE)
186 << QString("Bad input to calculate1D: ra %1 %2 total pulse %3")
187 .arg(dx).arg(dy).arg(RATotalPulse);
188 return false;
189 }
190
191 const double phi = calculateRotation(arcSecondsX, arcSecondsY);
192 if (phi < 0)
193 return false;
194
195 setAngle(phi);
196 calibrationAngle = phi;
197 calibrationAngleRA = phi;
198 calibrationAngleDEC = -1;
199 decSwap = calibrationDecSwap = false;
200
201 if (RATotalPulse > 0)
202 setRaPulseMsPerArcsecond(RATotalPulse / arcSeconds);
203
204 if (raPulseMillisecondsPerArcsecond() > 10000)
205 {
206 qCDebug(KSTARS_EKOS_GUIDE)
207 << "Calibration computed unreasonable pulse-milliseconds-per-arcsecond: "
208 << raPulseMillisecondsPerArcsecond() << " & " << decPulseMillisecondsPerArcsecond();
209 }
210
211 initialized = true;
212 return true;
213}
214
215bool Calibration::calculate2D(
216 double start_ra_x, double start_ra_y, double end_ra_x, double end_ra_y,
217 double start_dec_x, double start_dec_y, double end_dec_x, double end_dec_y,
218 bool *reverse_dec_dir, int RATotalPulse, int DETotalPulse)
219{
220 return calculate2D((end_ra_x - start_ra_x),
221 (end_ra_y - start_ra_y),
222 (end_dec_x - start_dec_x),
223 (end_dec_y - start_dec_y),
224 reverse_dec_dir, RATotalPulse, DETotalPulse);
225}
226bool Calibration::calculate2D(
227 double ra_dx, double ra_dy, double dec_dx, double dec_dy,
228 bool *reverse_dec_dir, int RATotalPulse, int DETotalPulse)
229{
230 const double raArcsecondsdX = ra_dx * xArcsecondsPerPixel();
231 const double raArcsecondsdY = ra_dy * yArcsecondsPerPixel();
232 const double decArcsecondsdX = dec_dx * xArcsecondsPerPixel();
233 const double decArcsecondsdY = dec_dy * yArcsecondsPerPixel();
234 const double raArcseconds = std::hypot(raArcsecondsdX, raArcsecondsdY);
235 const double decArcseconds = std::hypot(decArcsecondsdX, decArcsecondsdY);
236 if (raArcseconds < .1 || decArcseconds < .1 || RATotalPulse <= 0 || DETotalPulse <= 0)
237 {
238 qCDebug(KSTARS_EKOS_GUIDE)
239 << QString("Bad input to calculate2D: ra %1 %2 dec %3 %4 total pulses %5 %6")
240 .arg(ra_dx).arg(ra_dy).arg(dec_dx).arg(dec_dy).arg(RATotalPulse).arg(DETotalPulse);
241 return false;
242 }
243
244 double phi_ra = 0; // angle calculated by GUIDE_RA drift
245 double phi_dec = 0; // angle calculated by GUIDE_DEC drift
246 double phi = 0;
247
248 GuiderUtils::Vector ra_vect = GuiderUtils::Normalize(GuiderUtils::Vector(raArcsecondsdX, raArcsecondsdY, 0));
249 GuiderUtils::Vector dec_vect = GuiderUtils::Normalize(GuiderUtils::Vector(decArcsecondsdX, decArcsecondsdY, 0));
250
251 GuiderUtils::Vector dec_vect_rotated_CCW = dec_vect * GuiderUtils::RotateZ(M_PI / 2);
252 GuiderUtils::Vector dec_vect_rotated_CW = dec_vect * GuiderUtils::RotateZ(-M_PI / 2);
253
254 double scalar_product_CCW = dec_vect_rotated_CCW & ra_vect;
255 double scalar_product_CW = dec_vect_rotated_CW & ra_vect;
256
257 bool ra_dec_is_CW_system = scalar_product_CCW > scalar_product_CW ? true : false;
258
259 phi_ra = calculateRotation(raArcsecondsdX, raArcsecondsdY);
260 if (phi_ra < 0)
261 return false;
262
263 phi_dec = calculateRotation(decArcsecondsdX, decArcsecondsdY);
264 if (phi_dec < 0)
265 return false;
266
267 // Store the calibration angles.
268 calibrationAngleRA = phi_ra;
269 calibrationAngleDEC = phi_dec;
270
271 if (ra_dec_is_CW_system)
272 phi_dec += 90;
273 else // ra-dec is standard CCW system
274 phi_dec -= 90;
275
276 if (phi_dec > 360)
277 phi_dec -= 360.0;
278 if (phi_dec < 0)
279 phi_dec += 360.0;
280
281 if (fabs(phi_dec - phi_ra) > 180)
282 {
283 if (phi_ra > phi_dec)
284 phi_ra -= 360;
285 else
286 phi_dec -= 360;
287 }
288
289 // average angles
290 phi = (phi_ra + phi_dec) / 2;
291 if (phi < 0)
292 phi += 360.0;
293
294 // setAngle sets the angle we'll use when guiding.
295 // calibrationAngle is the saved angle to be stored.
296 // They're the same now, but if we flip pier sides, angle may change.
297 setAngle(phi);
298 calibrationAngle = phi;
299
300 // check DEC: Make standard CCW coordinate system
301 if (reverse_dec_dir)
302 *reverse_dec_dir = decSwap = ra_dec_is_CW_system;
303 calibrationDecSwap = decSwap;
304
305 if (RATotalPulse > 0)
306 setRaPulseMsPerArcsecond(RATotalPulse / raArcseconds);
307
308 if (DETotalPulse > 0)
309 setDecPulseMsPerArcsecond(DETotalPulse / decArcseconds);
310
311 // Check for unreasonable values.
312 if (raPulseMillisecondsPerArcsecond() > 10000 || decPulseMillisecondsPerArcsecond() > 10000)
313 {
314 qCDebug(KSTARS_EKOS_GUIDE)
315 << "Calibration computed unreasonable pulse-milliseconds-per-arcsecond: "
316 << raPulseMillisecondsPerArcsecond() << " & " << decPulseMillisecondsPerArcsecond();
317 return false;
318 }
319
320 qCDebug(KSTARS_EKOS_GUIDE) << QString("Set RA ms/as = %1ms / %2as = %3. DEC: %4ms / %5px = %6.")
321 .arg(RATotalPulse).arg(raArcseconds).arg(raPulseMillisecondsPerArcsecond())
322 .arg(DETotalPulse).arg(decArcseconds).arg(decPulseMillisecondsPerArcsecond());
323 initialized = true;
324 return true;
325}
326
327void Calibration::computeDrift(const GuiderUtils::Vector &detection, const GuiderUtils::Vector &reference,
328 double *raDrift, double *decDrift) const
329{
330 GuiderUtils::Vector drift = detection - reference;
331 drift = rotateToRaDec(drift);
332 *raDrift = drift.x;
333 *decDrift = drift.y;
334}
335
336// Not sure why we allow this.
337void Calibration::setDeclinationSwapEnabled(bool value)
338{
339 qCDebug(KSTARS_EKOS_GUIDE) << QString("decSwap set to %1, was %2, cal %3")
340 .arg(value ? "T" : "F")
341 .arg(decSwap ? "T" : "F")
342 .arg(calibrationDecSwap ? "T" : "F");
343 decSwap = value;
344}
345
346QString Calibration::serialize() const
347{
348 QDateTime now = QDateTime::currentDateTime();
349 QString dateStr = now.toString("yyyy-MM-dd hh:mm:ss");
350 QString raStr = std::isnan(calibrationRA.Degrees()) ? "" : calibrationRA.toDMSString(false, true, true);
351 QString decStr = std::isnan(calibrationDEC.Degrees()) ? "" : calibrationDEC.toHMSString(true, true);
352 QString s =
353 QString("%16,bx=%1,by=%2,pw=%3,ph=%4,fl=%5,ang=%6,angR=%7,angD=%8,"
354 "ramspas=%9,decmspas=%10,swap=%11,ra=%12,dec=%13,side=%14,when=%15,calEnd")
355 .arg(subBinX).arg(subBinY).arg(ccd_pixel_width).arg(ccd_pixel_height)
356 .arg(focalMm).arg(calibrationAngle).arg(calibrationAngleRA)
357 .arg(calibrationAngleDEC).arg(raPulseMsPerArcsecond)
358 .arg(decPulseMsPerArcsecond).arg(calibrationDecSwap ? 1 : 0)
359 .arg(raStr).arg(decStr).arg(static_cast<int>(calibrationPierSide)).arg(dateStr).arg(CAL_VERSION);
360 return s;
361}
362
363namespace
364{
365bool parseString(const QString &ref, const QString &id, QString *result)
366{
367 if (!ref.startsWith(id)) return false;
368 *result = ref.mid(id.size());
369 return true;
370}
371
372bool parseDouble(const QString &ref, const QString &id, double *result)
373{
374 bool ok;
375 if (!ref.startsWith(id)) return false;
376 *result = ref.mid(id.size()).toDouble(&ok);
377 return ok;
378}
379
380bool parseInt(const QString &ref, const QString &id, int *result)
381{
382 bool ok;
383 if (!ref.startsWith(id)) return false;
384 *result = ref.mid(id.size()).toInt(&ok);
385 return ok;
386}
387} // namespace
388
389bool Calibration::restore(const QString &encoding)
390{
391 QStringList items = encoding.split(',');
392 if (items.size() != 17) return false;
393 int i = 0;
394
395 bool fixOldCalibration = false;
396 if (items[i] == CAL_VERSION_1_0)
397 {
398 fixOldCalibration = true;
399 }
400 else if (items[i] == CAL_VERSION) fixOldCalibration = false;
401 else return false;
402
403 if (!parseInt(items[++i], "bx=", &subBinX)) return false;
404 if (!parseInt(items[++i], "by=", &subBinY)) return false;
405 if (!parseDouble(items[++i], "pw=", &ccd_pixel_width)) return false;
406 if (!parseDouble(items[++i], "ph=", &ccd_pixel_height)) return false;
407 if (!parseDouble(items[++i], "fl=", &focalMm)) return false;
408 if (!parseDouble(items[++i], "ang=", &calibrationAngle)) return false;
409 setAngle(calibrationAngle);
410 if (!parseDouble(items[++i], "angR=", &calibrationAngleRA)) return false;
411 if (!parseDouble(items[++i], "angD=", &calibrationAngleDEC)) return false;
412
413 // Switched from storing raPulseMsPerPixel to ...PerArcsecond and similar for DEC.
414 // The below allows back-compatibility for older stored calibrations.
415 if (!parseDouble(items[++i], "ramspas=", &raPulseMsPerArcsecond))
416 {
417 // Try the old version
418 double raPulseMsPerPixel;
419 if (parseDouble(items[i], "ramspp=", &raPulseMsPerPixel) && (xArcsecondsPerPixel() > 0))
420 {
421 // The previous calibration only worked on square pixels.
422 raPulseMsPerArcsecond = raPulseMsPerPixel / xArcsecondsPerPixel();
423 }
424 else return false;
425 }
426 if (!parseDouble(items[++i], "decmspas=", &decPulseMsPerArcsecond))
427 {
428 // Try the old version
429 double decPulseMsPerPixel;
430 if (parseDouble(items[i], "decmspp=", &decPulseMsPerPixel) && (yArcsecondsPerPixel() > 0))
431 {
432 // The previous calibration only worked on square pixels.
433 decPulseMsPerArcsecond = decPulseMsPerPixel / yArcsecondsPerPixel();
434 }
435 else return false;
436 }
437
438 int tempInt;
439 if (!parseInt(items[++i], "swap=", &tempInt)) return false;
440 decSwap = static_cast<bool>(tempInt);
441 if (fixOldCalibration)
442 {
443 qCDebug(KSTARS_EKOS_GUIDE) << QString("Modifying v1.0 calibration");
444 decSwap = !decSwap;
445 }
446 calibrationDecSwap = decSwap;
447 QString tempStr;
448 if (!parseString(items[++i], "ra=", &tempStr)) return false;
449 dms nullDms;
450 calibrationRA = tempStr.size() == 0 ? nullDms : dms::fromString(tempStr, true);
451 if (!parseString(items[++i], "dec=", &tempStr)) return false;
452 calibrationDEC = tempStr.size() == 0 ? nullDms : dms::fromString(tempStr, false);
453 if (!parseInt(items[++i], "side=", &tempInt)) return false;
454 calibrationPierSide = static_cast<ISD::Mount::PierSide>(tempInt);
455 if (!parseString(items[++i], "when=", &tempStr)) return false;
456 // Don't keep the time. It's just for reference.
457 if (items[++i] != "calEnd") return false;
458 initialized = true;
459 return true;
460}
461
462void Calibration::save() const
463{
464 QString encoding = serialize();
465 Options::setSerializedCalibration(encoding);
466 qCDebug(KSTARS_EKOS_GUIDE) << QString("Saved calibration: %1").arg(encoding);
467}
468
469bool Calibration::restore(ISD::Mount::PierSide currentPierSide,
470 bool reverseDecOnPierChange, int currentBinX, int currentBinY,
471 const dms *declination)
472{
473 return restore(Options::serializedCalibration(), currentPierSide,
474 reverseDecOnPierChange, currentBinX, currentBinY, declination);
475}
476
477double Calibration::correctRA(double raMsPerArcsec, const dms &calibrationDec, const dms &currentDec)
478{
479 constexpr double MAX_DEC = 60.0;
480 // Don't use uninitialized dms values.
481 if (std::isnan(calibrationDec.Degrees()) || std::isnan(currentDec.Degrees()))
482 {
483 qCDebug(KSTARS_EKOS_GUIDE) << QString("Did not convert calibration RA rate. Input DEC invalid");
484 return raMsPerArcsec;
485 }
486 if ((calibrationDec.Degrees() > MAX_DEC) || (calibrationDec.Degrees() < -MAX_DEC))
487 {
488 qCDebug(KSTARS_EKOS_GUIDE) << QString("Didn't correct calibration RA rate. Calibration DEC %1 too close to pole")
489 .arg(QString::number(calibrationDec.Degrees(), 'f', 1));
490 return raMsPerArcsec;
491 }
492 const double toRadians = M_PI / 180.0;
493 const double numer = std::cos(currentDec.Degrees() * toRadians);
494 const double denom = std::cos(calibrationDec.Degrees() * toRadians);
495 if (raMsPerArcsec == 0) return raMsPerArcsec;
496 const double rate = 1.0 / raMsPerArcsec;
497 if (denom == 0) return raMsPerArcsec;
498 const double adjustedRate = numer * rate / denom;
499 if (adjustedRate == 0) return raMsPerArcsec;
500 const double adjustedMsPerArcsecond = 1.0 / adjustedRate;
501 qCDebug(KSTARS_EKOS_GUIDE) << QString("Corrected calibration RA rate. %1 --> %2. Calibration DEC %3 current DEC %4.")
502 .arg(QString::number(raMsPerArcsec, 'f', 1))
503 .arg(QString::number(adjustedMsPerArcsecond, 'f', 1))
504 .arg(QString::number(calibrationDec.Degrees(), 'f', 1))
505 .arg(QString::number(currentDec.Degrees(), 'f', 1));
506 return adjustedMsPerArcsecond;
507}
508
509bool Calibration::restore(const QString &encoding, ISD::Mount::PierSide currentPierSide,
510 bool reverseDecOnPierChange, int currentBinX, int currentBinY,
511 const dms *currentDeclination)
512{
513 // Fail if we couldn't read the calibration.
514 if (!restore(encoding))
515 {
516 qCDebug(KSTARS_EKOS_GUIDE) << "Could not restore calibration--couldn't read.";
517 return false;
518 }
519 // We don't restore calibrations where either the calibration or the current pier side
520 // is unknown.
521 if (calibrationPierSide == ISD::Mount::PIER_UNKNOWN ||
522 currentPierSide == ISD::Mount::PIER_UNKNOWN)
523 {
524 qCDebug(KSTARS_EKOS_GUIDE) << "Could not restore calibration--pier side unknown.";
525 return false;
526 }
527
528 if (currentDeclination != nullptr)
529 raPulseMsPerArcsecond = correctRA(raPulseMsPerArcsecond, calibrationDEC, *currentDeclination);
530
531 subBinXused = currentBinX;
532 subBinYused = currentBinY;
533
534 // Succeed if the calibration was on the same side of the pier as we're currently using.
535 if (currentPierSide == calibrationPierSide)
536 {
537 qCDebug(KSTARS_EKOS_GUIDE) << "Restored calibration--same pier side. Encoding:" << encoding;
538 return true;
539 }
540
541 // Otherwise, swap the angles and succeed.
542 angle = angle + 180.0;
543 while (angle >= 360.0)
544 angle = angle - 360.0;
545 while (angle < 0.0)
546 angle = angle + 360.0;
547 // Although angle is already set, we do this to set the rotation matrix.
548 setAngle(angle);
549
550 // Note the negation in testing the option.
551 // Since above the angle rotated by 180 degrees, both RA and DEC have already reversed.
552 // If the user says not to reverse DEC, then we re-reverse by setting decSwap.
553 // Doing it this way makes the option consistent with other programs the user may be familiar with (PHD2).
554 if (!reverseDecOnPierChange)
555 decSwap = !calibrationDecSwap;
556
557 qCDebug(KSTARS_EKOS_GUIDE)
558 << QString("Restored calibration--flipped angles. Angle %1, swap %2 ms/as: %3 %4. Encoding: %5")
559 .arg(angle).arg(decSwap ? "T" : "F").arg(raPulseMsPerArcsecond).arg(decPulseMsPerArcsecond).arg(encoding);
560 return true;
561}
An angle, stored as degrees, but expressible in many ways.
Definition dms.h:38
static dms fromString(const QString &s, bool deg)
Static function to create a DMS object from a QString.
Definition dms.cpp:429
const double & Degrees() const
Definition dms.h:141
QDateTime currentDateTime()
QString toString(QStringView format, QCalendar cal) const const
qsizetype size() const const
QString arg(Args &&... args) const const
QString mid(qsizetype position, qsizetype n) const const
QString number(double n, char format, int precision)
qsizetype size() const const
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
double toDouble(bool *ok) const const
int toInt(bool *ok, int base) const const
QTextStream & dec(QTextStream &stream)
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Jan 31 2025 11:53:47 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.