Changeset View
Changeset View
Standalone View
Standalone View
kstars/ekos/scheduler/schedulestrategy.cpp
- This file was added.
1 | /* Ekos Scheduling Strategy | ||||
---|---|---|---|---|---|
2 | Copyright (C) Jasem Mutlaq <mutlaqja@ikarustech.com> | ||||
3 | | ||||
4 | This application is free software; you can redistribute it and/or | ||||
5 | modify it under the terms of the GNU General Public | ||||
6 | License as published by the Free Software Foundation; either | ||||
7 | version 2 of the License, or (at your option) any later version. | ||||
8 | */ | ||||
9 | | ||||
10 | #include "schedulestrategy.h" | ||||
11 | | ||||
12 | #include "ksalmanac.h" | ||||
13 | #include "kstars.h" | ||||
14 | #include "kstarsdata.h" | ||||
15 | #include "skymapcomposite.h" | ||||
16 | #include "Options.h" | ||||
17 | | ||||
18 | #include <ekos_scheduler_debug.h> | ||||
19 | | ||||
20 | #define BAD_SCORE -1000 | ||||
21 | #define DEFAULT_MIN_ALTITUDE 15 | ||||
22 | | ||||
23 | namespace Ekos | ||||
24 | { | ||||
25 | | ||||
26 | ScheduleStrategy::ScheduleStrategy() { | ||||
27 | | ||||
28 | moon = dynamic_cast<KSMoon *>(KStarsData::Instance()->skyComposite()->findByName("Moon")); | ||||
29 | | ||||
30 | calculateDawnDusk(); | ||||
31 | } | ||||
32 | | ||||
33 | double ScheduleStrategy::findAltitude(const SkyPoint &target, const QDateTime &when, bool * is_setting, bool debug) | ||||
34 | { | ||||
35 | // FIXME: block calculating target coordinates at a particular time is duplicated in several places | ||||
36 | | ||||
37 | GeoLocation * const geo = KStarsData::Instance()->geo(); | ||||
38 | | ||||
39 | // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone! | ||||
40 | KStarsDateTime ltWhen(when.isValid() ? | ||||
41 | Qt::UTC == when.timeSpec() ? geo->UTtoLT(KStarsDateTime(when)) : when : | ||||
42 | KStarsData::Instance()->lt()); | ||||
43 | | ||||
44 | // Create a sky object with the target catalog coordinates | ||||
45 | SkyObject o; | ||||
46 | o.setRA0(target.ra0()); | ||||
47 | o.setDec0(target.dec0()); | ||||
48 | | ||||
49 | // Update RA/DEC of the target for the current fraction of the day | ||||
50 | KSNumbers numbers(ltWhen.djd()); | ||||
51 | o.updateCoordsNow(&numbers); | ||||
52 | | ||||
53 | // Calculate alt/az coordinates using KStars instance's geolocation | ||||
54 | CachingDms const LST = geo->GSTtoLST(geo->LTtoUT(ltWhen).gst()); | ||||
55 | o.EquatorialToHorizontal(&LST, geo->lat()); | ||||
56 | | ||||
57 | // Hours are reduced to [0,24[, meridian being at 0 | ||||
58 | double offset = LST.Hours() - o.ra().Hours(); | ||||
59 | if (24.0 <= offset) | ||||
60 | offset -= 24.0; | ||||
61 | else if (offset < 0.0) | ||||
62 | offset += 24.0; | ||||
63 | bool const passed_meridian = 0.0 <= offset && offset < 12.0; | ||||
64 | | ||||
65 | if (debug) | ||||
66 | qCDebug(KSTARS_EKOS_SCHEDULER) << QString("When:%9 LST:%8 RA:%1 RA0:%2 DEC:%3 DEC0:%4 alt:%5 setting:%6 HA:%7") | ||||
67 | .arg(o.ra().toHMSString()) | ||||
68 | .arg(o.ra0().toHMSString()) | ||||
69 | .arg(o.dec().toHMSString()) | ||||
70 | .arg(o.dec0().toHMSString()) | ||||
71 | .arg(o.alt().Degrees()) | ||||
72 | .arg(passed_meridian ? "yes" : "no") | ||||
73 | .arg(o.ra().Hours()) | ||||
74 | .arg(LST.toHMSString()) | ||||
75 | .arg(ltWhen.toString("HH:mm:ss")); | ||||
76 | | ||||
77 | if (is_setting) | ||||
78 | *is_setting = passed_meridian; | ||||
79 | | ||||
80 | return o.alt().Degrees(); | ||||
81 | } | ||||
82 | | ||||
83 | QDateTime ScheduleStrategy::calculateAltitudeTime(SchedulerJob const *job, double minAltitude, double minMoonAngle, QDateTime const &when) const | ||||
84 | { | ||||
85 | // FIXME: block calculating target coordinates at a particular time is duplicated in several places | ||||
86 | GeoLocation *geo = KStarsData::Instance()->geo(); | ||||
87 | | ||||
88 | // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone! | ||||
89 | KStarsDateTime ltWhen(when.isValid() ? | ||||
90 | Qt::UTC == when.timeSpec() ? geo->UTtoLT(KStarsDateTime(when)) : when : | ||||
91 | KStarsData::Instance()->lt()); | ||||
92 | | ||||
93 | // Create a sky object with the target catalog coordinates | ||||
94 | SkyPoint const target = job->getTargetCoords(); | ||||
95 | SkyObject o; | ||||
96 | o.setRA0(target.ra0()); | ||||
97 | o.setDec0(target.dec0()); | ||||
98 | | ||||
99 | // Calculate the UT at the argument time | ||||
100 | KStarsDateTime const ut = geo->LTtoUT(ltWhen); | ||||
101 | | ||||
102 | double const SETTING_ALTITUDE_CUTOFF = Options::settingAltitudeCutoff(); | ||||
103 | | ||||
104 | // Within the next 24 hours, search when the job target matches the altitude and moon constraints | ||||
105 | for (unsigned int minute = 0; minute < 24 * 60; minute++) | ||||
106 | { | ||||
107 | KStarsDateTime const ltOffset(ltWhen.addSecs(minute * 60)); | ||||
108 | | ||||
109 | // Update RA/DEC of the target for the current fraction of the day | ||||
110 | KSNumbers numbers(ltOffset.djd()); | ||||
111 | o.updateCoordsNow(&numbers); | ||||
112 | | ||||
113 | // Compute local sidereal time for the current fraction of the day, calculate altitude | ||||
114 | CachingDms const LST = geo->GSTtoLST(geo->LTtoUT(ltOffset).gst()); | ||||
115 | o.EquatorialToHorizontal(&LST, geo->lat()); | ||||
116 | double const altitude = o.alt().Degrees(); | ||||
117 | | ||||
118 | if (minAltitude <= altitude) | ||||
119 | { | ||||
120 | // Don't test proximity to dawn in this situation, we only cater for altitude here | ||||
121 | | ||||
122 | // Continue searching if Moon separation is not good enough | ||||
123 | if (0 < minMoonAngle && getMoonSeparationScore(job, ltOffset) < 0) | ||||
124 | continue; | ||||
125 | | ||||
126 | // Continue searching if target is setting and under the cutoff | ||||
127 | double offset = LST.Hours() - o.ra().Hours(); | ||||
128 | if (24.0 <= offset) | ||||
129 | offset -= 24.0; | ||||
130 | else if (offset < 0.0) | ||||
131 | offset += 24.0; | ||||
132 | if (0.0 <= offset && offset < 12.0) | ||||
133 | if (altitude - SETTING_ALTITUDE_CUTOFF < minAltitude) | ||||
134 | continue; | ||||
135 | | ||||
136 | return ltOffset; | ||||
137 | } | ||||
138 | } | ||||
139 | | ||||
140 | return QDateTime(); | ||||
141 | } | ||||
142 | | ||||
143 | QDateTime ScheduleStrategy::calculateCulmination(SchedulerJob const *job, int offset_minutes, QDateTime const &when) const | ||||
144 | { | ||||
145 | // FIXME: culmination calculation is a min altitude requirement, should be an interval altitude requirement | ||||
146 | // FIXME: block calculating target coordinates at a particular time is duplicated in calculateCulmination | ||||
147 | GeoLocation *geo = KStarsData::Instance()->geo(); | ||||
148 | | ||||
149 | // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone! | ||||
150 | KStarsDateTime ltWhen(when.isValid() ? | ||||
151 | Qt::UTC == when.timeSpec() ? geo->UTtoLT(KStarsDateTime(when)) : when : | ||||
152 | KStarsData::Instance()->lt()); | ||||
153 | | ||||
154 | // Create a sky object with the target catalog coordinates | ||||
155 | SkyPoint const target = job->getTargetCoords(); | ||||
156 | SkyObject o; | ||||
157 | o.setRA0(target.ra0()); | ||||
158 | o.setDec0(target.dec0()); | ||||
159 | | ||||
160 | // Update RA/DEC for the argument date/time | ||||
161 | KSNumbers numbers(ltWhen.djd()); | ||||
162 | o.updateCoordsNow(&numbers); | ||||
163 | | ||||
164 | // Calculate transit date/time at the argument date - transitTime requires UT and returns LocalTime | ||||
165 | KStarsDateTime transitDateTime(ltWhen.date(), o.transitTime(geo->LTtoUT(ltWhen), geo), Qt::LocalTime); | ||||
166 | | ||||
167 | // Shift transit date/time by the argument offset | ||||
168 | KStarsDateTime observationDateTime = transitDateTime.addSecs(offset_minutes * 60); | ||||
169 | | ||||
170 | // Relax observation time, culmination calculation is stable at minute only | ||||
171 | KStarsDateTime relaxedDateTime = observationDateTime.addSecs(Options::leadTime() * 60); | ||||
172 | | ||||
173 | // Verify resulting observation time is under lead time vs. argument time | ||||
174 | // If sooner, delay by 8 hours to get to the next transit - perhaps in a third call | ||||
175 | if (relaxedDateTime < ltWhen) | ||||
176 | { | ||||
177 | qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' startup %2 is posterior to transit %3, shifting by 8 hours.") | ||||
178 | .arg(job->getName()) | ||||
179 | .arg(ltWhen.toString(job->getDateTimeDisplayFormat())) | ||||
180 | .arg(relaxedDateTime.toString(job->getDateTimeDisplayFormat())); | ||||
181 | | ||||
182 | return calculateCulmination(job, offset_minutes, when.addSecs(8 * 60 * 60)); | ||||
183 | } | ||||
184 | | ||||
185 | // Guarantees - culmination calculation is stable at minute level, so relax by lead time | ||||
186 | Q_ASSERT_X(observationDateTime.isValid(), __FUNCTION__, "Observation time for target culmination is valid."); | ||||
187 | Q_ASSERT_X(ltWhen <= relaxedDateTime, __FUNCTION__, "Observation time for target culmination is at or after than argument time"); | ||||
188 | | ||||
189 | // Return consolidated culmination time | ||||
190 | return Qt::UTC == observationDateTime.timeSpec() ? geo->UTtoLT(observationDateTime) : observationDateTime; | ||||
191 | } | ||||
192 | | ||||
193 | #if 0 | ||||
194 | int16_t ScheduleStrategy::getWeatherScore() | ||||
195 | { | ||||
196 | if (weatherCheck->isEnabled() == false || weatherCheck->isChecked() == false) | ||||
197 | return 0; | ||||
198 | | ||||
199 | if (getWeatherStatus() == IPS_BUSY) | ||||
200 | return BAD_SCORE / 2; | ||||
201 | else if (getWeatherStatus() == IPS_ALERT) | ||||
202 | return BAD_SCORE; | ||||
203 | | ||||
204 | return 0; | ||||
205 | } | ||||
206 | #endif | ||||
207 | | ||||
208 | int16_t ScheduleStrategy::getDarkSkyScore(QDateTime const &when) const | ||||
209 | { | ||||
210 | double const secsPerDay = 24.0 * 3600.0; | ||||
211 | double const minsPerDay = 24.0 * 60.0; | ||||
212 | | ||||
213 | // Dark sky score is calculated based on distance to today's dawn and next dusk. | ||||
214 | // Option "Pre-dawn Time" avoids executing a job when dawn is approaching, and is a value in minutes. | ||||
215 | // - If observation is between option "Pre-dawn Time" and dawn, score is BAD_SCORE/50. | ||||
216 | // - If observation is before dawn today, score is fraction of the day from beginning of observation to dawn time, as percentage. | ||||
217 | // - If observation is after dusk, score is fraction of the day from dusk to beginning of observation, as percentage. | ||||
218 | // - If observation is between dawn and dusk, score is BAD_SCORE. | ||||
219 | // | ||||
220 | // If observation time is invalid, the score is calculated for the current day time. | ||||
221 | // Note exact dusk time is considered valid in terms of night time, and will return a positive, albeit null, score. | ||||
222 | | ||||
223 | // FIXME: Dark sky score should consider the middle of the local night as best value. | ||||
224 | // FIXME: Current algorithm uses the dawn and dusk of today, instead of the day of the observation. | ||||
225 | | ||||
226 | int const earlyDawnSecs = static_cast <int> ((Dawn - static_cast <double> (Options::preDawnTime()) / minsPerDay) * secsPerDay); | ||||
227 | int const dawnSecs = static_cast <int> (Dawn * secsPerDay); | ||||
228 | int const duskSecs = static_cast <int> (Dusk * secsPerDay); | ||||
229 | int const obsSecs = (when.isValid() ? when : KStarsData::Instance()->lt()).time().msecsSinceStartOfDay() / 1000; | ||||
230 | | ||||
231 | int16_t score = 0; | ||||
232 | | ||||
233 | if (earlyDawnSecs <= obsSecs && obsSecs < dawnSecs) | ||||
234 | { | ||||
235 | score = BAD_SCORE / 50; | ||||
236 | | ||||
237 | //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Dark sky score at %1 is %2 (between pre-dawn and dawn).") | ||||
238 | // .arg(observationDateTime.toString()) | ||||
239 | // .arg(QString::asprintf("%+d", score)); | ||||
240 | } | ||||
241 | else if (obsSecs < dawnSecs) | ||||
242 | { | ||||
243 | score = static_cast <int16_t> ((dawnSecs - obsSecs) / secsPerDay) * 100; | ||||
244 | | ||||
245 | //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Dark sky score at %1 is %2 (before dawn).") | ||||
246 | // .arg(observationDateTime.toString()) | ||||
247 | // .arg(QString::asprintf("%+d", score)); | ||||
248 | } | ||||
249 | else if (duskSecs <= obsSecs) | ||||
250 | { | ||||
251 | score = static_cast <int16_t> ((obsSecs - duskSecs) / secsPerDay) * 100; | ||||
252 | | ||||
253 | //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Dark sky score at %1 is %2 (after dusk).") | ||||
254 | // .arg(observationDateTime.toString()) | ||||
255 | // .arg(QString::asprintf("%+d", score)); | ||||
256 | } | ||||
257 | else | ||||
258 | { | ||||
259 | score = BAD_SCORE; | ||||
260 | | ||||
261 | //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Dark sky score at %1 is %2 (during daylight).") | ||||
262 | // .arg(observationDateTime.toString()) | ||||
263 | // .arg(QString::asprintf("%+d", score)); | ||||
264 | } | ||||
265 | | ||||
266 | return score; | ||||
267 | } | ||||
268 | | ||||
269 | int16_t ScheduleStrategy::calculateJobScore(SchedulerJob const *job, QDateTime const &when) const | ||||
270 | { | ||||
271 | if (nullptr == job) | ||||
272 | return BAD_SCORE; | ||||
273 | | ||||
274 | /* Only consolidate the score if light frames are required, calibration frames can run whenever needed */ | ||||
275 | if (!job->getLightFramesRequired()) | ||||
276 | return 1000; | ||||
277 | | ||||
278 | int16_t total = 0; | ||||
279 | | ||||
280 | /* As soon as one score is negative, it's a no-go and other scores are unneeded */ | ||||
281 | | ||||
282 | if (job->getEnforceTwilight()) | ||||
283 | { | ||||
284 | int16_t const darkSkyScore = getDarkSkyScore(when); | ||||
285 | | ||||
286 | qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' dark sky score is %2 at %3") | ||||
287 | .arg(job->getName()) | ||||
288 | .arg(QString::asprintf("%+d", darkSkyScore)) | ||||
289 | .arg(when.toString(job->getDateTimeDisplayFormat())); | ||||
290 | | ||||
291 | total += darkSkyScore; | ||||
292 | } | ||||
293 | | ||||
294 | /* We still enforce altitude if the job is neither required to track nor guide, because this is too confusing for the end-user. | ||||
295 | * If we bypass calculation here, it must also be bypassed when checking job constraints in checkJobStage. | ||||
296 | */ | ||||
297 | if (0 <= total /*&& ((job->getStepPipeline() & SchedulerJob::USE_TRACK) || (job->getStepPipeline() & SchedulerJob::USE_GUIDE))*/) | ||||
298 | { | ||||
299 | int16_t const altitudeScore = getAltitudeScore(job, when); | ||||
300 | | ||||
301 | qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' altitude score is %2 at %3") | ||||
302 | .arg(job->getName()) | ||||
303 | .arg(QString::asprintf("%+d", altitudeScore)) | ||||
304 | .arg(when.toString(job->getDateTimeDisplayFormat())); | ||||
305 | | ||||
306 | total += altitudeScore; | ||||
307 | } | ||||
308 | | ||||
309 | if (0 <= total) | ||||
310 | { | ||||
311 | int16_t const moonSeparationScore = getMoonSeparationScore(job, when); | ||||
312 | | ||||
313 | qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' Moon separation score is %2 at %3") | ||||
314 | .arg(job->getName()) | ||||
315 | .arg(QString::asprintf("%+d", moonSeparationScore)) | ||||
316 | .arg(when.toString(job->getDateTimeDisplayFormat())); | ||||
317 | | ||||
318 | total += moonSeparationScore; | ||||
319 | } | ||||
320 | | ||||
321 | qCInfo(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' has a total score of %2 at %3.") | ||||
322 | .arg(job->getName()) | ||||
323 | .arg(QString::asprintf("%+d", total)) | ||||
324 | .arg(when.toString(job->getDateTimeDisplayFormat())); | ||||
325 | | ||||
326 | return total; | ||||
327 | } | ||||
328 | | ||||
329 | int16_t ScheduleStrategy::getAltitudeScore(SchedulerJob const *job, QDateTime const &when) const | ||||
330 | { | ||||
331 | // FIXME: block calculating target coordinates at a particular time is duplicated in several places | ||||
332 | GeoLocation *geo = KStarsData::Instance()->geo(); | ||||
333 | | ||||
334 | // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone! | ||||
335 | KStarsDateTime ltWhen(when.isValid() ? | ||||
336 | Qt::UTC == when.timeSpec() ? geo->UTtoLT(KStarsDateTime(when)) : when : | ||||
337 | KStarsData::Instance()->lt()); | ||||
338 | | ||||
339 | // Create a sky object with the target catalog coordinates | ||||
340 | SkyPoint const target = job->getTargetCoords(); | ||||
341 | SkyObject o; | ||||
342 | o.setRA0(target.ra0()); | ||||
343 | o.setDec0(target.dec0()); | ||||
344 | | ||||
345 | // Update RA/DEC of the target for the current fraction of the day | ||||
346 | KSNumbers numbers(ltWhen.djd()); | ||||
347 | o.updateCoordsNow(&numbers); | ||||
348 | | ||||
349 | // Compute local sidereal time for the current fraction of the day, calculate altitude | ||||
350 | CachingDms const LST = geo->GSTtoLST(geo->LTtoUT(ltWhen).gst()); | ||||
351 | o.EquatorialToHorizontal(&LST, geo->lat()); | ||||
352 | double const altitude = o.alt().Degrees(); | ||||
353 | | ||||
354 | double const SETTING_ALTITUDE_CUTOFF = Options::settingAltitudeCutoff(); | ||||
355 | int16_t score = BAD_SCORE - 1; | ||||
356 | | ||||
357 | // If altitude is negative, bad score | ||||
358 | // FIXME: some locations may allow negative altitudes | ||||
359 | if (altitude < 0) | ||||
360 | { | ||||
361 | score = BAD_SCORE; | ||||
362 | } | ||||
363 | else if (-90 < job->getMinAltitude()) | ||||
364 | { | ||||
365 | // If under altitude constraint, bad score | ||||
366 | if (altitude < job->getMinAltitude()) | ||||
367 | score = BAD_SCORE; | ||||
368 | // Else if setting and under altitude cutoff, job would end soon after starting, bad score | ||||
369 | // FIXME: half bad score when under altitude cutoff risk getting positive again | ||||
370 | else | ||||
371 | { | ||||
372 | double offset = LST.Hours() - o.ra().Hours(); | ||||
373 | if (24.0 <= offset) | ||||
374 | offset -= 24.0; | ||||
375 | else if (offset < 0.0) | ||||
376 | offset += 24.0; | ||||
377 | if (0.0 <= offset && offset < 12.0) | ||||
378 | if (altitude - SETTING_ALTITUDE_CUTOFF < job->getMinAltitude()) | ||||
379 | score = BAD_SCORE / 2; | ||||
380 | } | ||||
381 | } | ||||
382 | // If not constrained but below minimum hard altitude, set score to 10% of altitude value | ||||
383 | else if (altitude < DEFAULT_MIN_ALTITUDE) | ||||
384 | { | ||||
385 | score = static_cast <int16_t> (altitude / 10.0); | ||||
386 | } | ||||
387 | | ||||
388 | // Else default score calculation without altitude constraint | ||||
389 | if (score < BAD_SCORE) | ||||
390 | score = static_cast <int16_t> ((1.5 * pow(1.06, altitude)) - (DEFAULT_MIN_ALTITUDE / 10.0)); | ||||
391 | | ||||
392 | //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' target altitude is %3 degrees at %2 (score %4).") | ||||
393 | // .arg(job->getName()) | ||||
394 | // .arg(when.toString(job->getDateTimeDisplayFormat())) | ||||
395 | // .arg(currentAlt, 0, 'f', minAltitude->decimals()) | ||||
396 | // .arg(QString::asprintf("%+d", score)); | ||||
397 | | ||||
398 | return score; | ||||
399 | } | ||||
400 | | ||||
401 | double ScheduleStrategy::getCurrentMoonSeparation(SchedulerJob const *job) const | ||||
402 | { | ||||
403 | // FIXME: block calculating target coordinates at a particular time is duplicated in several places | ||||
404 | GeoLocation *geo = KStarsData::Instance()->geo(); | ||||
405 | | ||||
406 | // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone! | ||||
407 | KStarsDateTime ltWhen(KStarsData::Instance()->lt()); | ||||
408 | | ||||
409 | // Create a sky object with the target catalog coordinates | ||||
410 | SkyPoint const target = job->getTargetCoords(); | ||||
411 | SkyObject o; | ||||
412 | o.setRA0(target.ra0()); | ||||
413 | o.setDec0(target.dec0()); | ||||
414 | | ||||
415 | // Update RA/DEC of the target for the current fraction of the day | ||||
416 | KSNumbers numbers(ltWhen.djd()); | ||||
417 | o.updateCoordsNow(&numbers); | ||||
418 | | ||||
419 | // Update moon | ||||
420 | //ut = geo->LTtoUT(ltWhen); | ||||
421 | //KSNumbers ksnum(ut.djd()); // BUG: possibly LT.djd() != UT.djd() because of translation | ||||
422 | //LST = geo->GSTtoLST(ut.gst()); | ||||
423 | CachingDms LST = geo->GSTtoLST(geo->LTtoUT(ltWhen).gst()); | ||||
424 | moon->updateCoords(&numbers, true, geo->lat(), &LST, true); | ||||
425 | | ||||
426 | // Moon/Sky separation p | ||||
427 | return moon->angularDistanceTo(&o).Degrees(); | ||||
428 | } | ||||
429 | | ||||
430 | int16_t ScheduleStrategy::getMoonSeparationScore(SchedulerJob const *job, QDateTime const &when) const | ||||
431 | { | ||||
432 | // FIXME: block calculating target coordinates at a particular time is duplicated in several places | ||||
433 | GeoLocation *geo = KStarsData::Instance()->geo(); | ||||
434 | | ||||
435 | // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone! | ||||
436 | KStarsDateTime ltWhen(when.isValid() ? | ||||
437 | Qt::UTC == when.timeSpec() ? geo->UTtoLT(KStarsDateTime(when)) : when : | ||||
438 | KStarsData::Instance()->lt()); | ||||
439 | | ||||
440 | // Create a sky object with the target catalog coordinates | ||||
441 | SkyPoint const target = job->getTargetCoords(); | ||||
442 | SkyObject o; | ||||
443 | o.setRA0(target.ra0()); | ||||
444 | o.setDec0(target.dec0()); | ||||
445 | | ||||
446 | // Update RA/DEC of the target for the current fraction of the day | ||||
447 | KSNumbers numbers(ltWhen.djd()); | ||||
448 | o.updateCoordsNow(&numbers); | ||||
449 | | ||||
450 | // Update moon | ||||
451 | //ut = geo->LTtoUT(ltWhen); | ||||
452 | //KSNumbers ksnum(ut.djd()); // BUG: possibly LT.djd() != UT.djd() because of translation | ||||
453 | //LST = geo->GSTtoLST(ut.gst()); | ||||
454 | CachingDms LST = geo->GSTtoLST(geo->LTtoUT(ltWhen).gst()); | ||||
455 | moon->updateCoords(&numbers, true, geo->lat(), &LST, true); | ||||
456 | | ||||
457 | double const moonAltitude = moon->alt().Degrees(); | ||||
458 | | ||||
459 | // Lunar illumination % | ||||
460 | double const illum = moon->illum() * 100.0; | ||||
461 | | ||||
462 | // Moon/Sky separation p | ||||
463 | double const separation = moon->angularDistanceTo(&o).Degrees(); | ||||
464 | | ||||
465 | // Zenith distance of the moon | ||||
466 | double const zMoon = (90 - moonAltitude); | ||||
467 | // Zenith distance of target | ||||
468 | double const zTarget = (90 - o.alt().Degrees()); | ||||
469 | | ||||
470 | int16_t score = 0; | ||||
471 | | ||||
472 | // If target = Moon, or no illuminiation, or moon below horizon, return static score. | ||||
473 | if (zMoon == zTarget || illum == 0 || zMoon >= 90) | ||||
474 | score = 100; | ||||
475 | else | ||||
476 | { | ||||
477 | // JM: Some magic voodoo formula I came up with! | ||||
478 | double moonEffect = (pow(separation, 1.7) * pow(zMoon, 0.5)) / (pow(zTarget, 1.1) * pow(illum, 0.5)); | ||||
479 | | ||||
480 | // Limit to 0 to 100 range. | ||||
481 | moonEffect = KSUtils::clamp(moonEffect, 0.0, 100.0); | ||||
482 | | ||||
483 | if (job->getMinMoonSeparation() > 0) | ||||
484 | { | ||||
485 | if (separation < job->getMinMoonSeparation()) | ||||
486 | score = BAD_SCORE * 5; | ||||
487 | else | ||||
488 | score = moonEffect; | ||||
489 | } | ||||
490 | else | ||||
491 | score = moonEffect; | ||||
492 | } | ||||
493 | | ||||
494 | // Limit to 0 to 20 | ||||
495 | score /= 5.0; | ||||
496 | | ||||
497 | //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' target is %L3 degrees from Moon (score %2).") | ||||
498 | // .arg(job->getName()) | ||||
499 | // .arg(separation, 0, 'f', 3) | ||||
500 | // .arg(QString::asprintf("%+d", score)); | ||||
501 | | ||||
502 | return score; | ||||
503 | } | ||||
504 | | ||||
505 | void ScheduleStrategy::calculateDawnDusk() | ||||
506 | { | ||||
507 | KSAlmanac ksal; | ||||
508 | Dawn = ksal.getDawnAstronomicalTwilight(); | ||||
509 | Dusk = ksal.getDuskAstronomicalTwilight(); | ||||
510 | | ||||
511 | QTime now = KStarsData::Instance()->lt().time(); | ||||
512 | QTime dawn = QTime(0, 0, 0).addSecs(Dawn * 24 * 3600); | ||||
513 | QTime dusk = QTime(0, 0, 0).addSecs(Dusk * 24 * 3600); | ||||
514 | | ||||
515 | duskDateTime.setDate(KStars::Instance()->data()->lt().date()); | ||||
516 | duskDateTime.setTime(dusk); | ||||
517 | | ||||
518 | // FIXME: reduce spam by moving twilight time to a text label | ||||
519 | //appendLogText(i18n("Astronomical twilight: dusk at %1, dawn at %2, and current time is %3", | ||||
520 | // dusk.toString(), dawn.toString(), now.toString())); | ||||
521 | } | ||||
522 | bool ScheduleStrategy::isWeatherOK(SchedulerJob *job) | ||||
523 | { | ||||
524 | if (weatherStatus == IPS_OK) | ||||
525 | return true; | ||||
526 | else if (weatherStatus == IPS_IDLE) | ||||
527 | { | ||||
528 | // if (indiState == INDI_READY) | ||||
529 | // qCInfo(KSTARS_EKOS_SCHEDULER) << i18n("Weather information is pending..."); | ||||
530 | return true; | ||||
531 | } | ||||
532 | | ||||
533 | // Temporary BUSY is ALSO accepted for now | ||||
534 | // TODO Figure out how to exactly handle this | ||||
535 | if (weatherStatus == IPS_BUSY) | ||||
536 | return true; | ||||
537 | | ||||
538 | if (weatherStatus == IPS_ALERT) | ||||
539 | { | ||||
540 | job->setState(SchedulerJob::JOB_ABORTED); | ||||
541 | qCInfo(KSTARS_EKOS_SCHEDULER) << i18n("Job '%1' suffers from bad weather, marking aborted.", job->getName()); | ||||
542 | } | ||||
543 | /*else if (weatherStatus == IPS_BUSY) | ||||
544 | { | ||||
545 | appendLogText(i18n("%1 observation job delayed due to bad weather.", job->getName())); | ||||
546 | schedulerTimer.stop(); | ||||
547 | connect(this, &Scheduler::weatherChanged, this, &Scheduler::resumeCheckStatus); | ||||
548 | }*/ | ||||
549 | | ||||
550 | return false; | ||||
551 | } | ||||
552 | | ||||
553 | void ScheduleStrategy::updatePreDawn() | ||||
554 | { | ||||
555 | double earlyDawn = Dawn - Options::preDawnTime() / (60.0 * 24.0); | ||||
556 | int dayOffset = 0; | ||||
557 | QTime dawn = QTime(0, 0, 0).addSecs(Dawn * 24 * 3600); | ||||
558 | if (KStarsData::Instance()->lt().time() >= dawn) | ||||
559 | dayOffset = 1; | ||||
560 | preDawnDateTime.setDate(KStarsData::Instance()->lt().date().addDays(dayOffset)); | ||||
561 | preDawnDateTime.setTime(QTime::fromMSecsSinceStartOfDay(earlyDawn * 24 * 3600 * 1000)); | ||||
562 | } | ||||
563 | | ||||
564 | } // namespace Ekos |