File indexing completed on 2024-03-24 15:18:32

0001 /*
0002     SPDX-FileCopyrightText: 2020 Hy Murveit <hy@murveit.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "../indi/indiproperty.h"
0008 #include "ekos/guide/internalguide/guidestars.h"
0009 
0010 #include <QTest>
0011 
0012 #include <QObject>
0013 
0014 // The high-level methods, selectGuideStar() and findGuideStar() are not yet tested.
0015 // Neither are the SEP-related EvaluateSEPStars, findTopStars, findAllSEPStars().
0016 
0017 class TestGuideStars : public QObject
0018 {
0019         Q_OBJECT
0020 
0021     public:
0022         /** @short Constructor */
0023         TestGuideStars();
0024 
0025         /** @short Destructor */
0026         ~TestGuideStars() override = default;
0027 
0028     private slots:
0029         void basicTest();
0030         void calibrationTest();
0031 };
0032 
0033 #include "testguidestars.moc"
0034 #include "Options.h"
0035 
0036 TestGuideStars::TestGuideStars() : QObject()
0037 {
0038 }
0039 
0040 Edge makeEdge(float x, float y)
0041 {
0042     Edge e;
0043     e.x = x;
0044     e.y = y;
0045     e.HFR = 2.0;
0046     e.numPixels = 20;
0047     e.sum = 2000;
0048     return e;
0049 }
0050 
0051 #define CompareFloat(d1,d2) QVERIFY(fabs((d1) - (d2)) < .001)
0052 
0053 void TestGuideStars::basicTest()
0054 {
0055     Options::setMinDetectionsSEPMultistar(5);
0056     Options::setMaxMultistarReferenceStars(10);
0057     GuideStars g;
0058 
0059     // Test setCalibration() and calibration in general.
0060 
0061     // angle in degrees, pixel size in mm, focal length in mm.
0062     const int binning = 1;
0063     double angle = 0.0;
0064     const double pixel_size = 3e-3, focal_length = 750.0;
0065     dms ra, dec;
0066     ra.setFromString("120:30:40");
0067     dec.setFromString("10:20:30");
0068     ISD::Mount::PierSide side = ISD::Mount::PIER_EAST;
0069 
0070     Calibration cal;
0071     cal.setParameters(pixel_size, pixel_size, focal_length, binning, binning, side, ra, dec);
0072     cal.setAngle(angle);
0073     g.setCalibration(cal);
0074 
0075     // arcseconds = 3600*180/pi * (pix*ccd_pix_sz) / focal_len
0076     // Then needs to be rotated by the angle. Start with angle = 0;
0077     double arcsecondsPerPixel = (3600.0 * 180.0 / M_PI) * binning * pixel_size / focal_length;
0078 
0079     double x1 = 10, y1 = 50;
0080     GuiderUtils::Vector p(x1, y1, 0);
0081     GuiderUtils::Vector as = cal.convertToArcseconds(p);
0082     CompareFloat(as.x, arcsecondsPerPixel * x1);
0083     CompareFloat(as.y, arcsecondsPerPixel * y1);
0084 
0085     // Test computeStarDrift(), computing the distance between two stars in arcseconds.
0086     double dx = 2.5, dy = -0.7;
0087     Edge refStar = makeEdge(x1, y1);
0088     Edge star = makeEdge(x1 + dx, y1 + dy);
0089     double dRa, dDec;
0090     g.computeStarDrift(star, refStar, &dRa, &dDec);
0091     // Since the angle is 0, x differences should be reflected in RA, and y in DEC.
0092     CompareFloat(dRa, dx * arcsecondsPerPixel);
0093     // Y is inverted, because y=0 is at the top.
0094     CompareFloat(dDec, -dy * arcsecondsPerPixel);
0095 
0096     // Change the angle to 90, 180 and -90 degrees
0097     angle = 90.0;
0098     cal.setAngle(angle);
0099     g.setCalibration(cal);
0100     g.computeStarDrift(star, refStar, &dRa, &dDec);
0101     CompareFloat(-dDec, dx * arcsecondsPerPixel);
0102     CompareFloat(dRa, -dy * arcsecondsPerPixel);
0103 
0104     angle = 180.0;
0105     cal.setAngle(angle);
0106     g.setCalibration(cal);
0107     g.computeStarDrift(star, refStar, &dRa, &dDec);
0108     CompareFloat(-dRa, dx * arcsecondsPerPixel);
0109     CompareFloat(-dDec, -dy * arcsecondsPerPixel);
0110 
0111     angle = -90.0;
0112     cal.setAngle(angle);
0113     g.setCalibration(cal);
0114     g.computeStarDrift(star, refStar, &dRa, &dDec);
0115     CompareFloat(dDec, dx * arcsecondsPerPixel);
0116     CompareFloat(-dRa, -dy * arcsecondsPerPixel);
0117 
0118     // Use angle -90 so changes in x are changes in RA, and y --> -DEC.
0119     angle = 0.0;
0120     cal.setAngle(angle);
0121     g.setCalibration(cal);
0122 
0123     // Select the guide star.
0124 
0125     // Setup with 5 reference stars--a guide star and 4 others.
0126     double gstarX = 100, gstarY = 70;
0127     double gstar1X = 75, gstar1Y = 150;
0128     double gstar2X = 200, gstar2Y = 500;
0129     QList<Edge> stars;
0130     stars.append(makeEdge(gstarX, gstarY));
0131     stars.append(makeEdge(gstar1X, gstar1Y));
0132     stars.append(makeEdge(gstar2X, gstar2Y));
0133     stars.append(makeEdge(250, 800));
0134     stars.append(makeEdge(300, 800));
0135     QList<double> scores = { 40, 200, 70, 0, 0};
0136     QList<double> distances = { 100, 100, 100, 100, 100 };
0137     QVector3D gstar0 = g.selectGuideStar(stars, scores, 1000, 1000, distances);
0138     // It should select the highest scoring star (1), as it has the best score
0139     // and it isn't near an edge (near x,y = 0 or near x,y = 1000).
0140     CompareFloat(gstar0.x(), gstar1X);
0141     CompareFloat(gstar0.y(), gstar1Y);
0142 
0143     // If the guide star (#1) position is moved close to the border,
0144     // the next best star (#2) should be chosen.
0145     gstar1X = 10;
0146     stars[1].x = gstar1X;
0147     QVector3D gstar = g.selectGuideStar(stars, scores, 1000, 1000, distances);
0148     CompareFloat(gstar.x(), gstar2X);
0149     CompareFloat(gstar.y(), gstar2Y);
0150 
0151     // Now say that that star 2 has a close neighbor, it should go the #0
0152     distances[2] = 5;
0153     gstar = g.selectGuideStar(stars, scores, 1000, 1000, distances);
0154     CompareFloat(gstar.x(), gstarX);
0155     CompareFloat(gstar.y(), gstarY);
0156 
0157     // We have a guide star at (100,70) and other stars at
0158     // (10, 150), (200,500), (250, 800), (300, 800).
0159     // Normally the detected stars and the mapping from stars to the reference stars
0160     // is done inside findGuideStar(). To test the internals, we set these directly.
0161     g.setDetectedStars(stars);
0162     QVector<int> map = {0, 1, 2, 3, 4};
0163     g.setStarMap(map);
0164 
0165     // This is needed so SNR calculations can work. Parameters aren't important
0166     // as long as the star SNR wind up above 8dB.
0167     SkyBackground bg(10, 10, 1000);
0168     g.setSkyBackground(bg);
0169 
0170     bool success = g.getDrift(1, 100, 70, &dRa, &dDec);
0171     QVERIFY(success);
0172     // drift should be 0.
0173     CompareFloat(dRa, 0);
0174     CompareFloat(dDec, 0);
0175 
0176     // Move the reticle 1 pixel in x, should result in a drift of "arcsecondsPerPixel" in RA
0177     // and 0 in DEC.
0178     success = g.getDrift(1, 99, 70, &dRa, &dDec);
0179     QVERIFY(success);
0180     CompareFloat(dRa, arcsecondsPerPixel);
0181     CompareFloat(dDec, 0);
0182 
0183     // Similarly 2 pixels upward in y would affect DEC.
0184     success = g.getDrift(1, 100, 68, &dRa, &dDec);
0185     QVERIFY(success);
0186     CompareFloat(dRa, 0);
0187     CompareFloat(dDec, -2 * arcsecondsPerPixel);
0188 
0189     // Finally, since the drift is the median drift of the guide stars,
0190     // we move half up and half down and the middle will control the drift.
0191     // We only consider drifts within 2a-s of the guidestar (star 0) so
0192     // these drifts are bunched together.
0193     stars[0].x += 1;
0194     stars[0].y += 1;
0195     stars[1].x += 1;
0196     stars[1].y += 1;
0197     stars[2].x += 0.5;   // this controls the x-drift
0198     stars[2].y += 0.75;  // this controls the y-drift
0199     stars[3].x -= -.25;
0200     stars[3].y -= -.25;
0201     stars[4].x -= -.25;
0202     stars[4].y -= -.25;
0203     g.setDetectedStars(stars);
0204     success = g.getDrift(1, 100, 70, &dRa, &dDec);
0205     QVERIFY(success);
0206     CompareFloat(dRa, 0.5 * arcsecondsPerPixel);
0207     CompareFloat(dDec, -0.75 * arcsecondsPerPixel);
0208 
0209     // We don't accept multi-star drifts where the reference stars are too far (> 2 a-s)
0210     // from the guide-star drift. Here we move the guide star so it's different than
0211     // the rest, and it should control the drift.
0212     stars[0].x = 105;
0213     stars[0].y = 76;
0214     g.setDetectedStars(stars);
0215     success = g.getDrift(1, 100, 70, &dRa, &dDec);
0216     QVERIFY(success);
0217     CompareFloat(dRa, 5 * arcsecondsPerPixel);
0218     CompareFloat(dDec, -6 * arcsecondsPerPixel);
0219 
0220     // This should fail if either there aren't enough reference stars (< 2)
0221 
0222     // Test findMinDistance.
0223     double xx0 = 10, xx1 = 12, xx2 = 20;
0224     double yy0 = 7, yy1 = 13, yy2 = 4;
0225     Edge e0 = makeEdge(xx0, yy0);
0226     Edge e1 = makeEdge(xx1, yy1);
0227     Edge e2 = makeEdge(xx2, yy2);
0228     double d01 = hypot(xx0 - xx1, yy0 - yy1);
0229     double d02 = hypot(xx0 - xx2, yy0 - yy2);
0230     double d12 = hypot(xx1 - xx2, yy1 - yy2);
0231     QList<Edge *> edges;
0232     edges.append(&e0);
0233     edges.append(&e1);
0234     edges.append(&e2);
0235     CompareFloat(g.findMinDistance(0, edges), std::min(d01, d02));
0236     CompareFloat(g.findMinDistance(1, edges), std::min(d01, d12));
0237     CompareFloat(g.findMinDistance(2, edges), std::min(d02, d12));
0238 }
0239 
0240 // Takes the radians value input and converts to an angle in degrees 0 <= degrees < 360.
0241 double toDegrees(double radians)
0242 {
0243     double degrees = 360.0 * radians / (2.0 * M_PI);
0244     while (degrees < 0) degrees += 360.0;
0245     while (degrees >= 360.0) degrees -= 360.0;
0246     return degrees;
0247 }
0248 
0249 // This tests the Calibration class' API.
0250 void TestGuideStars::calibrationTest()
0251 {
0252     const int binning = 1;
0253     double angle = 0.0;
0254     const double pixel_size = 3e-3, focal_length = 750.0;
0255     Calibration cal;
0256     dms ra, dec;
0257     ra.setFromString("120:30:40");
0258     dec.setFromString("10:20:30");
0259     ISD::Mount::PierSide side = ISD::Mount::PIER_EAST;
0260 
0261     cal.setParameters(pixel_size, pixel_size, focal_length, binning, binning, side, ra, dec);
0262 
0263     // arcseconds = 3600*180/pi * (pix*ccd_pix_sz) / focal_len
0264     // Then needs to be rotated by the angle. Start with angle = 0;
0265     double arcsecondsPerPixel = (3600.0 * 180.0 / M_PI) * binning * pixel_size / focal_length;
0266 
0267     CompareFloat(angle, cal.getAngle());
0268     CompareFloat(focal_length, cal.getFocalLength());
0269 
0270     for (int i = -10; i <= 10; ++i)
0271     {
0272         GuiderUtils::Vector input(i, 2 * i, 0);
0273         GuiderUtils::Vector as = cal.convertToArcseconds(input);
0274         GuiderUtils::Vector px = cal.convertToPixels(input);
0275         CompareFloat(as.x, i * arcsecondsPerPixel);
0276         CompareFloat(as.y, 2 * i * arcsecondsPerPixel);
0277         CompareFloat(px.x, i / arcsecondsPerPixel);
0278         CompareFloat(px.y, 2 * i / arcsecondsPerPixel);
0279         double x, y;
0280         cal.convertToPixels(input.x, input.y, &x, &y);
0281         CompareFloat(x, i / arcsecondsPerPixel);
0282         CompareFloat(y, 2 * i / arcsecondsPerPixel);
0283     }
0284 
0285     CompareFloat(arcsecondsPerPixel, cal.xArcsecondsPerPixel());
0286     CompareFloat(arcsecondsPerPixel, cal.yArcsecondsPerPixel());
0287     CompareFloat(1.0 / arcsecondsPerPixel, cal.xPixelsPerArcsecond());
0288     CompareFloat(1.0 / arcsecondsPerPixel, cal.yPixelsPerArcsecond());
0289 
0290 
0291     // These are not yet estimated iniside Calibrate() so for now, just set them.
0292     //double raRate = 5.5, decRate = 3.3;
0293     double raMillisecondsPerPixel = 5.5;
0294     double raMillisecondsPerArcsecond = raMillisecondsPerPixel / arcsecondsPerPixel;
0295     double decMillisecondsPerPixel = 5.5;
0296     double decMillisecondsPerArcsecond = decMillisecondsPerPixel / arcsecondsPerPixel;
0297 
0298     cal.setRaPulseMsPerArcsecond(raMillisecondsPerArcsecond);
0299     cal.setDecPulseMsPerArcsecond(decMillisecondsPerArcsecond);
0300 
0301     CompareFloat(raMillisecondsPerArcsecond, cal.raPulseMillisecondsPerArcsecond());
0302     CompareFloat(decMillisecondsPerArcsecond, cal.decPulseMillisecondsPerArcsecond());
0303 
0304     /////////////////////////////////////////////////////////////////////
0305     // Check that conversions are right if binning is changed on the fly.
0306     /////////////////////////////////////////////////////////////////////
0307     const double bFactor = 3.0;
0308     cal.setBinningUsed(bFactor * binning, bFactor * binning);
0309     // In the loop below, the conversions to pixels will be smaller by bFactor (since we have
0310     // larger pixels with the binning), and the conversions to arc-seconds will increase by a factor
0311     // of bFactor, because each binned pixel is more arc-seconds than before.
0312     for (int i = -10; i <= 10; ++i)
0313     {
0314         GuiderUtils::Vector input(i, 2 * i, 0);
0315         GuiderUtils::Vector as = cal.convertToArcseconds(input);
0316         GuiderUtils::Vector px = cal.convertToPixels(input);
0317         CompareFloat(as.x, bFactor * i * arcsecondsPerPixel);
0318         CompareFloat(as.y, bFactor * 2 * i * arcsecondsPerPixel);
0319         CompareFloat(px.x, i / (bFactor * arcsecondsPerPixel));
0320         CompareFloat(px.y, 2 * i / (bFactor * arcsecondsPerPixel));
0321         double x, y;
0322         cal.convertToPixels(input.x, input.y, &x, &y);
0323         CompareFloat(x, i / (bFactor * arcsecondsPerPixel));
0324         CompareFloat(y, 2 * i / (bFactor * arcsecondsPerPixel));
0325     }
0326 
0327     CompareFloat(raMillisecondsPerArcsecond, cal.raPulseMillisecondsPerArcsecond());
0328     CompareFloat(decMillisecondsPerArcsecond, cal.decPulseMillisecondsPerArcsecond());
0329 
0330     CompareFloat(arcsecondsPerPixel * bFactor, cal.xArcsecondsPerPixel());
0331     CompareFloat(arcsecondsPerPixel * bFactor, cal.yArcsecondsPerPixel());
0332     CompareFloat(1.0 / (bFactor * arcsecondsPerPixel), cal.xPixelsPerArcsecond());
0333     CompareFloat(1.0 / (bFactor * arcsecondsPerPixel), cal.yPixelsPerArcsecond());
0334     cal.setBinningUsed(binning, binning);
0335     /////////////////////////////////////////////////////////////////////
0336 
0337     GuiderUtils::Vector px(1.0, 0.0, 0.0);
0338     cal.setAngle(0);
0339     GuiderUtils::Vector raDec = cal.rotateToRaDec(px);
0340     CompareFloat(px.x, raDec.x);
0341     CompareFloat(px.y, raDec.y);
0342     double rdx, rdy;
0343     cal.rotateToRaDec(px.x, px.y, &rdx, &rdy);
0344     CompareFloat(px.x, rdx);
0345     CompareFloat(px.y, rdy);
0346 
0347     cal.setAngle(90);
0348     raDec = cal.rotateToRaDec(px);
0349     CompareFloat(px.y, raDec.x);
0350     CompareFloat(-px.x, raDec.y);
0351     cal.rotateToRaDec(px.x, px.y, &rdx, &rdy);
0352     CompareFloat(px.y, rdx);
0353     CompareFloat(-px.x, rdy);
0354 
0355     cal.setAngle(180);
0356     raDec = cal.rotateToRaDec(px);
0357     CompareFloat(-px.x, raDec.x);
0358     CompareFloat(-px.y, raDec.y);
0359     cal.rotateToRaDec(px.x, px.y, &rdx, &rdy);
0360     CompareFloat(-px.x, rdx);
0361     CompareFloat(-px.y, rdy);
0362 
0363     cal.setAngle(270);
0364     raDec = cal.rotateToRaDec(px);
0365     CompareFloat(-px.y, raDec.x);
0366     CompareFloat(px.x, raDec.y);
0367     cal.rotateToRaDec(px.x, px.y, &rdx, &rdy);
0368     CompareFloat(-px.y, rdx);
0369     CompareFloat(px.x, rdy);
0370 
0371     // Test saving and restoring the calibration.
0372 
0373     // This should set the angle to 270 and keep raRate.
0374     cal.calculate1D(0, 10, raMillisecondsPerPixel * 10);
0375     angle = 270;
0376     int binX = 2, binY = 3;
0377     double pixSzW = .005, pixSzH = .006;
0378     cal.setParameters(pixSzW, pixSzH, focal_length, binX, binY, side, ra, dec);
0379     QString encodedCal = cal.serialize();
0380     Calibration cal2;
0381     QVERIFY(cal2.getFocalLength() != focal_length);
0382     QVERIFY(cal2.ccd_pixel_width != pixSzW);
0383     QVERIFY(cal2.ccd_pixel_height != pixSzH);
0384     QVERIFY(cal2.getAngle() != angle);
0385     QVERIFY(cal2.subBinX != binX);
0386     QVERIFY(cal2.subBinY != binY);
0387     QVERIFY(cal2.raPulseMillisecondsPerArcsecond() != raMillisecondsPerArcsecond);
0388     QVERIFY(cal2.decPulseMillisecondsPerArcsecond() != decMillisecondsPerArcsecond);
0389     QVERIFY(cal2.calibrationPierSide != side);
0390     QVERIFY(!(cal2.calibrationRA == ra));
0391     QVERIFY(!(cal2.calibrationDEC == dec));
0392     // swap defaults to false, and we haven't done anything to change it.
0393     QVERIFY(cal2.declinationSwapEnabled() == false);
0394 
0395     QVERIFY(!cal2.restore(""));
0396     QVERIFY(cal2.restore(encodedCal));
0397     cal2.setBinningUsed(binX, binY);
0398     QCOMPARE(cal2.getFocalLength(), focal_length);
0399     QCOMPARE(cal2.ccd_pixel_width, pixSzW);
0400     QCOMPARE(cal2.ccd_pixel_height, pixSzH);
0401     QCOMPARE(cal2.getAngle(), angle);
0402     QCOMPARE(cal2.subBinX, binX);
0403     QCOMPARE(cal2.subBinY, binY);
0404     CompareFloat(cal2.raPulseMillisecondsPerArcsecond(), raMillisecondsPerArcsecond);
0405     CompareFloat(cal2.decPulseMillisecondsPerArcsecond(), decMillisecondsPerArcsecond);
0406     QCOMPARE(cal2.calibrationPierSide, side);
0407     QVERIFY(cal2.calibrationRA == ra);
0408     QVERIFY(cal2.calibrationDEC == dec);
0409     // Swap still should be false.
0410     QVERIFY(cal2.declinationSwapEnabled() == false);
0411     bool swap;
0412 
0413     // This is an options checkbox that the user modifies depending on his/her mount.
0414     bool reverseDecOnPierChange = false;
0415 
0416     // Test restoring with a pier side.
0417     // This is same as above, as the encoded pier side was east.
0418     QVERIFY(cal2.restore(encodedCal, ISD::Mount::PIER_EAST, reverseDecOnPierChange, binning, binning));
0419     QCOMPARE(cal2.getAngle(), angle);
0420     QCOMPARE(cal2.declinationSwapEnabled(), false);
0421     // This tests that the rotation matrix got adjusted with the angle.
0422     cal2.rotateToRaDec(px.x, px.y, &rdx, &rdy);
0423     CompareFloat(-px.y, rdx);
0424     CompareFloat(px.x, rdy);
0425 
0426     // If we are now west, the angle should change by 180 degrees and dec-swap should invert.
0427     QVERIFY(cal2.restore(encodedCal, ISD::Mount::PIER_WEST, reverseDecOnPierChange, binning, binning));
0428     QCOMPARE(cal2.getAngle(), angle - 180.0);
0429     QCOMPARE(cal2.declinationSwapEnabled(), true);
0430     cal2.rotateToRaDec(px.x, px.y, &rdx, &rdy);
0431     CompareFloat(-px.y, -rdx);
0432     CompareFloat(px.x, -rdy);
0433 
0434     // Set the user option to reverse DEC on pier-side change.
0435     reverseDecOnPierChange = true;
0436     QVERIFY(cal2.restore(encodedCal, ISD::Mount::PIER_WEST, reverseDecOnPierChange, binning, binning));
0437     QCOMPARE(cal2.getAngle(), angle - 180.0);
0438     QCOMPARE(cal2.declinationSwapEnabled(), false);
0439     cal2.rotateToRaDec(px.x, px.y, &rdx, &rdy);
0440     CompareFloat(-px.y, -rdx);
0441     CompareFloat(px.x, -rdy);
0442     reverseDecOnPierChange = false;
0443 
0444     // If we go back east, the angle and decSwap should revert to their original values.
0445     QVERIFY(cal2.restore(encodedCal, ISD::Mount::PIER_EAST, reverseDecOnPierChange, binning, binning));
0446     QCOMPARE(cal2.getAngle(), angle);
0447     QCOMPARE(cal2.declinationSwapEnabled(), false);
0448     cal2.rotateToRaDec(px.x, px.y, &rdx, &rdy);
0449     CompareFloat(-px.y, rdx);
0450     CompareFloat(px.x, rdy);
0451 
0452     // Should not restore if the pier is unknown.
0453     QVERIFY(!cal2.restore(encodedCal, ISD::Mount::PIER_UNKNOWN, reverseDecOnPierChange, binning, binning));
0454 
0455     // Calculate the rotation.
0456     // Compute the angle the coordinates passed in make with the x-axis.
0457     // Oddly, though, the method first negates the y-coorainate (as images have y=0 on
0458     // top). So, account for that. Test in all 4 quadrents. Returns values 0-360 degrees.
0459     double x = 5.0, y = -7.0;
0460     QCOMPARE(Calibration::calculateRotation(x, y), toDegrees(atan2(-y, x)));
0461     x = -8.3, y = -2.4;
0462     QCOMPARE(Calibration::calculateRotation(x, y), toDegrees(atan2(-y, x)));
0463     x = -10.3, y = 8.2;
0464     QCOMPARE(Calibration::calculateRotation(x, y), toDegrees(atan2(-y, x)));
0465     x = 1.7, y = 8.2;
0466     QCOMPARE(Calibration::calculateRotation(x, y), toDegrees(atan2(-y, x)));
0467     x = 0, y = -8.0;
0468     QCOMPARE(Calibration::calculateRotation(x, y), toDegrees(atan2(-y, x)));
0469     x = 10, y = 0;
0470     QCOMPARE(Calibration::calculateRotation(x, y), toDegrees(atan2(-y, x)));
0471     // Short vectors (size less than 1.0) should return -1.
0472     x = .10, y = .20;
0473     QCOMPARE(Calibration::calculateRotation(x, y), -1.0);
0474 
0475     // Similar to above, a 1-D calibration.
0476     // Distance was 5.0 pixels, took 10 seconds of pulse
0477 
0478     // Set the pixels square for the below tests, since angles are in arc-seconds
0479     // and not pixel coordinates (this way both are equivalent).
0480     binX = 1.0;
0481     binY = 1.0;
0482     pixSzH = .005;
0483     pixSzW = .005;
0484 
0485     x = 3.0, y = -4.0;
0486     int pulseLength = 10000;
0487     side = ISD::Mount::PIER_WEST;
0488     cal.setParameters(pixSzW, pixSzH, focal_length, binX, binY, side, ra, dec);
0489     cal.calculate1D(x, y, pulseLength);
0490     CompareFloat(cal.getAngle(), toDegrees(atan2(-y, x)));
0491     CompareFloat(cal.raPulseMillisecondsPerArcsecond() * cal.xArcsecondsPerPixel(), pulseLength / std::hypot(x, y));
0492 
0493     // 2-D calibrations take coordinates and pulse lengths for both axes.
0494 
0495     // Made sure the ra and dec vectors were orthogonal and dec was 90-degrees greater
0496     // than ra (ignoring that y-flip).
0497     double ra_x = -16.0, ra_y = 16.0, dec_x = -12.0, dec_y = -12.0;
0498     double ra_factor = 750.0;
0499     int ra_pulse = std::hypot(ra_x, ra_y) * ra_factor;
0500     // Correct for rounding, since pulse is an integer.
0501     ra_factor = ra_pulse / std::hypot(ra_x, ra_y);
0502     double dec_factor = 300.0;
0503     int dec_pulse = std::hypot(dec_x, dec_y) * dec_factor;
0504     dec_factor = dec_pulse / std::hypot(dec_x, dec_y);
0505     cal.calculate2D(ra_x, ra_y, dec_x, dec_y, &swap, ra_pulse, dec_pulse);
0506     QCOMPARE(cal.raPulseMillisecondsPerArcsecond() * cal.xArcsecondsPerPixel(), ra_factor);
0507     QCOMPARE(cal.decPulseMillisecondsPerArcsecond() * cal.yArcsecondsPerPixel(), dec_factor);
0508     QCOMPARE(cal.getAngle(), toDegrees(atan2(-ra_y, ra_x)));
0509     QCOMPARE(swap, false);
0510 
0511     // Above we created a calibration where movement of size 1.0 along the x-axis in pixels
0512     // gets rotated towards -16,16 but keeps its original length.
0513     // Note, a 45-degree vector of length 1.0 has size length of sqrt(2)/2 in x and y.
0514     const double size1side = sqrt(2.0) / 2.0;
0515     cal.rotateToRaDec(1.0, 0.0, &rdx, &rdy);
0516     CompareFloat(rdx, -size1side);
0517     CompareFloat(rdy, size1side);
0518     // Similarly, a move (in dec) of size 1 down the y-axis would rotate toward -12,-12
0519     cal.rotateToRaDec(0.0, -1.0, &rdx, &rdy);
0520     CompareFloat(rdx, -size1side);
0521     CompareFloat(rdy, -size1side);
0522 
0523     // If we restored this on the EAST side
0524     QVERIFY(cal.restore(cal.serialize(), ISD::Mount::PIER_EAST, reverseDecOnPierChange, binning, binning));
0525     // ...RA moves should be inverted
0526     cal.rotateToRaDec(1.0, 0.0, &rdx, &rdy);
0527     CompareFloat(rdx, size1side);
0528     CompareFloat(rdy, -size1side);
0529     // ...and DEC moves should also invert.
0530     cal.rotateToRaDec(0.0, -1.0, &rdx, &rdy);
0531     CompareFloat(rdx, size1side);
0532     CompareFloat(rdy, size1side);
0533 
0534     // If we then move back to the WEST side, we should get the original results.
0535     QVERIFY(cal.restore(cal.serialize(), ISD::Mount::PIER_WEST, reverseDecOnPierChange, binning, binning));
0536     cal.rotateToRaDec(1.0, 0.0, &rdx, &rdy);
0537     CompareFloat(rdx, -size1side);
0538     CompareFloat(rdy, size1side);
0539     cal.rotateToRaDec(0.0, -1.0, &rdx, &rdy);
0540     CompareFloat(rdx, -size1side);
0541     CompareFloat(rdy, -size1side);
0542 
0543     // Test adjusting the RA rate according to DEC.
0544 
0545     dms calDec;
0546     calDec.setD(0, 0, 0);
0547     cal.setParameters(pixel_size, pixel_size, focal_length, binning, binning, side, ra, calDec);
0548     encodedCal = cal.serialize();
0549     double raPulseRate = cal.raPulseMillisecondsPerArcsecond() * cal.xArcsecondsPerPixel();
0550     dms currDec;
0551     currDec.setD(0, 0, 0);
0552     cal.restore(encodedCal, side, reverseDecOnPierChange, binning, binning, &currDec);
0553     CompareFloat(cal.raPulseMillisecondsPerArcsecond() * cal.xArcsecondsPerPixel(), raPulseRate);
0554     currDec.setD(70, 0, 0);
0555     cal.restore(encodedCal, side, reverseDecOnPierChange, binning, binning, &currDec);
0556     CompareFloat(cal.raPulseMillisecondsPerArcsecond() * cal.xArcsecondsPerPixel(),
0557                  raPulseRate / std::cos(70.0 * M_PI / 180.0));
0558     currDec.setD(-45, 0, 0);
0559     cal.restore(encodedCal, side, reverseDecOnPierChange, binning, binning, &currDec);
0560     CompareFloat(cal.raPulseMillisecondsPerArcsecond() * cal.xArcsecondsPerPixel(),
0561                  raPulseRate / std::cos(45.0 * M_PI / 180.0));
0562     currDec.setD(20, 0, 0);
0563     cal.restore(encodedCal, side, reverseDecOnPierChange, binning, binning, &currDec);
0564     CompareFloat(cal.raPulseMillisecondsPerArcsecond() * cal.xArcsecondsPerPixel(),
0565                  raPulseRate / std::cos(20.0 * M_PI / 180.0));
0566     // Set the rate back to its original value.
0567     currDec.setD(0, 0, 0);
0568     cal.restore(encodedCal, side, reverseDecOnPierChange, binning, binning, &currDec);
0569     CompareFloat(cal.raPulseMillisecondsPerArcsecond() * cal.xArcsecondsPerPixel(), raPulseRate);
0570 
0571     // Change the calibration DEC.
0572     // A null calibration DEC should result in no-change to the ra pulse rate.
0573     dms nullDEC;
0574     cal.setParameters(pixel_size, pixel_size, focal_length, binning, binning, side, ra, nullDEC);
0575     encodedCal = cal.serialize();
0576     raPulseRate = cal.raPulseMillisecondsPerArcsecond() * cal.xArcsecondsPerPixel();
0577     currDec.setD(20, 0, 0);
0578     cal.restore(encodedCal, side, reverseDecOnPierChange, binning, binning, &currDec);
0579     CompareFloat(cal.raPulseMillisecondsPerArcsecond() * cal.xArcsecondsPerPixel(), raPulseRate);
0580     // Both 20 should result in no change.
0581     calDec.setD(20, 0, 0);
0582     cal.setParameters(pixel_size, pixel_size, focal_length, binning, binning, side, ra, calDec);
0583     encodedCal = cal.serialize();
0584     currDec.setD(20, 0, 0);
0585     cal.restore(encodedCal, side, reverseDecOnPierChange, binning, binning, &currDec);
0586     CompareFloat(cal.raPulseMillisecondsPerArcsecond() * cal.xArcsecondsPerPixel(), raPulseRate);
0587     // Cal 20 and current 45 should change the rate accordingly.
0588     currDec.setD(45, 0, 0);
0589     cal.restore(encodedCal, side, reverseDecOnPierChange, binning, binning, &currDec);
0590     CompareFloat(cal.raPulseMillisecondsPerArcsecond() * cal.xArcsecondsPerPixel(),
0591                  raPulseRate * std::cos(20.0 * M_PI / 180.0) / std::cos(45.0 * M_PI / 180.0));
0592     // Changing cal dec to > 60-degrees or < -60 degrees results in no rate change.
0593     calDec.setD(65, 0, 0);
0594     cal.setParameters(pixel_size, pixel_size, focal_length, binning, binning, side, ra, calDec);
0595     encodedCal = cal.serialize();
0596     raPulseRate = cal.raPulseMillisecondsPerArcsecond() * cal.xArcsecondsPerPixel();
0597     currDec.setD(20, 0, 0);
0598     cal.restore(encodedCal, side, reverseDecOnPierChange, binning, binning, &currDec);
0599     CompareFloat(cal.raPulseMillisecondsPerArcsecond() * cal.xArcsecondsPerPixel(), raPulseRate);
0600     calDec.setD(-70, 0, 0);
0601     cal.setParameters(pixel_size, pixel_size, focal_length, binning, binning, side, ra, calDec);
0602     encodedCal = cal.serialize();
0603     raPulseRate = cal.raPulseMillisecondsPerArcsecond() * cal.xArcsecondsPerPixel();
0604     currDec.setD(20, 0, 0);
0605     cal.restore(encodedCal, side, reverseDecOnPierChange, binning, binning, &currDec);
0606     CompareFloat(cal.raPulseMillisecondsPerArcsecond() * cal.xArcsecondsPerPixel(), raPulseRate);
0607 }
0608 
0609 QTEST_GUILESS_MAIN(TestGuideStars)