File indexing completed on 2024-04-14 14:11:45

0001 /*
0002     SPDX-FileCopyrightText: 2010 Akarsh Simha <akarshsimha@gmail.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "starhopper.h"
0008 
0009 #include "kstarsdata.h"
0010 #include "ksutils.h"
0011 #include "starcomponent.h"
0012 #include "skyobjects/starobject.h"
0013 
0014 #include <kstars_debug.h>
0015 
0016 QList<StarObject *> *StarHopper::computePath(const SkyPoint &src, const SkyPoint &dest, float fov__, float maglim__,
0017                                              QStringList *metadata_)
0018 {
0019     QList<const StarObject *> starHopList_const = computePath_const(src, dest, fov__, maglim__, metadata_);
0020     QList<StarObject *> *starHopList_unconst    = new QList<StarObject *>();
0021     foreach (const StarObject *so, starHopList_const)
0022     {
0023         starHopList_unconst->append(const_cast<StarObject *>(so));
0024     }
0025     return starHopList_unconst;
0026 }
0027 
0028 QList<const StarObject *> StarHopper::computePath_const(const SkyPoint &src, const SkyPoint &dest, float fov_,
0029                                                         float maglim_, QStringList *metadata)
0030 {
0031     fov    = fov_;
0032     maglim = maglim_;
0033     start  = &src;
0034     end    = &dest;
0035 
0036     came_from.clear();
0037     result_path.clear();
0038 
0039     // Implements the A* search algorithm
0040 
0041     QList<SkyPoint const *> cSet;
0042     QList<SkyPoint const *> oSet;
0043     QHash<SkyPoint const *, double> g_score;
0044     QHash<SkyPoint const *, double> f_score;
0045     QHash<SkyPoint const *, double> h_score;
0046 
0047     qCDebug(KSTARS) << "StarHopper is trying to compute a path from source: " << src.ra().toHMSString()
0048              << src.dec().toDMSString() << " to destination: " << dest.ra().toHMSString() << dest.dec().toDMSString()
0049              << "; a starhop of " << src.angularDistanceTo(&dest).Degrees() << " degrees!";
0050 
0051     oSet.append(&src);
0052     g_score[&src] = 0;
0053     h_score[&src] = src.angularDistanceTo(&dest).Degrees() / fov;
0054     f_score[&src] = h_score[&src];
0055 
0056     while (!oSet.isEmpty())
0057     {
0058         qCDebug(KSTARS) << "Next step";
0059         // Find the node with the lowest f_score value
0060         SkyPoint const *curr_node = nullptr;
0061         double lowfscore          = 1.0e8;
0062 
0063         foreach (const SkyPoint *sp, oSet)
0064         {
0065             if (f_score[sp] < lowfscore)
0066             {
0067                 lowfscore = f_score[sp];
0068                 curr_node = sp;
0069             }
0070         }
0071         if (curr_node == nullptr)
0072             continue;
0073 
0074         qCDebug(KSTARS) << "Lowest fscore (vertex distance-plus-cost score) is " << lowfscore
0075                  << " with coords: " << curr_node->ra().toHMSString() << curr_node->dec().toDMSString()
0076                  << ". Considering this node now.";
0077         if (curr_node == &dest || (curr_node != &src && h_score[curr_node] < 0.5))
0078         {
0079             // We are at destination
0080             reconstructPath(came_from[curr_node]);
0081             qCDebug(KSTARS) << "We've arrived at the destination! Yay! Result path count: " << result_path.count();
0082 
0083             // Just a test -- try to print out useful instructions to the debug console. Once we make star hopper unexperimental, we should move this to some sort of a display
0084             qCDebug(KSTARS) << "Star Hopping Directions: ";
0085             const SkyPoint *prevHop = start;
0086 
0087             for (auto &hopStar : result_path)
0088             {
0089                 QString direction;
0090                 QString spectralChar = "";
0091                 double pa; // should be 0 to 2pi
0092 
0093                 dms angDist = prevHop->angularDistanceTo(hopStar, &pa);
0094 
0095                 dms dmsPA;
0096                 dmsPA.setRadians(pa);
0097                 direction = KSUtils::toDirectionString(dmsPA);
0098 
0099                 if (metadata)
0100                 {
0101                     if (!patternNames.contains(hopStar))
0102                     {
0103                         spectralChar += hopStar->spchar();
0104                         starHopDirections = i18n(" Slew %1 degrees %2 to find an %3 star of mag %4 ", QString::number(angDist.Degrees(), 'f', 2),
0105                                                  direction, spectralChar, QString::number(hopStar->mag()));
0106                         qCDebug(KSTARS) << starHopDirections;
0107                     }
0108                     else
0109                     {
0110                         starHopDirections = i18n(" Slew %1 degrees %2 to find a(n) %3", QString::number(angDist.Degrees(), 'f', 2), direction,
0111                                                      patternNames.value(hopStar));
0112                         qCDebug(KSTARS) << starHopDirections;
0113                     }
0114                     metadata->append(starHopDirections);
0115                     qCDebug(KSTARS) << "Appended starHopDirections to metadata";
0116                 }
0117                 prevHop = hopStar;
0118             }
0119             qCDebug(KSTARS) << "  The destination is within a field-of-view";
0120 
0121             return result_path;
0122         }
0123 
0124         oSet.removeOne(curr_node);
0125         cSet.append(curr_node);
0126 
0127         // FIXME: Make sense. If current node ---> dest distance is
0128         // larger than src --> dest distance by more than 20%, don't
0129         // even bother considering it.
0130 
0131         if (h_score[curr_node] > h_score[&src] * 1.2)
0132         {
0133             qCDebug(KSTARS) << "Node under consideration has larger distance to destination (h-score) than start node! "
0134                         "Ignoring it.";
0135             continue;
0136         }
0137 
0138         // Get the list of stars that are neighbours of this node
0139         QList<StarObject *> neighbors;
0140 
0141         // FIXME: Actually, this should be done in
0142         // HorizontalToEquatorial, but we do it here because SkyPoint
0143         // needs a lot of fixing to handle unprecessed and precessed,
0144         // equatorial and horizontal coordinates nicely
0145         SkyPoint *CurrentNode = const_cast<SkyPoint *>(curr_node);
0146         //CurrentNode->deprecess(KStarsData::Instance()->updateNum());
0147         CurrentNode->catalogueCoord(KStarsData::Instance()->updateNum()->julianDay());
0148         //qCDebug(KSTARS) << "Calling starsInAperture";
0149         StarComponent::Instance()->starsInAperture(neighbors, *curr_node, fov, maglim);
0150         qCDebug(KSTARS) << "Choosing next node from a set of " << neighbors.count();
0151         // Look for the potential next node
0152         double curr_g_score = g_score[curr_node];
0153 
0154         for (auto &nhd_node : neighbors)
0155         {
0156             if (cSet.contains(nhd_node))
0157                 continue;
0158 
0159             // Compute the tentative g_score
0160             double tentative_g_score = curr_g_score + cost(curr_node, nhd_node);
0161             bool tentative_better;
0162             if (!oSet.contains(nhd_node))
0163             {
0164                 oSet.append(nhd_node);
0165                 tentative_better = true;
0166             }
0167             else if (tentative_g_score < g_score[nhd_node])
0168                 tentative_better = true;
0169             else
0170                 tentative_better = false;
0171 
0172             if (tentative_better)
0173             {
0174                 came_from[nhd_node] = curr_node;
0175                 g_score[nhd_node]   = tentative_g_score;
0176                 h_score[nhd_node]   = nhd_node->angularDistanceTo(&dest).Degrees() / fov;
0177                 f_score[nhd_node]   = g_score[nhd_node] + h_score[nhd_node];
0178             }
0179         }
0180     }
0181     qCDebug(KSTARS) << "REGRET! Returning empty list!";
0182     return QList<StarObject const *>(); // Return an empty QList
0183 }
0184 
0185 void StarHopper::reconstructPath(SkyPoint const *curr_node)
0186 {
0187     if (curr_node != start)
0188     {
0189         StarObject const *s = dynamic_cast<StarObject const *>(curr_node);
0190         Q_ASSERT(s);
0191         result_path.prepend(s);
0192         reconstructPath(came_from[s]);
0193     }
0194 }
0195 
0196 float StarHopper::cost(const SkyPoint *curr, const SkyPoint *next)
0197 {
0198     // This is a very heuristic method, that tries to produce a cost
0199     // for each hop.
0200 
0201     float netcost = 0;
0202 
0203     // Convert 'next' into a StarObject
0204     if (next == start)
0205     {
0206         // If the next hop is back to square one, junk it
0207         return 1e8;
0208     }
0209     bool isThisTheEnd = (next == end);
0210 
0211     float magcost, speccost;
0212     magcost = speccost = 0;
0213 
0214     if (!isThisTheEnd)
0215     {
0216         // We ought to be dealing with a star
0217         StarObject const *nextstar = dynamic_cast<StarObject const *>(next);
0218         Q_ASSERT(nextstar);
0219 
0220         // Test 1: How bright is the star?
0221         magcost =
0222             nextstar->mag() - 7.0 +
0223             5 * log10(
0224                     fov); // The brighter, the better. FIXME: 8.0 is now an arbitrary reference to the average faint star. Should actually depend on FOV, something like log( FOV ).
0225 
0226         // Test 2: Is the star strikingly red / yellow coloured?
0227         QString SpType = nextstar->sptype();
0228         char spclass   = SpType.at(0).toLatin1();
0229         speccost       = (spclass == 'G' || spclass == 'K' || spclass == 'M') ? -0.3 : 0;
0230         /*
0231         // Test 3: Is the star in the general direction of the object?
0232         // We use the cosine rule to find the angle between the hop direction, and the direction to destination
0233         // a = side joining curr to end
0234         // b = side joining curr to next
0235         // c = side joining next to end
0236         // C = angle between curr-next and curr-end
0237         double sina, sinb, cosa, cosb;
0238         curr->angularDistanceTo(dest).SinCos( &sina, &cosa );
0239         curr->angularDistanceTo(next).SinCos( &sinb, &cosb );
0240         double cosc = cos(next->angularDistanceTo(end).radians());
0241         double cosC = ( cosc - cosa * cosb ) / (sina * sinb);
0242         float dircost;
0243         if( cosC < 0 ) // Wrong direction!
0244         dircost = 1e8; // Some humongous number;
0245         else
0246         dircost = sqrt( 1 - cosC * cosC ) / cosC; // tan( C )
0247         */
0248     }
0249 
0250     // Test 4: How far is the hop?
0251     double distcost =
0252         (curr->angularDistanceTo(next).Degrees() /
0253          fov); // 1 "magnitude" incremental cost for 1 FOV. Is this even required, or is it just equivalent to halving our distance unit? I think it is required since the hop is not necessarily in the direction of the object -- asimha
0254 
0255     // Test 5: How effective is the hop? [Might not be required with A*]
0256     //    double distredcost = -((src->angularDistanceTo( dest ).Degrees() - next->angularDistanceTo( dest ).Degrees()) * 60 / fov)*3; // 3 "magnitudes" for 1 FOV closer
0257 
0258     // Test 6: Is the destination an asterism? Are there bright stars clustered nearby?
0259     QList<StarObject *> localNeighbors;
0260     StarComponent::Instance()->starsInAperture(localNeighbors, *next, fov / 10, maglim + 1.0);
0261     double stardensitycost = 1 - localNeighbors.count(); // -1 "magnitude" for every neighbouring star
0262 
0263 // Test 7: Identify star patterns
0264 
0265 #define RIGHT_ANGLE_THRESHOLD 0.05
0266 #define EQUAL_EDGE_THRESHOLD  0.025
0267 
0268     double patterncost = 0;
0269     QString patternName;
0270     if (!isThisTheEnd)
0271     {
0272         // We ought to be dealing with a star
0273         StarObject const *nextstar = dynamic_cast<StarObject const *>(next);
0274         Q_ASSERT(nextstar);
0275 
0276         float factor = 1.0;
0277         while (factor <= 10.0)
0278         {
0279             localNeighbors.clear();
0280             StarComponent::Instance()->starsInAperture(
0281                 localNeighbors, *next, fov / factor,
0282                 nextstar->mag() + 1.0); // Use a larger aperture for pattern identification; max 1.0 mag difference
0283             foreach (StarObject *star, localNeighbors)
0284             {
0285                 if (star == nextstar)
0286                     localNeighbors.removeOne(star);
0287                 else if (fabs(star->mag() - nextstar->mag()) > 1.0)
0288                     localNeighbors.removeOne(star);
0289             } // Now, we should have a pruned list
0290             factor += 1.0;
0291             if (localNeighbors.size() == 2)
0292                 break;
0293         }
0294         factor -= 1.0;
0295         if (localNeighbors.size() == 2)
0296         {
0297             patternName = i18n("triangle (of similar magnitudes)"); // any three stars form a triangle!
0298             // Try to find triangles. Note that we assume that the standard Euclidian metric works on a sphere for small angles, i.e. the celestial sphere is nearly flat over our FOV.
0299             StarObject *star1 = localNeighbors[0];
0300             double dRA1       = nextstar->ra().radians() - star1->ra().radians();
0301             double dDec1      = nextstar->dec().radians() - star1->dec().radians();
0302             double dist1sqr   = dRA1 * dRA1 + dDec1 * dDec1;
0303 
0304             StarObject *star2 = localNeighbors[1];
0305             double dRA2       = nextstar->ra().radians() - star2->ra().radians();
0306             double dDec2      = nextstar->dec().radians() - star2->dec().radians();
0307             double dist2sqr   = dRA2 * dRA2 + dDec2 * dDec2;
0308 
0309             // Check for right-angled triangles (without loss of generality, right angle is at this vertex)
0310             if (fabs((dRA1 * dRA2 - dDec1 * dDec2) / sqrt(dist1sqr * dist2sqr)) < RIGHT_ANGLE_THRESHOLD)
0311             {
0312                 // We have a right angled triangle! Give -3 magnitudes!
0313                 patterncost += -3;
0314                 patternName = i18n("right-angled triangle");
0315             }
0316 
0317             // Check for isosceles triangles (without loss of generality, this is the vertex)
0318             if (fabs((dist1sqr - dist2sqr) / (dist1sqr)) < EQUAL_EDGE_THRESHOLD)
0319             {
0320                 patterncost += -1;
0321                 patternName = i18n("isosceles triangle");
0322                 if (fabs((dRA2 * dDec1 - dRA1 * dDec2) / sqrt(dist1sqr * dist2sqr)) < RIGHT_ANGLE_THRESHOLD)
0323                 {
0324                     patterncost += -1;
0325                     patternName = i18n("straight line of 3 stars");
0326                 }
0327                 // Check for equilateral triangles
0328                 double dist3    = star1->angularDistanceTo(star2).radians();
0329                 double dist3sqr = dist3 * dist3;
0330                 if (fabs((dist3sqr - dist1sqr) / dist1sqr) < EQUAL_EDGE_THRESHOLD)
0331                 {
0332                     patterncost += -1;
0333                     patternName = i18n("equilateral triangle");
0334                 }
0335             }
0336         }
0337         // TODO: Identify squares.
0338         if (!patternName.isEmpty())
0339         {
0340             patternName += i18n(" within %1% of FOV of the marked star", (int)(100.0 / factor));
0341             patternNames.insert(nextstar, patternName);
0342         }
0343     }
0344 
0345     netcost = magcost + speccost + distcost + stardensitycost + patterncost;
0346     if (netcost < 0)
0347         netcost = 0.1; // FIXME: Heuristics aren't supposed to be entirely random. This one is.
0348     qCDebug(KSTARS) << "Mag cost: " << magcost << "; Spec Cost: " << speccost << "; Dist Cost: " << distcost
0349              << "; Density cost: " << stardensitycost << "; Pattern cost: " << patterncost << "; Net cost: " << netcost
0350              << "; Pattern: " << patternName;
0351     return netcost;
0352 }