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()
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);
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;
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
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.
242 if (Options::rememberJobProgress())
243 {
244 completedIterations = 0;
245 for (const QString &key : expected.keys())
246 {
247 const int iterationsCompleted = capturedFramesCount[key] / expected[key];
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).
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
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
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));
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.
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()));
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 */
437 bool const rememberJobProgress = Options::rememberJobProgress();
438
439 double totalImagingTime = 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);
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());
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.
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()))
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(
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 {
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.
554 if (seqJob->getFrameType() == FRAME_LIGHT)
555 {
557 {
558 qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 completed its sequence of %2 light frames.").arg(seqName).arg(
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 {
571 const double secsPerCapture = (seqJob->getCoreProperty(SequenceJob::SJ_Exposure).toDouble() +
572 (seqJob->getCoreProperty(SequenceJob::SJ_Delay).toInt() / 1000.0));
574 imagingTimePerRepeat += fabs(secsPerCapture * seqJob->getCoreProperty(SequenceJob::SJ_Count).toInt());
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);
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
607 schedJob->setCompletedCount(totalCompletedCount);
608
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,
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());
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)
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
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 path(const QString &relativePath)
QString name(StandardShortcut id)
const QList< QKeySequence > & completion()
QCA_EXPORT Logger * logger()
QDateTime fromString(QStringView string, QStringView format, QCalendar cal)
QString path() const const
Int toInt() const const
QLocale c()
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 May 3 2024 11:49:51 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.