Kstars

schedulerutils.cpp
1/*
2 SPDX-FileCopyrightText: 2023 Wolfgang Reissenberger <sterne-jaeger@openfuture.de>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6
7#include "schedulerutils.h"
8#include "schedulerjob.h"
9#include "schedulermodulestate.h"
10#include "ekos/capture/sequencejob.h"
11#include "Options.h"
12#include "skypoint.h"
13#include "kstarsdata.h"
14#include <ekos_scheduler_debug.h>
15
16namespace Ekos
17{
18
19SchedulerUtils::SchedulerUtils()
20{
21
22}
23
24SchedulerJob *SchedulerUtils::createJob(XMLEle *root)
25{
26 SchedulerJob *job = new SchedulerJob();
27 QString name, group;
28 dms ra, dec;
29 double rotation = 0.0, minimumAltitude = 0.0, minimumMoonSeparation = 0.0;
30 QUrl sequenceURL, fitsURL;
31 StartupCondition startup = START_ASAP;
32 CompletionCondition completion = FINISH_SEQUENCE;
33 QDateTime startupTime, completionTime;
34 int completionRepeats = 0;
35 bool enforceWeather = false, enforceTwilight = false, enforceArtificialHorizon = false,
36 track = false, focus = false, align = false, guide = false;
37
38
39 XMLEle *ep, *subEP;
40 // We expect all data read from the XML to be in the C locale - QLocale::c()
41 QLocale cLocale = QLocale::c();
42
43 for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
44 {
45 if (!strcmp(tagXMLEle(ep), "Name"))
46 name = pcdataXMLEle(ep);
47 else if (!strcmp(tagXMLEle(ep), "Group"))
48 group = pcdataXMLEle(ep);
49 else if (!strcmp(tagXMLEle(ep), "Coordinates"))
50 {
51 subEP = findXMLEle(ep, "J2000RA");
52 if (subEP)
53 ra.setH(cLocale.toDouble(pcdataXMLEle(subEP)));
54
55 subEP = findXMLEle(ep, "J2000DE");
56 if (subEP)
57 dec.setD(cLocale.toDouble(pcdataXMLEle(subEP)));
58 }
59 else if (!strcmp(tagXMLEle(ep), "Sequence"))
60 {
61 sequenceURL = QUrl::fromUserInput(pcdataXMLEle(ep));
62 }
63 else if (!strcmp(tagXMLEle(ep), "FITS"))
64 {
65 fitsURL = QUrl::fromUserInput(pcdataXMLEle(ep));
66 }
67 else if (!strcmp(tagXMLEle(ep), "PositionAngle"))
68 {
69 rotation = cLocale.toDouble(pcdataXMLEle(ep));
70 }
71 else if (!strcmp(tagXMLEle(ep), "StartupCondition"))
72 {
73 for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0))
74 {
75 if (!strcmp("ASAP", pcdataXMLEle(subEP)))
76 startup = START_ASAP;
77 else if (!strcmp("At", pcdataXMLEle(subEP)))
78 {
79 startup = START_AT;
80 startupTime = QDateTime::fromString(findXMLAttValu(subEP, "value"), Qt::ISODate);
81 // Todo sterne-jaeger 2024-01-01: setting time spec from KStars necessary?
82 }
83 }
84 }
85 else if (!strcmp(tagXMLEle(ep), "Constraints"))
86 {
87 for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0))
88 {
89 if (!strcmp("MinimumAltitude", pcdataXMLEle(subEP)))
90 {
91 Options::setEnableAltitudeLimits(true);
92 minimumAltitude = cLocale.toDouble(findXMLAttValu(subEP, "value"));
93 }
94 else if (!strcmp("MoonSeparation", pcdataXMLEle(subEP)))
95 {
96 Options::setSchedulerMoonSeparation(true);
97 minimumMoonSeparation = cLocale.toDouble(findXMLAttValu(subEP, "value"));
98 }
99 else if (!strcmp("EnforceWeather", pcdataXMLEle(subEP)))
100 enforceWeather = true;
101 else if (!strcmp("EnforceTwilight", pcdataXMLEle(subEP)))
102 enforceTwilight = true;
103 else if (!strcmp("EnforceArtificialHorizon", pcdataXMLEle(subEP)))
104 enforceArtificialHorizon = true;
105 }
106 }
107 else if (!strcmp(tagXMLEle(ep), "CompletionCondition"))
108 {
109 for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0))
110 {
111 if (!strcmp("Sequence", pcdataXMLEle(subEP)))
112 completion = FINISH_SEQUENCE;
113 else if (!strcmp("Repeat", pcdataXMLEle(subEP)))
114 {
115 completion = FINISH_REPEAT;
116 completionRepeats = cLocale.toInt(findXMLAttValu(subEP, "value"));
117 }
118 else if (!strcmp("Loop", pcdataXMLEle(subEP)))
119 completion = FINISH_LOOP;
120 else if (!strcmp("At", pcdataXMLEle(subEP)))
121 {
122 completion = FINISH_AT;
123 completionTime = QDateTime::fromString(findXMLAttValu(subEP, "value"), Qt::ISODate);
124 }
125 }
126 }
127 else if (!strcmp(tagXMLEle(ep), "Steps"))
128 {
129 XMLEle *module;
130
131 for (module = nextXMLEle(ep, 1); module != nullptr; module = nextXMLEle(ep, 0))
132 {
133 const char *proc = pcdataXMLEle(module);
134
135 if (!strcmp(proc, "Track"))
136 track = true;
137 else if (!strcmp(proc, "Focus"))
138 focus = true;
139 else if (!strcmp(proc, "Align"))
140 align = true;
141 else if (!strcmp(proc, "Guide"))
142 guide = true;
143 }
144 }
145 }
146 SchedulerUtils::setupJob(*job, name, group, ra, dec,
147 KStarsData::Instance()->ut().djd(),
148 rotation, sequenceURL, fitsURL,
149
150 startup, startupTime,
151 completion, completionTime, completionRepeats,
152
153 minimumAltitude,
154 minimumMoonSeparation,
155 enforceWeather,
156 enforceTwilight,
157 enforceArtificialHorizon,
158
159 track,
160 focus,
161 align,
162 guide);
163
164 return job;
165}
166
167void SchedulerUtils::setupJob(SchedulerJob &job, const QString &name, const QString &group, const dms &ra, const dms &dec,
168 double djd, double rotation, const QUrl &sequenceUrl, const QUrl &fitsUrl, StartupCondition startup,
169 const QDateTime &startupTime, CompletionCondition completion, const QDateTime &completionTime, int completionRepeats,
170 double minimumAltitude, double minimumMoonSeparation, bool enforceWeather, bool enforceTwilight,
171 bool enforceArtificialHorizon, bool track, bool focus, bool align, bool guide)
172{
173 /* Configure or reconfigure the observation job */
174
175 job.setName(name);
176 job.setGroup(group);
177 // djd should be ut.djd
178 job.setTargetCoords(ra, dec, djd);
179 job.setPositionAngle(rotation);
180
181 /* Consider sequence file is new, and clear captured frames map */
182 job.setCapturedFramesMap(CapturedFramesMap());
183 job.setSequenceFile(sequenceUrl);
184 job.setFITSFile(fitsUrl);
185 // #1 Startup conditions
186
187 job.setStartupCondition(startup);
188 if (startup == START_AT)
189 {
190 job.setStartupTime(startupTime);
191 }
192 /* Store the original startup condition */
193 job.setFileStartupCondition(job.getStartupCondition());
194 job.setFileStartupTime(job.getStartupTime());
195
196 // #2 Constraints
197
198 job.setMinAltitude(minimumAltitude);
199 job.setMinMoonSeparation(minimumMoonSeparation);
200
201 // Check enforce weather constraints
202 job.setEnforceWeather(enforceWeather);
203 // twilight constraints
204 job.setEnforceTwilight(enforceTwilight);
205 job.setEnforceArtificialHorizon(enforceArtificialHorizon);
206
207 job.setCompletionCondition(completion);
208 if (completion == FINISH_AT)
209 job.setCompletionTime(completionTime);
210 else if (completion == FINISH_REPEAT)
211 {
212 job.setRepeatsRequired(completionRepeats);
213 job.setRepeatsRemaining(completionRepeats);
214 }
215 // Job steps
216 job.setStepPipeline(SchedulerJob::USE_NONE);
217 if (track)
218 job.setStepPipeline(static_cast<SchedulerJob::StepPipeline>(job.getStepPipeline() | SchedulerJob::USE_TRACK));
219 if (focus)
220 job.setStepPipeline(static_cast<SchedulerJob::StepPipeline>(job.getStepPipeline() | SchedulerJob::USE_FOCUS));
221 if (align)
222 job.setStepPipeline(static_cast<SchedulerJob::StepPipeline>(job.getStepPipeline() | SchedulerJob::USE_ALIGN));
223 if (guide)
224 job.setStepPipeline(static_cast<SchedulerJob::StepPipeline>(job.getStepPipeline() | SchedulerJob::USE_GUIDE));
225
226 /* Store the original startup condition */
227 job.setFileStartupCondition(job.getStartupCondition());
228 job.setFileStartupTime(job.getStartupTime());
229
230 /* Reset job state to evaluate the changes */
231 job.reset();
232}
233
234uint16_t SchedulerUtils::fillCapturedFramesMap(const QMap<QString, uint16_t> &expected,
235 const CapturedFramesMap &capturedFramesCount, SchedulerJob &schedJob, CapturedFramesMap &capture_map,
236 int &completedIterations)
237{
238 uint16_t totalCompletedCount = 0;
239
240 // Figure out which repeat this is for the key with the least progress.
241 int minIterationsCompleted = -1, currentIteration = 0;
242 if (Options::rememberJobProgress())
243 {
244 completedIterations = 0;
245 for (const QString &key : expected.keys())
246 {
247 const int iterationsCompleted = capturedFramesCount[key] / expected[key];
248 if (minIterationsCompleted == -1 || iterationsCompleted < minIterationsCompleted)
249 minIterationsCompleted = iterationsCompleted;
250 }
251 // If this condition is FINISH_REPEAT, and we've already completed enough iterations
252 // Then set the currentIteratiion as 1 more than required. No need to go higher.
253 if (schedJob.getCompletionCondition() == FINISH_REPEAT
254 && minIterationsCompleted >= schedJob.getRepeatsRequired())
255 currentIteration = schedJob.getRepeatsRequired() + 1;
256 else
257 // Otherwise set it to one more than the number completed (i.e. the one it'll be working on).
258 currentIteration = minIterationsCompleted + 1;
259 completedIterations = std::max(0, currentIteration - 1);
260 }
261 else
262 // If we are not remembering progress, we'll only know the iterations completed
263 // by the current job's run.
264 completedIterations = schedJob.getCompletedIterations();
265
266 for (const QString &key : expected.keys())
267 {
268 if (Options::rememberJobProgress())
269 {
270 // If we're remembering progress, then figure out how many captures have not yet been captured.
271 const int diff = expected[key] * currentIteration - capturedFramesCount[key];
272
273 // Already captured more than required? Then don't capture any this round.
274 if (diff <= 0)
275 capture_map[key] = expected[key];
276 // Need more captures than one cycle could capture? If so, capture the full amount.
277 else if (diff >= expected[key])
278 capture_map[key] = 0;
279 // Otherwise we know that 0 < diff < expected[key]. Capture just the number needed.
280 else
281 capture_map[key] = expected[key] - diff;
282 }
283 else
284 // If we are not remembering progress, then the capture module, which reads this
285 // Will capture all requirements in the .esq file.
286 capture_map[key] = 0;
287
288 // collect all captured frames counts
289 if (schedJob.getCompletionCondition() == FINISH_LOOP)
290 totalCompletedCount += capturedFramesCount[key];
291 else
292 totalCompletedCount += std::min(capturedFramesCount[key],
293 static_cast<uint16_t>(expected[key] * schedJob.getRepeatsRequired()));
294 }
295 return totalCompletedCount;
296}
297
298void SchedulerUtils::updateLightFramesRequired(SchedulerJob *oneJob, const QList<SequenceJob *> &seqjobs,
299 const CapturedFramesMap &framesCount)
300{
301 bool lightFramesRequired = false;
303 switch (oneJob->getCompletionCondition())
304 {
305 case FINISH_SEQUENCE:
306 case FINISH_REPEAT:
307 // Step 1: determine expected frames
308 SchedulerUtils::calculateExpectedCapturesMap(seqjobs, expected);
309 // Step 2: compare with already captured frames
310 for (SequenceJob *oneSeqJob : seqjobs)
311 {
312 QString const signature = oneSeqJob->getSignature();
313 /* If frame is LIGHT, how many do we have left? */
314 if (oneSeqJob->getFrameType() == FRAME_LIGHT && expected[signature] * oneJob->getRepeatsRequired() > framesCount[signature])
315 {
316 lightFramesRequired = true;
317 // exit the loop, one found is sufficient
318 break;
319 }
320 }
321 break;
322 default:
323 // in all other cases it does not depend on the number of captured frames
324 lightFramesRequired = true;
325 }
326 oneJob->setLightFramesRequired(lightFramesRequired);
327}
328
329SequenceJob *SchedulerUtils::processSequenceJobInfo(XMLEle *root, SchedulerJob *schedJob)
330{
331 SequenceJob *job = new SequenceJob(root, schedJob->getName());
332 if (schedJob)
333 {
334 if (FRAME_LIGHT == job->getFrameType())
335 schedJob->setLightFramesRequired(true);
336 if (job->getCalibrationPreAction() & ACTION_PARK_MOUNT)
337 schedJob->setCalibrationMountPark(true);
338 }
339
340 auto placeholderPath = Ekos::PlaceholderPath();
341 placeholderPath.processJobInfo(job);
342
343 return job;
344}
345
346bool SchedulerUtils::loadSequenceQueue(const QString &fileURL, SchedulerJob *schedJob, QList<SequenceJob *> &jobs,
347 bool &hasAutoFocus, ModuleLogger *logger)
348{
349 QFile sFile;
350 sFile.setFileName(fileURL);
351
352 if (!sFile.open(QIODevice::ReadOnly))
353 {
354 if (logger != nullptr) logger->appendLogText(i18n("Unable to open sequence queue file '%1'", fileURL));
355 return false;
356 }
357
358 LilXML *xmlParser = newLilXML();
359 char errmsg[MAXRBUF];
360 XMLEle *root = nullptr;
361 XMLEle *ep = nullptr;
362 char c;
363
364 while (sFile.getChar(&c))
365 {
366 root = readXMLEle(xmlParser, c, errmsg);
367
368 if (root)
369 {
370 for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
371 {
372 if (!strcmp(tagXMLEle(ep), "Autofocus"))
373 hasAutoFocus = (!strcmp(findXMLAttValu(ep, "enabled"), "true"));
374 else if (!strcmp(tagXMLEle(ep), "Job"))
375 {
376 SequenceJob *thisJob = processSequenceJobInfo(ep, schedJob);
377 jobs.append(thisJob);
378 if (jobs.count() == 1)
379 {
380 auto &firstJob = jobs.first();
381 if (FRAME_LIGHT == firstJob->getFrameType() && nullptr != schedJob)
382 {
383 schedJob->setInitialFilter(firstJob->getCoreProperty(SequenceJob::SJ_Filter).toString());
384 }
385
386 }
387 }
388 }
389 delXMLEle(root);
390 }
391 else if (errmsg[0])
392 {
393 if (logger != nullptr) logger->appendLogText(QString(errmsg));
394 delLilXML(xmlParser);
395 qDeleteAll(jobs);
396 return false;
397 }
398 }
399
400 return true;
401}
402
403bool SchedulerUtils::estimateJobTime(SchedulerJob *schedJob, const QMap<QString, uint16_t> &capturedFramesCount,
404 ModuleLogger *logger)
405{
406 static SchedulerJob *jobWarned = nullptr;
407
408 // Load the sequence job associated with the argument scheduler job.
409 QList<SequenceJob *> seqJobs;
410 bool hasAutoFocus = false;
411 bool result = loadSequenceQueue(schedJob->getSequenceFile().toLocalFile(), schedJob, seqJobs, hasAutoFocus, logger);
412 if (result == false)
413 {
414 qCWarning(KSTARS_EKOS_SCHEDULER) <<
415 QString("Warning: Failed estimating the duration of job '%1', its sequence file is invalid.").arg(
416 schedJob->getSequenceFile().toLocalFile());
417 return result;
418 }
419
420 // FIXME: setting in-sequence focus should be done in XML processing.
421 schedJob->setInSequenceFocus(hasAutoFocus);
422
423 // Stop spam of log on re-evaluation. If we display the warning once, then that's it.
424 if (schedJob != jobWarned && hasAutoFocus && !(schedJob->getStepPipeline() & SchedulerJob::USE_FOCUS))
425 {
426 logger->appendLogText(
427 i18n("Warning: Job '%1' has its focus step disabled, periodic and/or HFR procedures currently set in its sequence will not occur.",
428 schedJob->getName()));
429 jobWarned = schedJob;
430 }
431
432 /* This is the map of captured frames for this scheduler job, keyed per storage signature.
433 * It will be forwarded to the Capture module in order to capture only what frames are required.
434 * If option "Remember Job Progress" is disabled, this map will be empty, and the Capture module will process all requested captures unconditionally.
435 */
436 CapturedFramesMap capture_map;
437 bool const rememberJobProgress = Options::rememberJobProgress();
438
439 double totalImagingTime = 0;
440 double imagingTimePerRepeat = 0, imagingTimeLeftThisRepeat = 0;
441
442 // Determine number of captures in the scheduler job
444 uint16_t allCapturesPerRepeat = calculateExpectedCapturesMap(seqJobs, expected);
445
446 // fill the captured frames map
447 int completedIterations;
448 uint16_t totalCompletedCount = fillCapturedFramesMap(expected, capturedFramesCount, *schedJob, capture_map,
449 completedIterations);
450 schedJob->setCompletedIterations(completedIterations);
451 // Loop through sequence jobs to calculate the number of required frames and estimate duration.
452 foreach (SequenceJob *seqJob, seqJobs)
453 {
454 // FIXME: find a way to actually display the filter name.
455 QString seqName = i18n("Job '%1' %2x%3\" %4", schedJob->getName(), seqJob->getCoreProperty(SequenceJob::SJ_Count).toInt(),
456 seqJob->getCoreProperty(SequenceJob::SJ_Exposure).toDouble(),
457 seqJob->getCoreProperty(SequenceJob::SJ_Filter).toString());
458
459 if (seqJob->getUploadMode() == ISD::Camera::UPLOAD_LOCAL)
460 {
461 qCInfo(KSTARS_EKOS_SCHEDULER) <<
462 QString("%1 duration cannot be estimated time since the sequence saves the files remotely.").arg(seqName);
463 schedJob->setEstimatedTime(-2);
464 qDeleteAll(seqJobs);
465 return true;
466 }
467
468 // Note that looping jobs will have zero repeats required.
469 QString const signature = seqJob->getSignature();
470 QString const signature_path = QFileInfo(signature).path();
471 int captures_required = seqJob->getCoreProperty(SequenceJob::SJ_Count).toInt() * schedJob->getRepeatsRequired();
472 int captures_completed = capturedFramesCount[signature];
473 const int capturesRequiredPerRepeat = std::max(1, seqJob->getCoreProperty(SequenceJob::SJ_Count).toInt());
474 int capturesLeftThisRepeat = std::max(0, capturesRequiredPerRepeat - (captures_completed % capturesRequiredPerRepeat));
475 if (captures_completed >= (1 + completedIterations) * capturesRequiredPerRepeat)
476 {
477 // Something else is causing this iteration to be incomplete. Nothing left to do for this seqJob.
478 capturesLeftThisRepeat = 0;
479 }
480
481 if (rememberJobProgress && schedJob->getCompletionCondition() != FINISH_LOOP)
482 {
483 /* Enumerate sequence jobs associated to this scheduler job, and assign them a completed count.
484 *
485 * The objective of this block is to fill the storage map of the scheduler job with completed counts for each capture storage.
486 *
487 * Sequence jobs capture to a storage folder, and are given a count of captures to store at that location.
488 * The tricky part is to make sure the repeat count of the scheduler job is properly transferred to each sequence job.
489 *
490 * For instance, a scheduler job repeated three times must execute the full list of sequence jobs three times, thus
491 * has to tell each sequence job it misses all captures, three times. It cannot tell the sequence job three captures are
492 * missing, first because that's not how the sequence job is designed (completed count, not required count), and second
493 * because this would make the single sequence job repeat three times, instead of repeating the full list of sequence
494 * jobs three times.
495 *
496 * The consolidated storage map will be assigned to each sequence job based on their signature when the scheduler job executes them.
497 *
498 * For instance, consider a RGBL sequence of single captures. The map will store completed captures for R, G, B and L storages.
499 * If R and G have 1 file each, and B and L have no files, map[storage(R)] = map[storage(G)] = 1 and map[storage(B)] = map[storage(L)] = 0.
500 * When that scheduler job executes, only B and L captures will be processed.
501 *
502 * In the case of a RGBLRGB sequence of single captures, the second R, G and B map items will count one less capture than what is really in storage.
503 * If R and G have 1 file each, and B and L have no files, map[storage(R1)] = map[storage(B1)] = 1, and all others will be 0.
504 * When that scheduler job executes, B1, L, R2, G2 and B2 will be processed.
505 *
506 * This doesn't handle the case of duplicated scheduler jobs, that is, scheduler jobs with the same storage for capture sets.
507 * Those scheduler jobs will all change state to completion at the same moment as they all target the same storage.
508 * This is why it is important to manage the repeat count of the scheduler job, as stated earlier.
509 */
510
511 captures_required = expected[seqJob->getSignature()] * schedJob->getRepeatsRequired();
512
513 qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 sees %2 captures in output folder '%3'.").arg(seqName).arg(
514 captures_completed).arg(QFileInfo(signature).path());
515
516 // Enumerate sequence jobs to check how many captures are completed overall in the same storage as the current one
517 foreach (SequenceJob *prevSeqJob, seqJobs)
518 {
519 // Enumerate seqJobs up to the current one
520 if (seqJob == prevSeqJob)
521 break;
522
523 // If the previous sequence signature matches the current, skip counting to take duplicates into account
524 if (!signature.compare(prevSeqJob->getSignature()))
525 captures_required = 0;
526
527 // And break if no captures remain, this job does not need to be executed
528 if (captures_required == 0)
529 break;
530 }
531
532 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("%1 has completed %2/%3 of its required captures in output folder '%4'.").arg(
533 seqName).arg(captures_completed).arg(captures_required).arg(signature_path);
534
535 }
536 // Else rely on the captures done during this session
537 else if (0 < allCapturesPerRepeat)
538 {
539 captures_completed = schedJob->getCompletedCount() / allCapturesPerRepeat * seqJob->getCoreProperty(
540 SequenceJob::SJ_Count).toInt();
541 }
542 else
543 {
544 captures_completed = 0;
545 }
546
547 // Check if we still need any light frames. Because light frames changes the flow of the observatory startup
548 // Without light frames, there is no need to do focusing, alignment, guiding...etc
549 // We check if the frame type is LIGHT and if either the number of captures_completed frames is less than required
550 // OR if the completion condition is set to LOOP so it is never complete due to looping.
551 // Note that looping jobs will have zero repeats required.
552 // FIXME: As it is implemented now, FINISH_LOOP may loop over a capture-complete, therefore inoperant, scheduler job.
553 bool const areJobCapturesComplete = (0 == captures_required || captures_completed >= captures_required);
554 if (seqJob->getFrameType() == FRAME_LIGHT)
555 {
556 if(areJobCapturesComplete)
557 {
558 qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 completed its sequence of %2 light frames.").arg(seqName).arg(
559 captures_required);
560 }
561 }
562 else
563 {
564 qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 captures calibration frames.").arg(seqName);
565 }
566
567 /* If captures are not complete, we have imaging time left */
568 if (!areJobCapturesComplete || schedJob->getCompletionCondition() == FINISH_LOOP)
569 {
570 unsigned int const captures_to_go = captures_required - captures_completed;
571 const double secsPerCapture = (seqJob->getCoreProperty(SequenceJob::SJ_Exposure).toDouble() +
572 (seqJob->getCoreProperty(SequenceJob::SJ_Delay).toInt() / 1000.0));
573 totalImagingTime += fabs(secsPerCapture * captures_to_go);
574 imagingTimePerRepeat += fabs(secsPerCapture * seqJob->getCoreProperty(SequenceJob::SJ_Count).toInt());
575 imagingTimeLeftThisRepeat += fabs(secsPerCapture * capturesLeftThisRepeat);
576 /* If we have light frames to process, add focus/dithering delay */
577 if (seqJob->getFrameType() == FRAME_LIGHT)
578 {
579 // If inSequenceFocus is true
580 if (hasAutoFocus)
581 {
582 // Wild guess, 10s of autofocus for each capture required. Can vary a lot, but this is just a completion estimate.
583 constexpr int afSecsPerCapture = 10;
584 qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 requires a focus procedure.").arg(seqName);
585 totalImagingTime += captures_to_go * afSecsPerCapture;
586 imagingTimePerRepeat += capturesRequiredPerRepeat * afSecsPerCapture;
587 imagingTimeLeftThisRepeat += capturesLeftThisRepeat * afSecsPerCapture;
588 }
589 // If we're dithering after each exposure, that's another 10-20 seconds
590 if (schedJob->getStepPipeline() & SchedulerJob::USE_GUIDE && Options::ditherEnabled())
591 {
592 constexpr int ditherSecs = 15;
593 qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 requires a dither procedure.").arg(seqName);
594 totalImagingTime += (captures_to_go * ditherSecs) / Options::ditherFrames();
595 imagingTimePerRepeat += (capturesRequiredPerRepeat * ditherSecs) / Options::ditherFrames();
596 imagingTimeLeftThisRepeat += (capturesLeftThisRepeat * ditherSecs) / Options::ditherFrames();
597 }
598 }
599 }
600 }
601
602 schedJob->setCapturedFramesMap(capture_map);
603 schedJob->setSequenceCount(allCapturesPerRepeat * schedJob->getRepeatsRequired());
604
605 // only in case we remember the job progress, we change the completion count
606 if (rememberJobProgress)
607 schedJob->setCompletedCount(totalCompletedCount);
608
609 qDeleteAll(seqJobs);
610
611 schedJob->setEstimatedTimePerRepeat(imagingTimePerRepeat);
612 schedJob->setEstimatedTimeLeftThisRepeat(imagingTimeLeftThisRepeat);
613 if (schedJob->getLightFramesRequired())
614 schedJob->setEstimatedStartupTime(timeHeuristics(schedJob));
615
616 // FIXME: Move those ifs away to the caller in order to avoid estimating in those situations!
617
618 // We can't estimate times that do not finish when sequence is done
619 if (schedJob->getCompletionCondition() == FINISH_LOOP)
620 {
621 // We can't know estimated time if it is looping indefinitely
622 schedJob->setEstimatedTime(-2);
623 qCDebug(KSTARS_EKOS_SCHEDULER) <<
624 QString("Job '%1' is configured to loop until Scheduler is stopped manually, has undefined imaging time.")
625 .arg(schedJob->getName());
626 }
627 // If we know startup and finish times, we can estimate time right away
628 else if (schedJob->getStartupCondition() == START_AT &&
629 schedJob->getCompletionCondition() == FINISH_AT)
630 {
631 // FIXME: SchedulerJob is probably doing this already
632 qint64 const diff = schedJob->getStartupTime().secsTo(schedJob->getCompletionTime());
633 schedJob->setEstimatedTime(diff);
634
635 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' has a startup time and fixed completion time, will run for %2.")
636 .arg(schedJob->getName())
637 .arg(dms(diff * 15.0 / 3600.0f).toHMSString());
638 }
639 // If we know finish time only, we can roughly estimate the time considering the job starts now
640 else if (schedJob->getStartupCondition() != START_AT &&
641 schedJob->getCompletionCondition() == FINISH_AT)
642 {
643 qint64 const diff = SchedulerModuleState::getLocalTime().secsTo(schedJob->getCompletionTime());
644 schedJob->setEstimatedTime(diff);
645 qCDebug(KSTARS_EKOS_SCHEDULER) <<
646 QString("Job '%1' has no startup time but fixed completion time, will run for %2 if started now.")
647 .arg(schedJob->getName())
648 .arg(dms(diff * 15.0 / 3600.0f).toHMSString());
649 }
650 // Rely on the estimated imaging time to determine whether this job is complete or not - this makes the estimated time null
651 else if (totalImagingTime <= 0)
652 {
653 schedJob->setEstimatedTime(0);
654 schedJob->setEstimatedTimePerRepeat(1);
655 schedJob->setEstimatedTimeLeftThisRepeat(0);
656
657 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' will not run, complete with %2/%3 captures.")
658 .arg(schedJob->getName()).arg(schedJob->getCompletedCount()).arg(schedJob->getSequenceCount());
659 }
660 // Else consolidate with step durations
661 else
662 {
663 if (schedJob->getLightFramesRequired())
664 {
665 totalImagingTime += timeHeuristics(schedJob);
666 schedJob->setEstimatedStartupTime(timeHeuristics(schedJob));
667 }
668 dms const estimatedTime(totalImagingTime * 15.0 / 3600.0);
669 schedJob->setEstimatedTime(std::ceil(totalImagingTime));
670
671 qCInfo(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' estimated to take %2 to complete.").arg(schedJob->getName(),
672 estimatedTime.toHMSString());
673 }
674
675 return true;
676}
677
678int SchedulerUtils::timeHeuristics(const SchedulerJob *schedJob)
679{
680 double imagingTime = 0;
681 /* FIXME: estimation should base on actual measure of each step, eventually with preliminary data as what it used now */
682 // Are we doing tracking? It takes about 30 seconds
683 if (schedJob->getStepPipeline() & SchedulerJob::USE_TRACK)
684 imagingTime += 30;
685 // Are we doing initial focusing? That can take about 2 minutes
686 if (schedJob->getStepPipeline() & SchedulerJob::USE_FOCUS)
687 imagingTime += 120;
688 // Are we doing astrometry? That can take about 60 seconds
689 if (schedJob->getStepPipeline() & SchedulerJob::USE_ALIGN)
690 {
691 imagingTime += 60;
692 }
693 // Are we doing guiding?
694 if (schedJob->getStepPipeline() & SchedulerJob::USE_GUIDE)
695 {
696 // Looping, finding guide star, settling takes 15 sec
697 imagingTime += 15;
698
699 // Add guiding settle time from dither setting (used by phd2::guide())
700 imagingTime += Options::ditherSettle();
701 // Add guiding settle time from ekos sccheduler setting
702 imagingTime += Options::guidingSettle();
703
704 // If calibration always cleared
705 // then calibration process can take about 2 mins
706 if(Options::resetGuideCalibration())
707 imagingTime += 120;
708 }
709 return imagingTime;
710
711}
712
713uint16_t SchedulerUtils::calculateExpectedCapturesMap(const QList<SequenceJob *> &seqJobs,
714 QMap<QString, uint16_t> &expected)
715{
716 uint16_t capturesPerRepeat = 0;
717 for (auto &seqJob : seqJobs)
718 {
719 capturesPerRepeat += seqJob->getCoreProperty(SequenceJob::SJ_Count).toInt();
720 QString signature = seqJob->getCoreProperty(SequenceJob::SJ_Signature).toString();
721 expected[signature] = static_cast<uint16_t>(seqJob->getCoreProperty(SequenceJob::SJ_Count).toInt()) + (expected.contains(
722 signature) ? expected[signature] : 0);
723 }
724 return capturesPerRepeat;
725}
726
727double SchedulerUtils::findAltitude(const SkyPoint &target, const QDateTime &when, bool *is_setting, bool debug)
728{
729 // FIXME: block calculating target coordinates at a particular time is duplicated in several places
730
731 // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone!
732 KStarsDateTime ltWhen(when.isValid() ?
733 Qt::UTC == when.timeSpec() ? SchedulerModuleState::getGeo()->UTtoLT(KStarsDateTime(when)) : when :
734 SchedulerModuleState::getLocalTime());
735
736 // Create a sky object with the target catalog coordinates
737 SkyObject o;
738 o.setRA0(target.ra0());
739 o.setDec0(target.dec0());
740
741 // Update RA/DEC of the target for the current fraction of the day
742 KSNumbers numbers(ltWhen.djd());
743 o.updateCoordsNow(&numbers);
744
745 // Calculate alt/az coordinates using KStars instance's geolocation
746 CachingDms const LST = SchedulerModuleState::getGeo()->GSTtoLST(SchedulerModuleState::getGeo()->LTtoUT(ltWhen).gst());
747 o.EquatorialToHorizontal(&LST, SchedulerModuleState::getGeo()->lat());
748
749 // Hours are reduced to [0,24[, meridian being at 0
750 double offset = LST.Hours() - o.ra().Hours();
751 if (24.0 <= offset)
752 offset -= 24.0;
753 else if (offset < 0.0)
754 offset += 24.0;
755 bool const passed_meridian = 0.0 <= offset && offset < 12.0;
756
757 if (debug)
758 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("When:%9 LST:%8 RA:%1 RA0:%2 DEC:%3 DEC0:%4 alt:%5 setting:%6 HA:%7")
759 .arg(o.ra().toHMSString())
760 .arg(o.ra0().toHMSString())
761 .arg(o.dec().toHMSString())
762 .arg(o.dec0().toHMSString())
763 .arg(o.alt().Degrees())
764 .arg(passed_meridian ? "yes" : "no")
765 .arg(o.ra().Hours())
766 .arg(LST.toHMSString())
767 .arg(ltWhen.toString("HH:mm:ss"));
768
769 if (is_setting)
770 *is_setting = passed_meridian;
771
772 return o.alt().Degrees();
773}
774
775} // namespace
a dms subclass that caches its sine and cosine values every time the angle is changed.
Definition cachingdms.h:19
There are several time-dependent values used in position calculations, that are not specific to an ob...
Definition ksnumbers.h:43
Extension of QDateTime for KStars KStarsDateTime can represent the date/time as a Julian Day,...
Sequence Job is a container for the details required to capture a series of images.
Provides all necessary information about an object in the sky: its coordinates, name(s),...
Definition skyobject.h:42
The sky coordinates of a point in the sky.
Definition skypoint.h:45
const CachingDms & dec() const
Definition skypoint.h:269
const CachingDms & ra0() const
Definition skypoint.h:251
virtual void updateCoordsNow(const KSNumbers *num)
updateCoordsNow Shortcut for updateCoords( const KSNumbers *num, false, nullptr, nullptr,...
Definition skypoint.h:382
const CachingDms & ra() const
Definition skypoint.h:263
void EquatorialToHorizontal(const CachingDms *LST, const CachingDms *lat)
Determine the (Altitude, Azimuth) coordinates of the SkyPoint from its (RA, Dec) coordinates,...
Definition skypoint.cpp:77
void setRA0(dms r)
Sets RA0, the catalog Right Ascension.
Definition skypoint.h:94
const dms & alt() const
Definition skypoint.h:281
const CachingDms & dec0() const
Definition skypoint.h:257
void setDec0(dms d)
Sets Dec0, the catalog Declination.
Definition skypoint.h:119
An angle, stored as degrees, but expressible in many ways.
Definition dms.h:38
double Hours() const
Definition dms.h:168
virtual void setH(const double &x)
Sets floating-point value of angle, in hours.
Definition dms.h:210
const QString toHMSString(const bool machineReadable=false, const bool highPrecision=false) const
Definition dms.cpp:378
const double & Degrees() const
Definition dms.h:141
QString i18n(const char *text, const TYPE &arg...)
char * toString(const EngineQuery &query)
Ekos is an advanced Astrophotography tool for Linux.
Definition align.cpp:79
QMap< QString, uint16_t > CapturedFramesMap
mapping signature --> frames count
QString name(GameStandardAction id)
QString path(const QString &relativePath)
const QList< QKeySequence > & completion()
QCA_EXPORT Logger * logger()
QDateTime fromString(QStringView string, QStringView format, QCalendar cal)
bool isValid() const const
Qt::TimeSpec timeSpec() const const
bool open(FILE *fh, OpenMode mode, FileHandleFlags handleFlags)
void setFileName(const QString &name)
QString path() const const
bool getChar(char *c)
void append(QList< T > &&value)
qsizetype count() const const
T & first()
QLocale c()
double toDouble(QStringView s, bool *ok) const const
int toInt(QStringView s, bool *ok) const const
bool contains(const Key &key) const const
QString arg(Args &&... args) const const
int compare(QLatin1StringView s1, const QString &s2, Qt::CaseSensitivity cs)
QTextStream & dec(QTextStream &stream)
QUrl fromUserInput(const QString &userInput, const QString &workingDirectory, UserInputResolutionOptions options)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Jul 26 2024 11:59:51 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.