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 }