1 /*
2 * gear_simulator.cpp
3 * PHD Guiding
4 *
5 * Created by Craig Stark.
6 * Reimplemented for PHD2 by Andy Galasso.
7 * Copyright (c) 2006-2010 Craig Stark
8 * Copyright (c) 2015-2018 Andy Galasso
9 * All rights reserved.
10 *
11 * This source code is distributed under the following "BSD" license
12 * Redistribution and use in source and binary forms, with or without
13 * modification, are permitted provided that the following conditions are met:
14 * Redistributions of source code must retain the above copyright notice,
15 * this list of conditions and the following disclaimer.
16 * Redistributions in binary form must reproduce the above copyright notice,
17 * this list of conditions and the following disclaimer in the
18 * documentation and/or other materials provided with the distribution.
19 * Neither the name of openphdguiding.org nor the names of its
20 * contributors may be used to endorse or promote products derived from
21 * this software without specific prior written permission.
22 *
23 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
24 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
26 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
27 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
28 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
29 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
30 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
31 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
32 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
33 * POSSIBILITY OF SUCH DAMAGE.
34 *
35 */
36
37 #include "phd.h"
38
39 #ifdef SIMULATOR
40
41 #include "camera.h"
42 #include "gear_simulator.h"
43 #include "image_math.h"
44
45 #include <wx/dir.h>
46 #include <wx/gdicmn.h>
47 #include <wx/stopwatch.h>
48 #include <wx/radiobut.h>
49
50 #include <wx/wfstream.h>
51 #include <wx/txtstrm.h>
52 #include <wx/tokenzr.h>
53
54 #define SIMMODE 3 // 1=FITS, 2=BMP, 3=Generate
55 // #define SIMDEBUG
56
57 /* simulation parameters for SIMMODE = 3*/
58 // #define SIM_FILE_DISPLACEMENTS // subset of SIMMODE = 3, reading raw star displacements from a file
59
60 struct SimCamParams
61 {
62 static unsigned int width;
63 static unsigned int height;
64 static unsigned int border;
65 static unsigned int nr_stars;
66 static unsigned int nr_hot_pixels;
67 static double noise_multiplier;
68 static double dec_backlash;
69 static double pe_scale;
70 static double dec_drift_rate;
71 static double seeing_scale;
72 static double cam_angle;
73 static double guide_rate;
74 static PierSide pier_side;
75 static bool reverse_dec_pulse_on_west_side;
76 static unsigned int clouds_inten;
77 static double clouds_opacity; // UI has percentage, internally 0-1.0
78 static double image_scale; // arc-sec per pixel
79 static bool use_pe;
80 static bool use_stiction;
81 static bool use_default_pe_params;
82 static double custom_pe_amp;
83 static double custom_pe_period;
84 static bool show_comet;
85 static double comet_rate_x;
86 static double comet_rate_y;
87 static bool allow_async_st4;
88 static unsigned int frame_download_ms;
89 };
90
91 unsigned int SimCamParams::width = 752; // simulated camera image width
92 unsigned int SimCamParams::height = 580; // simulated camera image height
93 unsigned int SimCamParams::border = 12; // do not place any stars within this size border
94 unsigned int SimCamParams::nr_stars; // number of stars to generate
95 unsigned int SimCamParams::nr_hot_pixels; // number of hot pixels to generate
96 double SimCamParams::noise_multiplier; // noise factor, increase to increase noise
97 double SimCamParams::dec_backlash; // dec backlash amount (pixels)
98 double SimCamParams::pe_scale; // scale factor controlling magnitude of simulated periodic error
99 double SimCamParams::dec_drift_rate; // dec drift rate (pixels per second)
100 double SimCamParams::seeing_scale; // simulated seeing scale factor
101 double SimCamParams::cam_angle; // simulated camera angle (degrees)
102 double SimCamParams::guide_rate; // guide rate, pixels per second
103 PierSide SimCamParams::pier_side; // side of pier
104 bool SimCamParams::reverse_dec_pulse_on_west_side; // reverse dec pulse on west side of pier, like ASCOM pulse guided equatorial mounts
105 unsigned int SimCamParams::clouds_inten = 50; // seed brightness for cloud contribution
106 double SimCamParams::clouds_opacity;
107 double SimCamParams::image_scale; // arc-sec per pixel
108 bool SimCamParams::use_pe;
109 bool SimCamParams::use_stiction;
110 bool SimCamParams::use_default_pe_params;
111 double SimCamParams::custom_pe_amp;
112 double SimCamParams::custom_pe_period;
113 bool SimCamParams::show_comet;
114 double SimCamParams::comet_rate_x;
115 double SimCamParams::comet_rate_y;
116 bool SimCamParams::allow_async_st4 = true;
117 unsigned int SimCamParams::frame_download_ms; // frame download time, ms
118
119 // Note: these are all in units appropriate for the UI
120 #define NR_STARS_DEFAULT 20
121 #define NR_HOT_PIXELS_DEFAULT 8
122 #define NOISE_DEFAULT 2.0
123 #define NOISE_MAX 5.0
124 #define DEC_BACKLASH_DEFAULT 5.0 // arc-sec
125 #define DEC_BACKLASH_MAX 100.0
126 #define DEC_DRIFT_DEFAULT 5.0 // arc-sec per minute
127 #define DEC_DRIFT_MAX 30.0
128 #define SEEING_DEFAULT 2.0 // arc-sec FWHM
129 #define SEEING_MAX 5.0
130 #define CAM_ANGLE_DEFAULT 15.0
131 #define CAM_ANGLE_MAX 360.0
132 #define GUIDE_RATE_DEFAULT (1.0 * 15.0) // multiples of sidereal rate, a-s/sec
133 #define GUIDE_RATE_MAX (1.0 * 15.0)
134 #define PIER_SIDE_DEFAULT PIER_SIDE_EAST
135 #define REVERSE_DEC_PULSE_ON_WEST_SIDE_DEFAULT true
136 #define CLOUDS_OPACITY_DEFAULT 0
137 #define USE_PE_DEFAULT true
138 #define USE_STICTION_DEFAULT false
139 #define PE_SCALE_DEFAULT 5.0 // amplitude arc-sec
140 #define PE_SCALE_MAX 30.0
141 #define USE_PE_DEFAULT_PARAMS true
142 #define PE_CUSTOM_AMP_DEFAULT 2.0 // Give them a trivial 2 a-s 4 min smooth curve
143 #define PE_CUSTOM_PERIOD_DEFAULT 240.0
144 #define SHOW_COMET_DEFAULT false
145 #define COMET_RATE_X_DEFAULT 555.0 // pixels per hour
146 #define COMET_RATE_Y_DEFAULT -123.4 // pixels per hour
147 #define SIM_FILE_DISPLACEMENTS_DEFAULT "star_displacements.csv"
148
149 // Needed to handle legacy registry values that may no longer be in correct units or range
range_check(double thisval,double minval,double maxval)150 static double range_check(double thisval, double minval, double maxval)
151 {
152 return wxMin(wxMax(thisval, minval), maxval);
153 }
154
load_sim_params()155 static void load_sim_params()
156 {
157 SimCamParams::image_scale = pFrame->GetCameraPixelScale();
158
159 SimCamParams::nr_stars = pConfig->Profile.GetInt("/SimCam/nr_stars", NR_STARS_DEFAULT);
160 SimCamParams::nr_hot_pixels = pConfig->Profile.GetInt("/SimCam/nr_hot_pixels", NR_HOT_PIXELS_DEFAULT);
161 SimCamParams::noise_multiplier = pConfig->Profile.GetDouble("/SimCam/noise", NOISE_DEFAULT);
162 SimCamParams::use_pe = pConfig->Profile.GetBoolean("/SimCam/use_pe", USE_PE_DEFAULT);
163 SimCamParams::use_stiction = pConfig->Profile.GetBoolean("/SimCam/use_stiction", USE_STICTION_DEFAULT);
164 SimCamParams::use_default_pe_params = pConfig->Profile.GetBoolean("/SimCam/use_default_pe", USE_PE_DEFAULT_PARAMS);
165 SimCamParams::custom_pe_amp = pConfig->Profile.GetDouble("/SimCam/pe_cust_amp", PE_CUSTOM_AMP_DEFAULT);
166 SimCamParams::custom_pe_period = pConfig->Profile.GetDouble("/SimCam/pe_cust_period", PE_CUSTOM_PERIOD_DEFAULT);
167
168 double dval = pConfig->Profile.GetDouble("/SimCam/dec_drift", DEC_DRIFT_DEFAULT);
169 SimCamParams::dec_drift_rate = range_check(dval, -DEC_DRIFT_MAX, DEC_DRIFT_MAX) / (SimCamParams::image_scale * 60.0); //a-s per min is saved
170 // backlash is in arc-secs in UI - map to px for internal use
171 dval = pConfig->Profile.GetDouble("/SimCam/dec_backlash", DEC_BACKLASH_DEFAULT);
172 SimCamParams::dec_backlash = range_check(dval, 0, DEC_BACKLASH_MAX) / SimCamParams::image_scale;
173 SimCamParams::pe_scale = range_check(pConfig->Profile.GetDouble("/SimCam/pe_scale", PE_SCALE_DEFAULT), 0, PE_SCALE_MAX);
174
175 SimCamParams::seeing_scale = range_check(pConfig->Profile.GetDouble("/SimCam/seeing_scale", SEEING_DEFAULT), 0, SEEING_MAX); // FWHM a-s
176 SimCamParams::cam_angle = pConfig->Profile.GetDouble("/SimCam/cam_angle", CAM_ANGLE_DEFAULT);
177 SimCamParams::clouds_opacity = pConfig->Profile.GetDouble("/SimCam/clouds_opacity", CLOUDS_OPACITY_DEFAULT);
178 SimCamParams::guide_rate = range_check(pConfig->Profile.GetDouble("/SimCam/guide_rate", GUIDE_RATE_DEFAULT), 0, GUIDE_RATE_MAX);
179 SimCamParams::pier_side = (PierSide) pConfig->Profile.GetInt("/SimCam/pier_side", PIER_SIDE_DEFAULT);
180 SimCamParams::reverse_dec_pulse_on_west_side = pConfig->Profile.GetBoolean("/SimCam/reverse_dec_pulse_on_west_side", REVERSE_DEC_PULSE_ON_WEST_SIDE_DEFAULT);
181
182 SimCamParams::show_comet = pConfig->Profile.GetBoolean("/SimCam/show_comet", SHOW_COMET_DEFAULT);
183 SimCamParams::comet_rate_x = pConfig->Profile.GetDouble("/SimCam/comet_rate_x", COMET_RATE_X_DEFAULT);
184 SimCamParams::comet_rate_y = pConfig->Profile.GetDouble("/SimCam/comet_rate_y", COMET_RATE_Y_DEFAULT);
185
186 SimCamParams::frame_download_ms = pConfig->Profile.GetInt("/SimCam/frame_download_ms", 50);
187 }
188
save_sim_params()189 static void save_sim_params()
190 {
191 pConfig->Profile.SetInt("/SimCam/nr_stars", SimCamParams::nr_stars);
192 pConfig->Profile.SetInt("/SimCam/nr_hot_pixels", SimCamParams::nr_hot_pixels);
193 pConfig->Profile.SetDouble("/SimCam/noise", SimCamParams::noise_multiplier);
194 pConfig->Profile.SetDouble("/SimCam/dec_backlash", SimCamParams::dec_backlash * SimCamParams::image_scale);
195 pConfig->Profile.SetBoolean("/SimCam/use_pe", SimCamParams::use_pe);
196 pConfig->Profile.SetBoolean("/SimCam/use_stiction", SimCamParams::use_stiction);
197 pConfig->Profile.SetBoolean("/SimCam/use_default_pe", SimCamParams::use_default_pe_params);
198 pConfig->Profile.SetDouble("/SimCam/pe_scale", SimCamParams::pe_scale);
199 pConfig->Profile.SetDouble("/SimCam/pe_cust_amp", SimCamParams::custom_pe_amp);
200 pConfig->Profile.SetDouble("/SimCam/pe_cust_period", SimCamParams::custom_pe_period);
201 pConfig->Profile.SetDouble("/SimCam/dec_drift", SimCamParams::dec_drift_rate * 60.0 * SimCamParams::image_scale);
202 pConfig->Profile.SetDouble("/SimCam/seeing_scale", SimCamParams::seeing_scale);
203 pConfig->Profile.SetDouble("/SimCam/clouds_opacity", SimCamParams::clouds_opacity);
204 pConfig->Profile.SetDouble("/SimCam/cam_angle", SimCamParams::cam_angle);
205 pConfig->Profile.SetDouble("/SimCam/guide_rate", SimCamParams::guide_rate);
206 pConfig->Profile.SetInt("/SimCam/pier_side", (int) SimCamParams::pier_side);
207 pConfig->Profile.SetBoolean("/SimCam/reverse_dec_pulse_on_west_side", SimCamParams::reverse_dec_pulse_on_west_side);
208 pConfig->Profile.SetBoolean("/SimCam/show_comet", SimCamParams::show_comet);
209 pConfig->Profile.SetDouble("/SimCam/comet_rate_x", SimCamParams::comet_rate_x);
210 pConfig->Profile.SetDouble("/SimCam/comet_rate_y", SimCamParams::comet_rate_y);
211 pConfig->Profile.SetInt("/SimCam/frame_download_ms", SimCamParams::frame_download_ms);
212 }
213
214 #ifdef STEPGUIDER_SIMULATOR
215
216 struct SimAoParams
217 {
218 static unsigned int max_position; // max position in steps
219 static double scale; // arcsec per step
220 static double angle; // angle relative to camera (degrees)
221 };
222
223 unsigned int SimAoParams::max_position = 45;
224 double SimAoParams::scale = 0.10;
225 double SimAoParams::angle = 35.0;
226
227 class StepGuiderSimulator : public StepGuider
228 {
229 public:
230 StepGuiderSimulator();
231 virtual ~StepGuiderSimulator();
232
233 bool Connect() override;
234 bool Disconnect() override;
235
236 private:
237 bool Center() override;
238 STEP_RESULT Step(GUIDE_DIRECTION direction, int steps) override;
239 int MaxPosition(GUIDE_DIRECTION direction) const override;
240 bool SetMaxPosition(int steps) override;
241 bool HasNonGuiMove() override;
242 };
243
244 static StepGuiderSimulator *s_sim_ao;
245
StepGuiderSimulator()246 StepGuiderSimulator::StepGuiderSimulator()
247 {
248 m_Name = _("AO-Simulator");
249 SimAoParams::max_position = pConfig->Profile.GetInt("/SimAo/max_steps", 45);
250 }
251
~StepGuiderSimulator()252 StepGuiderSimulator::~StepGuiderSimulator()
253 {
254 }
255
Connect()256 bool StepGuiderSimulator::Connect()
257 {
258 if (StepGuider::Connect())
259 return true;
260
261 ZeroCurrentPosition();
262
263 s_sim_ao = this;
264
265 Debug.AddLine("AO Simulator Connected");
266
267 if (!pCamera || pCamera->Name != _T("Simulator"))
268 {
269 pFrame->Alert(_("The AO Simulator only works with the Camera Simulator. You should either disconnect the AO Simulator or connect the Camera Simulator."));
270 }
271
272 return false;
273 }
274
Disconnect()275 bool StepGuiderSimulator::Disconnect()
276 {
277 if (StepGuider::Disconnect())
278 return true;
279
280 if (s_sim_ao == this) {
281 Debug.AddLine("AO Simulator Disconnected");
282 s_sim_ao = 0;
283 }
284
285 return false;
286 }
287
Center()288 bool StepGuiderSimulator::Center()
289 {
290 ZeroCurrentPosition();
291 return false;
292 }
293
Step(GUIDE_DIRECTION direction,int steps)294 StepGuider::STEP_RESULT StepGuiderSimulator::Step(GUIDE_DIRECTION direction, int steps)
295 {
296 #if 0 // enable to test step failure
297 wxPoint pos = GetAoPos();
298 if (direction == LEFT && pos.x - steps < -35)
299 {
300 Debug.Write("simulate step failure\n");
301 return STEP_LIMIT_REACHED;
302 }
303 #endif
304
305 // parent class maintains x/y offsets, so nothing to do here. Just simulate a delay.
306 enum { LATENCY_MS_PER_STEP = 5 };
307 wxMilliSleep(steps * LATENCY_MS_PER_STEP);
308 return STEP_OK;
309 }
310
MaxPosition(GUIDE_DIRECTION direction) const311 int StepGuiderSimulator::MaxPosition(GUIDE_DIRECTION direction) const
312 {
313 return SimAoParams::max_position;
314 }
315
SetMaxPosition(int steps)316 bool StepGuiderSimulator::SetMaxPosition(int steps)
317 {
318 SimAoParams::max_position = (unsigned int) steps;
319 pConfig->Profile.SetInt("/SimAo/max_steps", steps);
320 return false;
321 }
322
HasNonGuiMove()323 bool StepGuiderSimulator::HasNonGuiMove()
324 {
325 return true;
326 }
327
328 #endif // STEPGUIDER_SIMULATOR
329
330 #ifdef ROTATOR_SIMULATOR
331
332 class RotatorSimulator : public Rotator
333 {
334 public:
335 RotatorSimulator();
336 virtual ~RotatorSimulator();
337
338 bool Connect() override;
339 bool Disconnect() override;
340
341 // get the display name of the rotator device
342 wxString Name() const override;
343
344 // get the rotator position in degrees
345 float Position() const override;
346 };
347
RotatorSimulator()348 RotatorSimulator::RotatorSimulator()
349 {
350 }
351
~RotatorSimulator()352 RotatorSimulator::~RotatorSimulator()
353 {
354 }
355
Connect()356 bool RotatorSimulator::Connect()
357 {
358 if (!pCamera || pCamera->Name != _T("Simulator"))
359 {
360 pFrame->Alert(_("The Rotator Simulator only works with the Camera Simulator. You must either disconnect the Rotator Simulator or connect the Camera Simulator."));
361 return true;
362 }
363
364 Rotator::Connect();
365 return false;
366 }
367
Disconnect()368 bool RotatorSimulator::Disconnect()
369 {
370 Rotator::Disconnect();
371 return false;
372 }
373
Name() const374 wxString RotatorSimulator::Name() const
375 {
376 return _T("Simulator");
377 }
378
Position() const379 float RotatorSimulator::Position() const
380 {
381 assert(IsConnected());
382 return SimCamParams::cam_angle;
383 }
384
385 #endif // ROTATOR_SIMULATOR
386
387 // value with backlash
388 // There is an index value, and a lower and upper limit separated by the
389 // backlash amount. When the index moves past the upper limit, it carries
390 // both limits along, likewise for the lower limit. The current value is
391 // the value of the upper limit.
392 struct BacklashVal
393 {
394 double cur; // current index value
395 double upper; // upper limit
396 double amount; // backlash amount (lower limit is upper - amount)
397
BacklashValBacklashVal398 BacklashVal() { }
399
BacklashValBacklashVal400 BacklashVal(double backlash_amount)
401 : cur(0), upper(backlash_amount), amount(backlash_amount) { }
402
valBacklashVal403 double val() const { return upper; }
404
incrBacklashVal405 void incr(double d) {
406 cur += d;
407 if (d > 0.) {
408 if (cur > upper)
409 upper = cur;
410 }
411 else if (d < 0.) {
412 if (cur < upper - amount)
413 upper = cur + amount;
414 }
415 }
416 };
417
418 struct SimStar
419 {
420 wxRealPoint pos;
421 double inten;
422 };
423
424 struct StictionSim
425 {
426 int lastDirection;
427 bool pending;
428 double adjustment;
429
StictionSimStictionSim430 StictionSim()
431 {
432 lastDirection = NONE;
433 pending = false;
434 adjustment = 0.;
435 }
436
GetAdjustmentStictionSim437 double GetAdjustment(int Direction, int Duration, double Distance)
438 {
439 double rslt = 0;
440 if (lastDirection != NONE)
441 {
442 if (Duration > 300 && Direction != lastDirection && !pending)
443 {
444 adjustment = Distance / 3.0;
445 pending = true;
446 Debug.Write(wxString::Format("Stiction: reduced distance by %0.2f\n", adjustment));
447 rslt = -adjustment;
448 }
449 else
450 {
451 if (pending)
452 {
453 if (Direction == lastDirection)
454 {
455 rslt = adjustment;
456 Debug.Write(wxString::Format("Stiction: increased distance by %0.2f\n", adjustment));
457 adjustment = 0;
458 }
459 pending = false;
460 }
461 }
462 }
463 lastDirection = Direction;
464 return rslt;
465 }
466 };
467
468 static const double AMBIENT_TEMP = 15.;
469 static const double MIN_COOLER_TEMP = -15.;
470
471 struct Cooler
472 {
473 bool on;
474 double startTemp;
475 double endTemp;
476 double setTemp;
477 time_t endTime;
478 double rate; // degrees per second
479 double direction; // -1 = cooling, +1 = warming
480
CoolerCooler481 Cooler() : on(false), startTemp(AMBIENT_TEMP), endTemp(AMBIENT_TEMP), setTemp(AMBIENT_TEMP), endTime(0), rate(1. / 8.), direction(0.) { }
482
CurrentTempCooler483 double CurrentTemp() const
484 {
485 time_t now = wxDateTime::GetTimeNow();
486
487 if (now >= endTime)
488 return endTemp;
489
490 return endTemp - rate * direction * (double)(endTime - now);
491 }
492
TurnOnCooler493 void TurnOn()
494 {
495 if (!on)
496 {
497 _SetTemp(CurrentTemp());
498 on = true;
499 }
500 }
501
TurnOffCooler502 void TurnOff()
503 {
504 if (on)
505 {
506 _SetTemp(AMBIENT_TEMP);
507 on = false;
508 }
509 }
510
_SetTempCooler511 void _SetTemp(double newtemp)
512 {
513 startTemp = CurrentTemp();
514 endTemp = std::max(std::min(newtemp, AMBIENT_TEMP), MIN_COOLER_TEMP);
515 double dt = ceil(fabs(endTemp - startTemp) / rate);
516 endTime = wxDateTime::GetTimeNow() + (time_t) dt;
517 direction = endTemp < startTemp ? -1. : +1.;
518 }
519
SetTempCooler520 void SetTemp(double newtemp)
521 {
522 assert(on);
523 _SetTemp(newtemp);
524 setTemp = newtemp;
525 }
526 };
527
528 struct SimCamState
529 {
530 unsigned int width;
531 unsigned int height;
532 wxVector<SimStar> stars; // star positions and intensities (ra, dec)
533 wxVector<wxPoint> hotpx; // hot pixels
534 double ra_ofs; // assume no backlash in RA
535 BacklashVal dec_ofs; // simulate backlash in DEC
536 double cum_dec_drift; // cumulative dec drift
537 wxStopWatch timer; // platform-independent timer
538 long last_exposure_time; // last expoure time, milliseconds
539 Cooler cooler; // simulated cooler
540 StictionSim stictionSim;
541
542 #ifdef SIMDEBUG
543 wxFFile DebugFile;
544 double last_ra_move;
545 double last_dec_move;
546 #endif
547
548 #ifdef SIM_FILE_DISPLACEMENTS
549 wxFileInputStream* pIStream;
550 wxTextInputStream* pText;
551 double scaleConversion;
552 void ReadDisplacements(double& cumX, double& cumY);
553 #endif
554
555 #if SIMMODE == 1
556 wxDir dir;
557 bool dirStarted;
558 bool ReadNextImage(usImage& img, const wxRect& subframe);
559 #endif
560
561 void Initialize();
562 void FillImage(usImage& img, const wxRect& subframe, int exptime, int gain, int offset);
563 };
564
Initialize()565 void SimCamState::Initialize()
566 {
567 width = SimCamParams::width;
568 height = SimCamParams::height;
569 // generate stars at random positions but no closer than 12 pixels from any edge
570 unsigned int const nr_stars = SimCamParams::nr_stars;
571 stars.resize(nr_stars);
572 unsigned int const border = SimCamParams::border;
573
574 srand(2); // always generate the same stars
575 for (unsigned int i = 0; i < nr_stars; i++)
576 {
577 // generate stars in ra/dec coordinates
578 stars[i].pos.x = (double)(rand() % (width - 2 * border)) - 0.5 * width;
579 stars[i].pos.y = (double)(rand() % (height - 2 * border)) - 0.5 * height;
580 double r = (double) (rand() % 90) / 3.0; // 0..30
581 if (i == 10)
582 stars[i].inten = 30.1; // Always have one saturated star
583 else
584 stars[i].inten = 0.1 + (double) (r * r * r) / 9000.0;
585
586 // force a couple stars to be close together. This is a useful test for Star::AutoFind
587 if (i == 3)
588 {
589 stars[i].pos.x = stars[i - 1].pos.x + 8;
590 stars[i].pos.y = stars[i - 1].pos.y + 8;
591 stars[i].inten = stars[i - 1].inten;
592 }
593 }
594
595 // generate hot pixels
596 unsigned int const nr_hot = SimCamParams::nr_hot_pixels;
597 hotpx.resize(nr_hot);
598 for (unsigned int i = 0; i < nr_hot; i++) {
599 hotpx[i].x = rand() % width;
600 hotpx[i].y = rand() % height;
601 }
602 srand(clock());
603 ra_ofs = 0.;
604 dec_ofs = BacklashVal(SimCamParams::dec_backlash);
605 cum_dec_drift = 0.;
606 last_exposure_time = 0;
607
608 #if SIMMODE == 1
609 dirStarted = false;
610 #endif
611
612 #ifdef SIM_FILE_DISPLACEMENTS
613 pIStream = nullptr;
614 wxString csvName = Debug.GetLogDir() + PATHSEPSTR + SIM_FILE_DISPLACEMENTS_DEFAULT;
615 if (wxFile::Exists(csvName))
616 pIStream = new wxFileInputStream(csvName);
617 else
618 {
619 wxFileDialog dlg(pFrame, _("Choose a star displacements file"), wxEmptyString, wxEmptyString,
620 wxT("Comma-separated files (*.csv)|*.csv"), wxFD_OPEN | wxFD_FILE_MUST_EXIST);
621 dlg.SetDirectory(Debug.GetLogDir());
622 if (dlg.ShowModal() == wxID_OK)
623 {
624 pIStream = new wxFileInputStream(dlg.GetPath());
625 if (!pIStream->IsOk())
626 {
627 wxMessageBox(_("Can't use this file for star displacements"));
628 }
629 }
630 else
631 wxMessageBox(_("Can't simulate any star movement without a displacement file"));
632 }
633 if (pIStream && pIStream->IsOk())
634 pText = new wxTextInputStream(*pIStream);
635 else
636 pText = nullptr; // User cancelled open dialog or file is useless
637 scaleConversion = 1.0; // safe default
638 #endif
639
640 #ifdef SIMDEBUG
641 DebugFile.Open ("Sim_Debug.txt", "w");
642 #ifdef SIM_FILE_DISPLACEMENTS
643 DebugFile.Write ("Total_X, Total_Y, RA_Ofs, Dec_Ofs \n");
644 #else
645 DebugFile.Write ("PE, Drift, RA_Seeing, Dec_Seeing, Total_X, Total_Y, RA_Ofs, Dec_Ofs, \n");
646 #endif
647 #endif
648 }
649
650 #if SIMMODE == 1
ReadNextImage(usImage & img,const wxRect & subframe)651 bool SimCamState::ReadNextImage(usImage& img, const wxRect& subframe)
652 {
653 wxString filename;
654
655 if (!dir.IsOpened())
656 {
657 dir.Open(wxFileName(Debug.GetLogDir(), "sim_images").GetFullPath());
658 }
659 if (dir.IsOpened())
660 {
661 if (!dirStarted)
662 {
663 dir.GetFirst(&filename, "*.fit", wxDIR_FILES);
664 dirStarted = true;
665 }
666 else
667 {
668 if (!dir.GetNext(&filename))
669 dir.GetFirst(&filename, "*.fit", wxDIR_FILES);
670 }
671 }
672 if (filename.IsEmpty())
673 {
674 return true;
675 }
676
677 Debug.Write("Sim file opened: " + filename + "\n");
678 fitsfile *fptr; // FITS file pointer
679 int status = 0; // CFITSIO status value MUST be initialized to zero!
680
681 if (PHD_fits_open_diskfile(&fptr, wxFileName(dir.GetName(), filename).GetFullPath(), READONLY, &status))
682 return true;
683
684 int hdutype;
685 if (fits_get_hdu_type(fptr, &hdutype, &status) || hdutype != IMAGE_HDU)
686 {
687 pFrame->Alert(_("FITS file is not of an image"));
688 PHD_fits_close_file(fptr);
689 return true;
690 }
691
692 int naxis;
693 fits_get_img_dim(fptr, &naxis, &status);
694
695 int nhdus;
696 fits_get_num_hdus(fptr, &nhdus, &status);
697 if ((nhdus != 1) || (naxis != 2)) {
698 pFrame->Alert(_("Unsupported type or read error loading FITS file"));
699 PHD_fits_close_file(fptr);
700 return true;
701 }
702
703 long fits_size[2];
704 fits_get_img_size(fptr, 2, fits_size, &status);
705
706 int xsize = (int) fits_size[0];
707 int ysize = (int) fits_size[1];
708
709 if (img.Init(xsize, ysize)) {
710 pFrame->Alert(_("Memory allocation error"));
711 PHD_fits_close_file(fptr);
712 return true;
713 }
714
715 unsigned short *buf = new unsigned short[img.NPixels];
716 bool useSubframe = !subframe.IsEmpty();
717 wxRect frame;
718 if (useSubframe)
719 frame = subframe;
720 else
721 frame = wxRect(0, 0, xsize, ysize);
722
723 long inc[] = { 1, 1 };
724 long fpixel[] = { frame.GetLeft() + 1, frame.GetTop() + 1 };
725 long lpixel[] = { frame.GetRight() + 1, frame.GetBottom() + 1 };
726 if (fits_read_subset(fptr, TUSHORT, fpixel, lpixel, inc, nullptr, buf, nullptr, &status))
727 {
728 pFrame->Alert(_("Error reading data"));
729 PHD_fits_close_file(fptr);
730 return true;
731 }
732
733 if (useSubframe)
734 {
735 img.Subframe = subframe;
736
737 // Clear out the image
738 img.Clear();
739
740 int i = 0;
741 for (int y = 0; y < subframe.height; y++)
742 {
743 unsigned short *dst = img.ImageData + (y + subframe.y) * xsize + subframe.x;
744 for (int x = 0; x < subframe.width; x++, i++)
745 *dst++ = (unsigned short) buf[i];
746 }
747 }
748 else
749 {
750 for (unsigned int i = 0; i < img.NPixels; i++)
751 img.ImageData[i] = (unsigned short) buf[i];
752 }
753
754 delete[] buf;
755
756 PHD_fits_close_file(fptr);
757
758 return false;
759 }
760 #endif // SIMMODE == 1
761
762 // get a pair of normally-distributed independent random values - Box-Muller algorithm, sigma=1
rand_normal(double r[2])763 static void rand_normal(double r[2])
764 {
765 double u = (double) rand() / RAND_MAX;
766 double v = (double) rand() / RAND_MAX;
767 double const a = sqrt(-2.0 * log(u));
768 double const p = 2 * M_PI * v;
769 r[0] = a * cos(p);
770 r[1] = a * sin(p);
771 }
772
pixel_addr(usImage & img,int x,int y)773 inline static unsigned short *pixel_addr(usImage& img, int x, int y)
774 {
775 if (x < 0 || x >= img.Size.x)
776 return 0;
777 if (y < 0 || y >= img.Size.y)
778 return 0;
779 return &img.Pixel(x, y);
780 }
781
set_pixel(usImage & img,int x,int y,unsigned short val)782 inline static void set_pixel(usImage& img, int x, int y, unsigned short val)
783 {
784 unsigned short *const addr = pixel_addr(img, x, y);
785 if (addr)
786 *addr = val;
787 }
788
incr_pixel(usImage & img,int x,int y,unsigned int val)789 inline static void incr_pixel(usImage& img, int x, int y, unsigned int val)
790 {
791 unsigned short *const addr = pixel_addr(img, x, y);
792 if (addr) {
793 unsigned int t = *addr;
794 t += val;
795 if (t > (unsigned int)(unsigned short)-1)
796 *addr = (unsigned short)-1;
797 else
798 *addr = (unsigned short)t;
799 }
800 }
801
render_comet(usImage & img,int binning,const wxRect & subframe,const wxRealPoint & p,double inten)802 static void render_comet(usImage& img, int binning, const wxRect& subframe, const wxRealPoint& p, double inten)
803 {
804 enum { WIDTH = 5 };
805 double STAR[][WIDTH] = { { 0.0, 0.8, 2.2, 0.8, 0.0, },
806 { 0.8, 16.6, 46.1, 16.6, 0.8, },
807 { 2.2, 46.1, 128.0, 46.1, 2.2, },
808 { 0.8, 16.6, 46.1, 16.6, 0.8, },
809 { 0.0, 0.8, 2.2, 0.8, 0.0, },
810 };
811
812 wxRealPoint intpart;
813 double fx = modf(p.x / (double) binning, &intpart.x);
814 double fy = modf(p.y / (double) binning, &intpart.y);
815 double f00 = (1.0 - fx) * (1.0 - fy);
816 double f01 = (1.0 - fx) * fy;
817 double f10 = fx * (1.0 - fy);
818 double f11 = fx * fy;
819
820 double d[WIDTH + 1][WIDTH + 1] = { { 0.0 } };
821 for (unsigned int i = 0; i < WIDTH; i++)
822 for (unsigned int j = 0; j < WIDTH; j++)
823 {
824 double s = STAR[i][j];
825 if (s > 0.0)
826 {
827 s *= inten / 256.0;
828 d[i][j] += f00 * s;
829 d[i + 1][j] += f10 * s;
830 d[i][j + 1] += f01 * s;
831 d[i + 1][j + 1] += f11 * s;
832 }
833 }
834
835 wxPoint c((int)intpart.x - (WIDTH - 1) / 2,
836 (int)intpart.y - (WIDTH - 1) / 2);
837
838 for (unsigned int x_inc = 0; x_inc < 10; x_inc++)
839 {
840 for (double y = -1; y < 1.5; y += 0.5)
841 {
842 int const cx = c.x + x_inc;
843 int const cy = c.y + y * x_inc;
844 if (cx < subframe.GetRight() && cy < subframe.GetBottom() && cy > subframe.GetTop())
845 incr_pixel(img, cx, cy, (int)d[2][2]);
846 }
847
848 }
849
850 }
851
render_star(usImage & img,int binning,const wxRect & subframe,const wxRealPoint & p,double inten)852 static void render_star(usImage& img, int binning, const wxRect& subframe, const wxRealPoint& p, double inten)
853 {
854 enum { WIDTH = 5 };
855 double STAR[][WIDTH] = {{ 0.0, 0.8, 2.2, 0.8, 0.0, },
856 { 0.8, 16.6, 46.1, 16.6, 0.8, },
857 { 2.2, 46.1, 128.0, 46.1, 2.2, },
858 { 0.8, 16.6, 46.1, 16.6, 0.8, },
859 { 0.0, 0.8, 2.2, 0.8, 0.0, },
860 };
861
862 wxRealPoint intpart;
863 double fx = modf(p.x / (double) binning, &intpart.x);
864 double fy = modf(p.y / (double) binning, &intpart.y);
865 double f00 = (1.0 - fx) * (1.0 - fy);
866 double f01 = (1.0 - fx) * fy;
867 double f10 = fx * (1.0 - fy);
868 double f11 = fx * fy;
869
870 double d[WIDTH + 1][WIDTH + 1] = { { 0.0 } };
871 for (unsigned int i = 0; i < WIDTH; i++)
872 for (unsigned int j = 0; j < WIDTH; j++)
873 {
874 double s = STAR[i][j];
875 if (s > 0.0)
876 {
877 s *= inten / 256.0;
878 d[i][j] += f00 * s;
879 d[i+1][j] += f10 * s;
880 d[i][j+1] += f01 * s;
881 d[i+1][j+1] += f11 * s;
882 }
883 }
884
885 wxPoint c((int) intpart.x - (WIDTH - 1) / 2,
886 (int) intpart.y - (WIDTH - 1) / 2);
887
888 for (unsigned int i = 0; i < WIDTH + 1; i++)
889 {
890 int const cx = c.x + i;
891 if (cx < subframe.GetLeft() || cx > subframe.GetRight())
892 continue;
893 for (unsigned int j = 0; j < WIDTH + 1; j++)
894 {
895 int const cy = c.y + j;
896 if (cy < subframe.GetTop() || cy > subframe.GetBottom())
897 continue;
898 int incr = (int) d[i][j];
899 if (incr > (unsigned short)-1)
900 incr = (unsigned short)-1;
901 incr_pixel(img, cx, cy, incr);
902 }
903 }
904 }
905
render_clouds(usImage & img,const wxRect & subframe,int exptime,int gain,int offset)906 static void render_clouds(usImage& img, const wxRect& subframe, int exptime, int gain, int offset)
907 {
908 unsigned short cloud_amt;
909 unsigned short *p0 = &img.Pixel(subframe.GetLeft(), subframe.GetTop());
910 for (int r = 0; r < subframe.GetHeight(); r++, p0 += img.Size.GetWidth())
911 {
912 unsigned short *const end = p0 + subframe.GetWidth();
913 for (unsigned short *p = p0; p < end; p++)
914 {
915 // Compute a randomized brightness contribution from clouds, then overlay that on the guide frame
916 cloud_amt = (unsigned short)(SimCamParams::clouds_inten * ((double)gain / 10.0 * offset * exptime / 100.0 + ((rand() % (gain * 100)) / 30.0)));
917 *p = (unsigned short) (SimCamParams::clouds_opacity * cloud_amt + (1 - SimCamParams::clouds_opacity) * *p);
918
919 }
920 }
921 }
922
923 #ifdef SIM_FILE_DISPLACEMENTS
924 // Get raw star displacements from a file generated by using the CAPTURE_DEFLECTIONS
925 // compile-time option in guider.cpp to record them
ReadDisplacements(double & incX,double & incY)926 void SimCamState::ReadDisplacements(double& incX, double& incY)
927 {
928 wxStringTokenizer tok;
929
930 // If we reach the EOF, just start over - we don't want to suddenly reverse direction on linear drifts, and the
931 // underlying seeing behavior is sufficiently random that a simple replay is warranted
932 if (pIStream->Eof())
933 pIStream->SeekI(wxFileOffset(0));
934
935 if (!pIStream->Eof())
936 {
937 wxString line = pText->ReadLine();
938 line.Trim(false); // trim leading whitespace
939
940 if (line.StartsWith("DeltaRA"))
941 {
942 // Get the image scale of the underlying raw data stream
943 tok.SetString(line, ", =");
944 wxString tk = tok.GetNextToken();
945 while (tk != "Scale")
946 tk = tok.GetNextToken();
947 tk = tok.GetNextToken(); // numeric image scale a-s/p
948 double realImageScale;
949 if (tk.ToDouble(&realImageScale))
950 {
951 // Will use this to scale subsequent raw star displacements to match simulator image scale
952 scaleConversion = realImageScale / SimCamParams::image_scale;
953 }
954 line = pText->ReadLine();
955 line.Trim(false);
956 }
957
958 tok.SetString(line, ", ");
959 wxString s1 = tok.GetNextToken();
960 wxString s2 = tok.GetNextToken();
961 double x, y;
962 if (s1.ToDouble(&x) && s2.ToDouble(&y))
963 {
964 incX = x * scaleConversion;
965 incY = y * scaleConversion;
966 }
967 else
968 {
969 Debug.AddLine(wxString::Format("Star_deflections file: bad input starting with %s", line));
970 }
971 }
972 }
973 #endif
974
FillImage(usImage & img,const wxRect & subframe,int exptime,int gain,int offset)975 void SimCamState::FillImage(usImage& img, const wxRect& subframe, int exptime, int gain, int offset)
976 {
977 unsigned int const nr_stars = stars.size();
978
979 #ifdef SIMDEBUG
980 static int CountUp (0);
981 if (CountUp == 0)
982 {
983 // Changes in the setup dialog are hard to track - just make sure we are using the params we think we are
984 Debug.AddLine (wxString::Format("SimDebug: img_scale: %.3f, seeing_scale: %.3f", SimCamParams::image_scale, SimCamParams::seeing_scale));
985 }
986 CountUp++;
987 #endif
988
989 // start with original star positions
990 wxVector<wxRealPoint> pos(nr_stars);
991 for (unsigned int i = 0; i < nr_stars; i++)
992 pos[i] = stars[i].pos;
993
994 double total_shift_x = 0;
995 double total_shift_y = 0;
996
997 #ifdef SIM_FILE_DISPLACEMENTS
998
999 double inc_x;
1000 double inc_y;
1001 if (pText)
1002 {
1003 ReadDisplacements(inc_x, inc_y);
1004 total_shift_x = ra_ofs + inc_x;
1005 total_shift_y = dec_ofs.val() + inc_y;
1006 // If user has disabled guiding, let him see the raw behavior of the displacement data - the
1007 // ra_ofs and dec_ofs variables are normally updated in the ST-4 guide function
1008 if (!pMount->GetGuidingEnabled())
1009 {
1010 ra_ofs += inc_x;
1011 dec_ofs.incr(inc_y);
1012 }
1013 }
1014
1015 #else // SIM_FILE_DISPLACEMENTS
1016
1017 long const cur_time = timer.Time();
1018 long const delta_time_ms = last_exposure_time - cur_time;
1019 last_exposure_time = cur_time;
1020
1021 // simulate worm phase changing with RA slew
1022 double dec, st, ra = 0.;
1023 if (pPointingSource)
1024 pPointingSource->GetCoordinates(&ra, &dec, &st);
1025
1026 static double s_prev_ra;
1027 double dra = norm(ra - s_prev_ra, -12.0, 12.0);
1028 s_prev_ra = ra;
1029
1030 // convert RA hours to SI seconds
1031 const double SECONDS_PER_HOUR = 60. * 60.;
1032 const double SIDEREAL_SECONDS_PER_SEC = 0.9973;
1033 dra *= SECONDS_PER_HOUR / SIDEREAL_SECONDS_PER_SEC;
1034 static double s_ra_offset;
1035 s_ra_offset += dra;
1036
1037 // an increase in RA means the worm moved backwards
1038 double const now = cur_time / 1000. - s_ra_offset;
1039
1040 // Compute PE - canned PE terms create some "steep" sections of the curve
1041 static double const max_amp = 4.85; // max amplitude of canned PE
1042 double pe = 0.;
1043
1044 if (SimCamParams::use_pe)
1045 {
1046 if (SimCamParams::use_default_pe_params)
1047 {
1048 static double const period[] = { 230.5, 122.0, 49.4, 9.56, 76.84, };
1049 static double const amp[] = {2.02, 0.69, 0.22, 0.137, 0.14}; // in a-s
1050 static double const phase[] = { 0.0, 1.4, 98.8, 35.9, 150.4, };
1051
1052 for (unsigned int i = 0; i < WXSIZEOF(period); i++)
1053 pe += amp[i] * cos((now - phase[i]) / period[i] * 2. * M_PI);
1054
1055 pe *= (SimCamParams::pe_scale / (max_amp * SimCamParams::image_scale)); // modulated PE in px
1056 }
1057 else
1058 {
1059 pe = SimCamParams::custom_pe_amp * cos(now / SimCamParams::custom_pe_period * 2.0 * M_PI) / SimCamParams::image_scale;
1060 }
1061 }
1062
1063 // simulate drift in DEC
1064 cum_dec_drift += (double) delta_time_ms * SimCamParams::dec_drift_rate / 1000.;
1065
1066 // Compute total movements from all sources - ra_ofs and dec_ofs are cumulative sums of all guider movements relative to zero-point
1067 total_shift_x = pe + ra_ofs;
1068 total_shift_y = cum_dec_drift + dec_ofs.val();
1069
1070 double seeing[2] = { 0.0 };
1071
1072 // simulate seeing
1073 if (SimCamParams::seeing_scale > 0.0)
1074 {
1075 rand_normal(seeing);
1076 static const double seeing_adjustment = (2.345 * 1.4 * 2.4); //FWHM, geometry, empirical
1077 double sigma = SimCamParams::seeing_scale / (seeing_adjustment * SimCamParams::image_scale);
1078 seeing[0] *= sigma;
1079 seeing[1] *= sigma;
1080 total_shift_x += seeing[0];
1081 total_shift_y += seeing[1];
1082 }
1083
1084 #endif // SIM_FILE_DISPLACEMENTS
1085
1086 for (unsigned int i = 0; i < nr_stars; i++)
1087 {
1088 pos[i].x += total_shift_x;
1089 pos[i].y += total_shift_y;
1090 }
1091
1092 #ifdef SIMDEBUG
1093 #ifdef SIM_FILE_DISPLACEMENTS
1094 DebugFile.Write(wxString::Format("%.3f, %.3f, %.3f, %.3f\n", total_shift_x, total_shift_y,
1095 ra_ofs, dec_ofs.val()));
1096 #else
1097 DebugFile.Write(wxString::Format( "%.3f, %.3f, %.3f, %.3f, %.3f, %.3f, %.3f, %.3f\n",
1098 pe, drift, seeing[0], seeing[1], total_shift_x, total_shift_y,
1099 ra_ofs, dec_ofs.val()));
1100 #endif
1101 #endif
1102
1103 // check for pier-flip
1104 if (pPointingSource)
1105 {
1106 PierSide new_side = pPointingSource->SideOfPier();
1107 if (new_side != SimCamParams::pier_side)
1108 {
1109 Debug.Write(wxString::Format("Cam simulator: pointing source pier side changed from %d to %d\n", SimCamParams::pier_side, new_side));
1110 SimCamParams::pier_side = new_side;
1111 }
1112 }
1113
1114 // convert to camera coordinates
1115 wxVector<wxRealPoint> cc(nr_stars);
1116 double angle = radians(SimCamParams::cam_angle);
1117
1118 if (SimCamParams::pier_side == PIER_SIDE_WEST)
1119 angle += M_PI;
1120 double const cos_t = cos(angle);
1121 double const sin_t = sin(angle);
1122 for (unsigned int i = 0; i < nr_stars; i++) {
1123 cc[i].x = pos[i].x * cos_t - pos[i].y * sin_t + width / 2.0;
1124 cc[i].y = pos[i].x * sin_t + pos[i].y * cos_t + height / 2.0;
1125 }
1126
1127 #ifdef STEPGUIDER_SIMULATOR
1128 // add-in AO offset
1129 if (s_sim_ao) {
1130 double const ao_angle = radians(SimAoParams::angle);
1131 double const cos_a = cos(ao_angle);
1132 double const sin_a = sin(ao_angle);
1133 double const ao_x = (double) s_sim_ao->CurrentPosition(RIGHT) * SimAoParams::scale;
1134 double const ao_y = (double) s_sim_ao->CurrentPosition(UP) * SimAoParams::scale;
1135 double const dx = ao_x * cos_a - ao_y * sin_a;
1136 double const dy = ao_x * sin_a + ao_y * cos_a;
1137 for (unsigned int i = 0; i < nr_stars; i++) {
1138 cc[i].x += dx;
1139 cc[i].y += dy;
1140 }
1141 }
1142 #endif // STEPGUIDER_SIMULATOR
1143
1144 // render each star
1145 if (!pCamera->ShutterClosed)
1146 {
1147 for (unsigned int i = 0; i < nr_stars; i++)
1148 {
1149 double star = stars[i].inten * exptime * gain;
1150 double dark = (double) gain / 10.0 * offset * exptime / 100.0;
1151 double noise = (double)(rand() % (gain * 100));
1152 double inten = star + dark + noise;
1153
1154 render_star(img, pCamera->Binning, subframe, cc[i], inten);
1155 }
1156
1157 #ifndef SIM_FILE_DISPLACEMENTS
1158 if (SimCamParams::show_comet)
1159 {
1160 double x = total_shift_x + now * SimCamParams::comet_rate_x / 3600.;
1161 double y = total_shift_y + now * SimCamParams::comet_rate_y / 3600.;
1162 double cx = x * cos_t - y * sin_t + width / 2.0;
1163 double cy = x * sin_t + y * cos_t + height / 2.0;
1164
1165 double inten = 3.0;
1166 double star = inten * exptime * gain;
1167 double dark = (double) gain / 10.0 * offset * exptime / 100.0;
1168 double noise = (double)(rand() % (gain * 100));
1169 inten = star + dark + noise;
1170
1171 render_comet(img, pCamera->Binning, subframe, wxRealPoint(cx, cy), inten);
1172 }
1173 #endif
1174 }
1175
1176 if (SimCamParams::clouds_opacity > 0)
1177 render_clouds(img, subframe, exptime, gain, offset);
1178
1179 // render hot pixels
1180 for (unsigned int i = 0; i < hotpx.size(); i++)
1181 {
1182 wxPoint p(hotpx[i]);
1183 p.x /= pCamera->Binning;
1184 p.y /= pCamera->Binning;
1185 if (subframe.Contains(p))
1186 set_pixel(img, p.x, p.y, (unsigned short)-1);
1187 }
1188 }
1189
1190 class CameraSimulator : public GuideCamera
1191 {
1192 SimCamState sim;
1193 public:
1194 CameraSimulator();
1195 ~CameraSimulator();
1196 bool Capture(int duration, usImage& img, int options, const wxRect& subframe) override;
1197 bool Connect(const wxString& camId) override;
1198 bool Disconnect() override;
1199 void ShowPropertyDialog() override;
HasNonGuiCapture()1200 bool HasNonGuiCapture() override { return true; }
1201 wxByte BitsPerPixel() override;
1202 bool SetCoolerOn(bool on) override;
1203 bool SetCoolerSetpoint(double temperature) override;
1204 bool GetCoolerStatus(bool *on, double *setpoint, double *power, double *temperature) override;
1205 bool GetSensorTemperature(double *temperature) override;
ST4HasNonGuiMove()1206 bool ST4HasNonGuiMove() override { return true; }
1207 bool ST4SynchronousOnly() override;
1208 bool ST4PulseGuideScope(int direction, int duration) override;
1209 PierSide SideOfPier() const;
1210 void FlipPierSide();
1211 };
1212
CameraSimulator()1213 CameraSimulator::CameraSimulator()
1214 {
1215 Connected = false;
1216 Name = _T("Simulator");
1217 m_hasGuideOutput = true;
1218 HasShutter = true;
1219 HasGainControl = true;
1220 HasSubframes = true;
1221 PropertyDialogType = PROPDLG_WHEN_CONNECTED;
1222 MaxBinning = 3;
1223 HasCooler = true;
1224 }
1225
BitsPerPixel()1226 wxByte CameraSimulator::BitsPerPixel()
1227 {
1228 #if 1
1229 return 16;
1230 #else
1231 return 8;
1232 #endif
1233 }
1234
Connect(const wxString & camId)1235 bool CameraSimulator::Connect(const wxString& camId)
1236 {
1237 load_sim_params();
1238 sim.Initialize();
1239
1240 struct ConnectInBg : public ConnectCameraInBg
1241 {
1242 CameraSimulator *cam;
1243 ConnectInBg(CameraSimulator *cam_) : cam(cam_) { }
1244 bool Entry()
1245 {
1246 //#define TEST_SLOW_CONNECT
1247 #ifdef TEST_SLOW_CONNECT
1248 for (int i = 0; i < 100; i++)
1249 {
1250 wxMilliSleep(100);
1251 if (IsCanceled())
1252 return true;
1253 }
1254 #endif
1255 return false;
1256 }
1257 };
1258
1259 bool err = ConnectInBg(this).Run();
1260 if (!err)
1261 Connected = true;
1262
1263 return err;
1264 }
1265
Disconnect()1266 bool CameraSimulator::Disconnect()
1267 {
1268 Connected = false;
1269 return false;
1270 }
1271
~CameraSimulator()1272 CameraSimulator::~CameraSimulator()
1273 {
1274 #ifdef SIMDEBUG
1275 sim.DebugFile.Close();
1276 #endif
1277 #ifdef SIM_FILE_DISPLACEMENTS
1278 if (sim.pText)
1279 delete sim.pText;
1280 if (sim.pIStream)
1281 delete sim.pIStream;
1282 #endif
1283 }
1284
1285 #if SIMMODE==2
Capture(int duration,usImage & img,int options,const wxRect & subframe)1286 bool CameraSimulator::Capture(int duration, usImage& img, int options, const wxRect& subframe)
1287 {
1288 int xsize, ysize;
1289 wxImage disk_image;
1290 unsigned short *dataptr;
1291 unsigned char *imgptr;
1292
1293 bool retval = disk_image.LoadFile("/Users/stark/dev/PHD/simimage.bmp");
1294 if (!retval) {
1295 pFrame->Alert(_("Cannot load simulated image"));
1296 return true;
1297 }
1298 xsize = disk_image.GetWidth();
1299 ysize = disk_image.GetHeight();
1300 if (img.Init(xsize,ysize)) {
1301 pFrame->Alert(_("Memory allocation error"));
1302 return true;
1303 }
1304
1305 dataptr = img.ImageData;
1306 imgptr = disk_image.GetData();
1307 for (unsigned int i = 0; i < img.NPixels; i++, dataptr++, imgptr++) {
1308 *dataptr = (unsigned short) *imgptr;
1309 imgptr++; imgptr++;
1310 }
1311 QuickLRecon(img);
1312 return false;
1313
1314 }
1315 #endif
1316
1317 #if SIMMODE == 3
fill_noise(usImage & img,const wxRect & subframe,int exptime,int gain,int offset)1318 static void fill_noise(usImage& img, const wxRect& subframe, int exptime, int gain, int offset)
1319 {
1320 unsigned short *p0 = &img.Pixel(subframe.GetLeft(), subframe.GetTop());
1321 for (int r = 0; r < subframe.GetHeight(); r++, p0 += img.Size.GetWidth())
1322 {
1323 unsigned short *const end = p0 + subframe.GetWidth();
1324 for (unsigned short *p = p0; p < end; p++)
1325 *p = (unsigned short) (SimCamParams::noise_multiplier * ((double) gain / 10.0 * offset * exptime / 100.0 + (rand() % (gain * 100))));
1326 }
1327 }
1328 #endif // SIMMODE == 3
1329
Capture(int duration,usImage & img,int options,const wxRect & subframeArg)1330 bool CameraSimulator::Capture(int duration, usImage& img, int options, const wxRect& subframeArg)
1331 {
1332 wxRect subframe(subframeArg);
1333 CameraWatchdog watchdog(duration, GetTimeoutMs());
1334
1335 // sleep before rendering the image so that any changes made in the middle of a long exposure (e.g. manual guide pulse) shows up in the image
1336
1337 if (duration > 5)
1338 {
1339 if (WorkerThread::MilliSleep(duration - 5, WorkerThread::INT_ANY))
1340 return true;
1341 if (watchdog.Expired())
1342 {
1343 DisconnectWithAlert(CAPT_FAIL_TIMEOUT);
1344 return true;
1345 }
1346 }
1347
1348 #if SIMMODE == 1
1349
1350 if (!UseSubframes)
1351 subframe = wxRect();
1352
1353 if (sim.ReadNextImage(img, subframe))
1354 return true;
1355
1356 FullSize = img.Size;
1357
1358 #else
1359
1360 int width = sim.width / Binning;
1361 int height = sim.height / Binning;
1362 FullSize = wxSize(width, height);
1363
1364 bool usingSubframe = UseSubframes;
1365 if (subframe.width <= 0 || subframe.height <= 0 || subframe.GetRight() >= width || subframe.GetBottom() >= height)
1366 usingSubframe = false;
1367 if (!usingSubframe)
1368 subframe = wxRect(0, 0, FullSize.GetWidth(), FullSize.GetHeight());
1369
1370 int const exptime = duration;
1371 int const gain = 30;
1372 int const offset = 100;
1373
1374 if (img.Init(FullSize))
1375 {
1376 pFrame->Alert(_("Memory allocation error"));
1377 return true;
1378 }
1379
1380 if (usingSubframe)
1381 img.Clear();
1382
1383 fill_noise(img, subframe, exptime, gain, offset);
1384
1385 sim.FillImage(img, subframe, exptime, gain, offset);
1386
1387 if (usingSubframe)
1388 img.Subframe = subframe;
1389
1390 if (options & CAPTURE_SUBTRACT_DARK) SubtractDark(img);
1391
1392 #endif // SIMMODE == 1
1393
1394 unsigned int tot_dur = duration + SimCamParams::frame_download_ms;
1395 long elapsed = watchdog.Time();
1396 if (elapsed < tot_dur)
1397 {
1398 if (WorkerThread::MilliSleep(tot_dur - elapsed, WorkerThread::INT_ANY))
1399 return true;
1400 if (watchdog.Expired())
1401 {
1402 DisconnectWithAlert(CAPT_FAIL_TIMEOUT);
1403 return true;
1404 }
1405 }
1406
1407 return false;
1408 }
1409
1410
1411
ST4PulseGuideScope(int direction,int duration)1412 bool CameraSimulator::ST4PulseGuideScope(int direction, int duration)
1413 {
1414 // Following must take into account how the render_star function works. Render_star uses camera binning explicitly, so
1415 // relying only on image scale in computing d creates distances that are too small by a factor of <binning>
1416 double d = SimCamParams::guide_rate * Binning * duration / (1000.0 * SimCamParams::image_scale);
1417
1418 // simulate RA motion scaling according to declination
1419 if (direction == WEST || direction == EAST)
1420 {
1421 double dec = pPointingSource->GetDeclination();
1422 if (dec == UNKNOWN_DECLINATION)
1423 dec = radians(25.0); // some arbitrary declination
1424 d *= cos(dec);
1425 }
1426
1427 // simulate stiction if option selected
1428 if (SimCamParams::use_stiction && (direction == NORTH || direction == SOUTH))
1429 d += sim.stictionSim.GetAdjustment(direction, duration, d);
1430
1431 if (SimCamParams::pier_side == PIER_SIDE_WEST && SimCamParams::reverse_dec_pulse_on_west_side)
1432 {
1433 // after pier flip, North/South have opposite affect on declination
1434 switch (direction) {
1435 case NORTH: direction = SOUTH; break;
1436 case SOUTH: direction = NORTH; break;
1437 }
1438 }
1439
1440 switch (direction) {
1441 case WEST: sim.ra_ofs += d; break;
1442 case EAST: sim.ra_ofs -= d; break;
1443 case NORTH: sim.dec_ofs.incr(d); break;
1444 case SOUTH: sim.dec_ofs.incr(-d); break;
1445 default: return true;
1446 }
1447 WorkerThread::MilliSleep(duration, WorkerThread::INT_ANY);
1448 return false;
1449 }
1450
SetCoolerOn(bool on)1451 bool CameraSimulator::SetCoolerOn(bool on)
1452 {
1453 if (on)
1454 sim.cooler.TurnOn();
1455 else
1456 sim.cooler.TurnOff();
1457
1458 return false; // no error
1459 }
1460
SetCoolerSetpoint(double temperature)1461 bool CameraSimulator::SetCoolerSetpoint(double temperature)
1462 {
1463 if (!sim.cooler.on)
1464 return true; // error
1465
1466 sim.cooler.SetTemp(temperature);
1467 return false;
1468 }
1469
GetCoolerStatus(bool * onp,double * setpoint,double * power,double * temperature)1470 bool CameraSimulator::GetCoolerStatus(bool *onp, double *setpoint, double *power, double *temperature)
1471 {
1472 bool on = sim.cooler.on;
1473 double cur = sim.cooler.CurrentTemp();
1474
1475 *onp = on;
1476
1477 if (on)
1478 {
1479 *setpoint = sim.cooler.setTemp;
1480 *power = cur < MIN_COOLER_TEMP ? 100. : cur >= AMBIENT_TEMP ? 0. : (AMBIENT_TEMP - cur) * 100. / (AMBIENT_TEMP - MIN_COOLER_TEMP);
1481 *temperature = cur;
1482 }
1483 else
1484 {
1485 *temperature = cur;
1486 }
1487
1488 return false;
1489 }
1490
GetSensorTemperature(double * temperature)1491 bool CameraSimulator::GetSensorTemperature(double *temperature)
1492 {
1493 bool on;
1494 double setpt, powr;
1495 return GetCoolerStatus(&on, &setpt, &powr, temperature);
1496 }
1497
SideOfPier() const1498 PierSide CameraSimulator::SideOfPier() const
1499 {
1500 return SimCamParams::pier_side;
1501 }
1502
ST4SynchronousOnly()1503 bool CameraSimulator::ST4SynchronousOnly()
1504 {
1505 return !SimCamParams::allow_async_st4;
1506 }
1507
OtherSide(PierSide side)1508 static PierSide OtherSide(PierSide side)
1509 {
1510 return side == PIER_SIDE_EAST ? PIER_SIDE_WEST : PIER_SIDE_EAST;
1511 }
1512
FlipPierSide()1513 void CameraSimulator::FlipPierSide()
1514 {
1515 SimCamParams::pier_side = OtherSide(SimCamParams::pier_side);
1516 Debug.Write(wxString::Format("CamSimulator FlipPierSide: side = %d cam_angle = %.1f\n", SimCamParams::pier_side, SimCamParams::cam_angle));
1517 }
1518
1519 #if SIMMODE == 4
Capture(int duration,usImage & img,int options,const wxRect & subframe)1520 bool CameraSimulator::Capture(int duration, usImage& img, int options, const wxRect& subframe)
1521 {
1522 int xsize, ysize;
1523 // unsigned short *dataptr;
1524 // int i;
1525 fitsfile *fptr; // FITS file pointer
1526 int status = 0; // CFITSIO status value MUST be initialized to zero!
1527 int hdutype, naxis;
1528 int nhdus=0;
1529 long fits_size[2];
1530 long fpixel[3] = {1,1,1};
1531 // char keyname[15];
1532 // char keystrval[80];
1533 static int frame = 0;
1534 static int step = 1;
1535 char fname[256];
1536 sprintf(fname,"/Users/stark/dev/PHD/simimg/DriftSim_%d.fit",frame);
1537 if (!PHD_fits_open_diskfile(&fptr, fname, READONLY, &status))
1538 {
1539 if (fits_get_hdu_type(fptr, &hdutype, &status) || hdutype != IMAGE_HDU) {
1540 pFrame->Alert(_("FITS file is not of an image"));
1541 PHD_fits_close_file(fptr);
1542 return true;
1543 }
1544
1545 // Get HDUs and size
1546 fits_get_img_dim(fptr, &naxis, &status);
1547 fits_get_img_size(fptr, 2, fits_size, &status);
1548 xsize = (int) fits_size[0];
1549 ysize = (int) fits_size[1];
1550 fits_get_num_hdus(fptr,&nhdus,&status);
1551 if ((nhdus != 1) || (naxis != 2)) {
1552 pFrame->Alert(wxString::Format(_("Unsupported type or read error loading FITS file %d %d"),nhdus,naxis));
1553 PHD_fits_close_file(fptr);
1554 return true;
1555 }
1556 if (img.Init(xsize,ysize)) {
1557 pFrame->Alert(_("Memory allocation error"));
1558 PHD_fits_close_file(fptr);
1559 return true;
1560 }
1561 if (fits_read_pix(fptr, TUSHORT, fpixel, xsize*ysize, nullptr, img.ImageData, nullptr, &status) ) { // Read image
1562 pFrame->Alert(_("Error reading data"));
1563 PHD_fits_close_file(fptr);
1564 return true;
1565 }
1566 PHD_fits_close_file(fptr);
1567 frame = frame + step;
1568 if (frame > 440) {
1569 step = -1;
1570 frame = 439;
1571 }
1572 else if (frame < 0) {
1573 step = 1;
1574 frame = 1;
1575 }
1576
1577 }
1578 return false;
1579
1580 }
1581 #endif // SIMMODE == 4
1582
1583 struct SimCamDialog : public wxDialog
1584 {
1585 wxSlider *pStarsSlider;
1586 wxSlider *pHotpxSlider;
1587 wxSlider *pNoiseSlider;
1588 wxSlider *pCloudSlider;
1589 wxSpinCtrlDouble *pBacklashSpin;
1590 wxSpinCtrlDouble *pDriftSpin;
1591 wxSpinCtrlDouble *pGuideRateSpin;
1592 wxSpinCtrlDouble *pCameraAngleSpin;
1593 wxSpinCtrlDouble *pSeeingSpin;
1594 wxCheckBox* showComet;
1595 wxCheckBox *pUsePECbx;
1596 wxCheckBox *pUseStiction;
1597 wxCheckBox *pReverseDecPulseCbx;
1598 PierSide pPierSide;
1599 wxStaticText *pPiersideLabel;
1600 wxRadioButton *pPEDefaultRb;
1601 wxSpinCtrlDouble *pPEDefScale;
1602 wxRadioButton *pPECustomRb;
1603 wxTextCtrl *pPECustomAmp;
1604 wxTextCtrl *pPECustomPeriod;
1605 wxButton *pPierFlip;
1606 wxButton *pResetBtn;
1607
1608 SimCamDialog(wxWindow *parent);
~SimCamDialogSimCamDialog1609 ~SimCamDialog() { }
1610 void OnReset(wxCommandEvent& event);
1611 void OnPierFlip(wxCommandEvent& event);
1612 void UpdatePierSideLabel();
1613 void OnRbDefaultPE(wxCommandEvent& evt);
1614 void OnRbCustomPE(wxCommandEvent& evt);
1615 void OnOkClick(wxCommandEvent& evt);
1616
1617 DECLARE_EVENT_TABLE()
1618 };
1619
BEGIN_EVENT_TABLE(SimCamDialog,wxDialog)1620 BEGIN_EVENT_TABLE(SimCamDialog, wxDialog)
1621 EVT_BUTTON(wxID_RESET, SimCamDialog::OnReset)
1622 EVT_BUTTON(wxID_CONVERT, SimCamDialog::OnPierFlip)
1623 END_EVENT_TABLE()
1624
1625 // Utility functions for adding controls with specified properties
1626 static wxSlider *NewSlider(wxWindow *parent, int val, int minval, int maxval, const wxString& tooltip)
1627 {
1628 wxSlider *pNewCtrl = new wxSlider(parent, wxID_ANY, val, minval, maxval, wxDefaultPosition, wxDefaultSize, wxSL_HORIZONTAL | wxSL_VALUE_LABEL);
1629 pNewCtrl->SetToolTip(tooltip);
1630 return pNewCtrl;
1631 }
1632
NewSpinner(wxWindow * parent,double val,double minval,double maxval,double inc,const wxString & tooltip)1633 static wxSpinCtrlDouble *NewSpinner(wxWindow *parent, double val, double minval, double maxval, double inc,
1634 const wxString& tooltip)
1635 {
1636 wxSize sz = pFrame->GetTextExtent(wxString::Format("%.2f", maxval * 10.));
1637 wxSpinCtrlDouble *pNewCtrl = pFrame->MakeSpinCtrlDouble(parent, wxID_ANY, wxEmptyString, wxDefaultPosition,
1638 sz, wxSP_ARROW_KEYS, minval, maxval, val, inc);
1639 pNewCtrl->SetDigits(2);
1640 pNewCtrl->SetToolTip(tooltip);
1641 return pNewCtrl;
1642 }
1643
NewCheckBox(wxWindow * parent,bool val,const wxString & label,const wxString & tooltip)1644 static wxCheckBox *NewCheckBox(wxWindow *parent, bool val, const wxString& label, const wxString& tooltip)
1645 {
1646 wxCheckBox *pNewCtrl = new wxCheckBox(parent, wxID_ANY, label);
1647 pNewCtrl->SetValue(val);
1648 pNewCtrl->SetToolTip(tooltip);
1649 return pNewCtrl;
1650 }
1651
1652 // Utility function to add the <label, input> pairs to a grid including tool-tips
AddTableEntryPair(wxWindow * parent,wxFlexGridSizer * pTable,const wxString & label,wxWindow * pControl)1653 static void AddTableEntryPair(wxWindow *parent, wxFlexGridSizer *pTable, const wxString& label, wxWindow *pControl)
1654 {
1655 wxStaticText *pLabel = new wxStaticText(parent, wxID_ANY, label + _(": "), wxPoint(-1,-1), wxSize(-1,-1));
1656 pTable->Add(pLabel, 1, wxALL, 5);
1657 pTable->Add(pControl, 1, wxALL, 5);
1658 }
1659
AddCustomPEField(wxWindow * parent,wxFlexGridSizer * pTable,const wxString & label,const wxString & tip,double val)1660 static wxTextCtrl *AddCustomPEField(wxWindow *parent, wxFlexGridSizer *pTable, const wxString& label, const wxString& tip, double val)
1661 {
1662 int width;
1663 int height;
1664
1665 parent->GetTextExtent(_T("999.9"), &width, &height);
1666 wxTextCtrl *pCtrl = new wxTextCtrl(parent, wxID_ANY, _T(" "), wxDefaultPosition, wxSize(width+30, -1));
1667 pCtrl->SetValue(wxString::Format("%.1f", val));
1668 pCtrl->SetToolTip(tip);
1669 AddTableEntryPair(parent, pTable, label, pCtrl);
1670 return pCtrl;
1671 }
1672
SetRBState(SimCamDialog * dlg,bool using_defaults)1673 static void SetRBState(SimCamDialog *dlg, bool using_defaults)
1674 {
1675 dlg->pPEDefScale->Enable(using_defaults);
1676 dlg->pPECustomAmp->Enable(!using_defaults);
1677 dlg->pPECustomPeriod->Enable(!using_defaults);
1678 }
1679
SetControlStates(SimCamDialog * dlg,bool captureActive)1680 static void SetControlStates(SimCamDialog *dlg, bool captureActive)
1681 {
1682 bool enable = !captureActive;
1683
1684 dlg->pBacklashSpin->Enable(enable);
1685 dlg->pGuideRateSpin->Enable(enable);
1686 dlg->pCameraAngleSpin->Enable(enable);
1687 dlg->pPEDefaultRb->Enable(enable);
1688 dlg->pPEDefScale->Enable(enable);
1689 dlg->pPECustomAmp->Enable(enable);
1690 dlg->pPECustomPeriod->Enable(enable);
1691 dlg->pPECustomRb->Enable(enable);
1692 dlg->pUsePECbx->Enable(enable);
1693 dlg->pUseStiction->Enable(false); // no good for end-users
1694 dlg->pPierFlip->Enable(enable);
1695 dlg->pReverseDecPulseCbx->Enable(enable);
1696 dlg->pResetBtn->Enable(enable);
1697 }
1698
1699 // Event handlers
OnRbDefaultPE(wxCommandEvent & evt)1700 void SimCamDialog::OnRbDefaultPE(wxCommandEvent& evt)
1701 {
1702 SetRBState(this, true);
1703 }
1704
OnRbCustomPE(wxCommandEvent & evt)1705 void SimCamDialog::OnRbCustomPE(wxCommandEvent& evt)
1706 {
1707 SetRBState(this, false);
1708 }
1709
1710 // Need to enforce semantics on free-form user input
OnOkClick(wxCommandEvent & evt)1711 void SimCamDialog::OnOkClick(wxCommandEvent& evt)
1712 {
1713 bool bOk = true;
1714
1715 if (pPECustomRb->GetValue())
1716 {
1717 wxString sAmp = pPECustomAmp->GetValue();
1718 wxString sPeriod = pPECustomPeriod->GetValue();
1719 double amp;
1720 double period;
1721 if (sAmp.ToDouble(&) && sPeriod.ToDouble(&period))
1722 {
1723 if (amp <= 0.0 || period <= 0.0)
1724 {
1725 wxMessageBox(_("PE amplitude and period must be > 0"), "Error", wxOK | wxICON_ERROR);
1726 bOk = false;
1727 }
1728 }
1729 else
1730 {
1731 wxMessageBox(_("PE amplitude and period must be numbers > 0"), "Error", wxOK | wxICON_ERROR);
1732 bOk = false;
1733 }
1734 }
1735
1736 if (bOk)
1737 wxDialog::EndModal(wxID_OK);
1738 }
1739
SimCamDialog(wxWindow * parent)1740 SimCamDialog::SimCamDialog(wxWindow *parent)
1741 : wxDialog(parent, wxID_ANY, _("Camera Simulator"))
1742 {
1743 wxBoxSizer *pVSizer = new wxBoxSizer(wxVERTICAL);
1744 double imageScale = pFrame->GetCameraPixelScale();
1745
1746 SimCamParams::image_scale = imageScale;
1747
1748 // Camera group controls
1749 wxStaticBoxSizer *pCamGroup = new wxStaticBoxSizer(wxVERTICAL, this, _("Camera"));
1750 wxFlexGridSizer *pCamTable = new wxFlexGridSizer(1, 6, 15, 15);
1751 pStarsSlider = NewSlider(this, SimCamParams::nr_stars, 1, 100, _("Number of simulated stars"));
1752 AddTableEntryPair(this, pCamTable, _("Stars"), pStarsSlider);
1753 pHotpxSlider = NewSlider(this, SimCamParams::nr_hot_pixels, 0, 50, _("Number of hot pixels"));
1754 AddTableEntryPair(this, pCamTable, _("Hot pixels"), pHotpxSlider);
1755 pNoiseSlider = NewSlider(this, (int)floor(SimCamParams::noise_multiplier * 100 / NOISE_MAX), 0, 100,
1756 /* xgettext:no-c-format */ _("% Simulated noise"));
1757 AddTableEntryPair(this, pCamTable, _("Noise"), pNoiseSlider);
1758 pCamGroup->Add(pCamTable);
1759
1760 // Mount group controls
1761 wxStaticBoxSizer *pMountGroup = new wxStaticBoxSizer(wxVERTICAL, this, _("Mount"));
1762 wxFlexGridSizer *pMountTable = new wxFlexGridSizer(2, 6, 5, 15);
1763 pBacklashSpin = NewSpinner(this, SimCamParams::dec_backlash * imageScale, 0, DEC_BACKLASH_MAX, 0.1, _("Dec backlash, arc-secs"));
1764 AddTableEntryPair(this, pMountTable, _("Dec backlash"), pBacklashSpin);
1765 pDriftSpin = NewSpinner(this, SimCamParams::dec_drift_rate * 60.0 * imageScale, -DEC_DRIFT_MAX, DEC_DRIFT_MAX, 0.5, _("Dec drift, arc-sec/min"));
1766 AddTableEntryPair(this, pMountTable, _("Dec drift"), pDriftSpin);
1767 pGuideRateSpin = NewSpinner(this, SimCamParams::guide_rate / 15.0, 0.25, GUIDE_RATE_MAX, 0.25, _("Guide rate, x sidereal"));
1768 AddTableEntryPair(this, pMountTable, _("Guide rate"), pGuideRateSpin);
1769 pUseStiction = NewCheckBox(this, SimCamParams::use_stiction, _("Apply stiction"), _("Simulate dec axis stiction"));
1770 pUseStiction->Enable(false); // too crude to put in hands of users
1771 pMountTable->Add(pUseStiction, 1, wxBOTTOM, 15);
1772 pMountGroup->Add(pMountTable);
1773
1774 // Add embedded group for PE info (still within mount group)
1775 wxStaticBoxSizer *pPEGroup = new wxStaticBoxSizer(wxVERTICAL, this, _("PE"));
1776 pUsePECbx = NewCheckBox(this, SimCamParams::use_pe, _("Apply PE"), _("Simulate periodic error"));
1777 wxBoxSizer *pPEHorSizer = new wxBoxSizer(wxHORIZONTAL);
1778 // Default PE parameters
1779 wxFlexGridSizer *pPEDefaults = new wxFlexGridSizer(1, 3, 10, 10);
1780 pPEDefaultRb = new wxRadioButton(this, wxID_ANY, _("Default curve"), wxDefaultPosition, wxDefaultSize, wxRB_GROUP);
1781 pPEDefaultRb->SetValue(SimCamParams::use_default_pe_params);
1782 pPEDefaultRb->SetToolTip(_("Use a built-in PE curve that has some steep and smooth sections."));
1783 pPEDefaultRb->Bind(wxEVT_COMMAND_RADIOBUTTON_SELECTED, &SimCamDialog::OnRbDefaultPE, this); // Event handler binding
1784 wxStaticText *pSliderLabel = new wxStaticText(this, wxID_ANY, _("Amplitude: "),wxPoint(-1,-1),wxSize(-1,-1));
1785 pPEDefScale = NewSpinner(this, SimCamParams::pe_scale, 0, PE_SCALE_MAX, 0.5, _("PE Amplitude, arc-secs"));
1786
1787 int hor_spacing = StringWidth(this, "9");
1788 pPEDefaults->Add(pPEDefaultRb);
1789 pPEDefaults->Add(pSliderLabel, wxSizerFlags().Border(wxLEFT, hor_spacing));
1790 pPEDefaults->Add(pPEDefScale, wxSizerFlags().Border(wxLEFT, hor_spacing + 1));
1791 // Custom PE parameters
1792 wxFlexGridSizer *pPECustom = new wxFlexGridSizer(1, 5, 10, 10);
1793 pPECustomRb = new wxRadioButton(this, wxID_ANY, _("Custom curve"), wxDefaultPosition, wxDefaultSize);
1794 pPECustomRb->SetValue(!SimCamParams::use_default_pe_params);
1795 pPECustomRb->SetToolTip(_("Use a simple sinusoidal curve. You can specify the amplitude and period."));
1796 pPECustomRb->Bind(wxEVT_COMMAND_RADIOBUTTON_SELECTED, &SimCamDialog::OnRbCustomPE, this); // Event handler binding
1797 pPECustom->Add(pPECustomRb, wxSizerFlags().Border(wxTOP, 4));
1798 pPECustomAmp = AddCustomPEField(this, pPECustom, _("Amplitude"), _("Amplitude, arc-secs"), SimCamParams::custom_pe_amp);
1799 pPECustomPeriod = AddCustomPEField(this, pPECustom, _("Period"), _("Period, seconds"), SimCamParams::custom_pe_period);
1800 // VSizer for default and custom controls
1801 wxBoxSizer *pPEVSizer = new wxBoxSizer(wxVERTICAL);
1802 pPEVSizer->Add(pPEDefaults, wxSizerFlags().Border(wxLEFT, 60));
1803 pPEVSizer->Add(pPECustom, wxSizerFlags().Border(wxLEFT, 60));
1804 // Finish off the whole PE group
1805 pPEHorSizer->Add(pUsePECbx);
1806 pPEHorSizer->Add(pPEVSizer);
1807 pPEGroup->Add(pPEHorSizer);
1808
1809 // Now add some miscellaneous mount-related stuff (still within mount group)
1810 wxBoxSizer *pMiscSizer = new wxBoxSizer(wxHORIZONTAL);
1811 pReverseDecPulseCbx = NewCheckBox(this, SimCamParams::reverse_dec_pulse_on_west_side, _("Reverse Dec pulse on West side of pier"),
1812 _("Simulate a mount that reverses guide pulse direction after a meridian flip, like an ASCOM pulse-guided mount."));
1813 pPierSide = SimCamParams::pier_side;
1814 pPiersideLabel = new wxStaticText(this, wxID_ANY, _("Side of Pier: MMMMM"));
1815 pMiscSizer->Add(pReverseDecPulseCbx, wxSizerFlags().Border(10).Expand());
1816 pPierFlip = new wxButton(this, wxID_CONVERT, _("Pier Flip"));
1817 pMiscSizer->Add(pPierFlip, wxSizerFlags().Border(wxLEFT, 30).Expand());
1818 pMiscSizer->Add(pPiersideLabel , wxSizerFlags().Border(wxLEFT, 30).Expand());
1819 pMountGroup->Add(pPEGroup, wxSizerFlags().Center().Border(10).Expand());
1820 pMountGroup->Add(pMiscSizer, wxSizerFlags().Border(wxTOP, 10).Expand());
1821
1822 // Session group controls
1823 wxStaticBoxSizer *pSessionGroup = new wxStaticBoxSizer(wxVERTICAL, this, _("Session"));
1824 wxFlexGridSizer *pSessionTable = new wxFlexGridSizer(1, 6, 15, 15);
1825 pCameraAngleSpin = NewSpinner(this, SimCamParams::cam_angle, 0, CAM_ANGLE_MAX, 10, _("Camera angle, degrees"));
1826 AddTableEntryPair(this, pSessionTable, _("Camera angle"), pCameraAngleSpin);
1827 pSeeingSpin = NewSpinner(this, SimCamParams::seeing_scale, 0, SEEING_MAX, 0.5, _("Seeing, FWHM arc-sec"));
1828 AddTableEntryPair(this, pSessionTable, _("Seeing"), pSeeingSpin);
1829 pCloudSlider = NewSlider(this, (int)(100 * SimCamParams::clouds_opacity), 0, 100, _("% cloud opacity"));
1830 AddTableEntryPair(this, pSessionTable, _("Cloud %"), pCloudSlider);
1831 showComet = new wxCheckBox(this, wxID_ANY, _("Comet"));
1832 showComet->SetValue(SimCamParams::show_comet);
1833 pSessionGroup->Add(pSessionTable);
1834 pSessionGroup->Add(showComet);
1835
1836 pVSizer->Add(pCamGroup, wxSizerFlags().Border(wxALL, 10).Expand());
1837 pVSizer->Add(pMountGroup, wxSizerFlags().Border(wxRIGHT | wxLEFT, 10));
1838 pVSizer->Add(pSessionGroup, wxSizerFlags().Border(wxRIGHT | wxLEFT, 10).Expand());
1839
1840 // Now deal with the buttons
1841 wxBoxSizer *pButtonSizer = new wxBoxSizer( wxHORIZONTAL );
1842 pResetBtn = new wxButton(this, wxID_RESET, _("Reset"));
1843 pResetBtn->SetToolTip(_("Reset all values to application defaults"));
1844 pButtonSizer->Add(
1845 pResetBtn,
1846 wxSizerFlags(0).Align(0).Border(wxALL, 10));
1847 // Need to handle the OK event ourselves to validate text input fields
1848 wxButton *pBtn = new wxButton(this, wxID_OK, _("OK"));
1849 pBtn->Bind(wxEVT_COMMAND_BUTTON_CLICKED, &SimCamDialog::OnOkClick, this);
1850 pButtonSizer->Add(
1851 pBtn,
1852 wxSizerFlags(0).Align(0).Border(wxALL, 10));
1853 pButtonSizer->Add(
1854 new wxButton( this, wxID_CANCEL, _("Cancel") ),
1855 wxSizerFlags(0).Align(0).Border(wxALL, 10));
1856
1857 //position the buttons centered with no border
1858 pVSizer->Add(
1859 pButtonSizer,
1860 wxSizerFlags(0).Center() );
1861
1862 SetSizerAndFit(pVSizer);
1863 SetControlStates(this, pFrame->CaptureActive);
1864 if (!pFrame->CaptureActive)
1865 SetRBState(this, pPEDefaultRb->GetValue()); // Enable matching PE-related controls
1866 UpdatePierSideLabel();
1867 }
1868
OnReset(wxCommandEvent & event)1869 void SimCamDialog::OnReset(wxCommandEvent& event)
1870 {
1871 pStarsSlider->SetValue(NR_STARS_DEFAULT);
1872 pHotpxSlider->SetValue(NR_HOT_PIXELS_DEFAULT);
1873 pNoiseSlider->SetValue((int)floor(NOISE_DEFAULT * 100.0 / NOISE_MAX));
1874 pBacklashSpin->SetValue(DEC_BACKLASH_DEFAULT);
1875 pCloudSlider->SetValue(0);
1876
1877 pDriftSpin->SetValue(DEC_DRIFT_DEFAULT);
1878 pSeeingSpin->SetValue(SEEING_DEFAULT);
1879 pCameraAngleSpin->SetValue(CAM_ANGLE_DEFAULT);
1880 pGuideRateSpin->SetValue(GUIDE_RATE_DEFAULT / GUIDE_RATE_MAX);
1881 pReverseDecPulseCbx->SetValue(REVERSE_DEC_PULSE_ON_WEST_SIDE_DEFAULT);
1882 pUsePECbx->SetValue(USE_PE_DEFAULT);
1883 pUseStiction->SetValue(USE_STICTION_DEFAULT);
1884 pPEDefaultRb->SetValue(USE_PE_DEFAULT_PARAMS);
1885 pPECustomRb->SetValue(!USE_PE_DEFAULT_PARAMS);
1886 pPEDefScale->SetValue(PE_SCALE_DEFAULT);
1887 pPECustomAmp->SetValue(wxString::Format("%0.1f",PE_CUSTOM_AMP_DEFAULT));
1888 pPECustomPeriod->SetValue(wxString::Format("%0.1f", PE_CUSTOM_PERIOD_DEFAULT));
1889 pPierSide = PIER_SIDE_DEFAULT;
1890 SetRBState( this, USE_PE_DEFAULT_PARAMS);
1891 UpdatePierSideLabel();
1892 showComet->SetValue(SHOW_COMET_DEFAULT);
1893 }
1894
OnPierFlip(wxCommandEvent & event)1895 void SimCamDialog::OnPierFlip(wxCommandEvent& event)
1896 {
1897 int angle = pCameraAngleSpin->GetValue();
1898 angle += 180;
1899 if (angle >= 360)
1900 angle -= 360;
1901 pCameraAngleSpin->SetValue(angle);
1902 pPierSide= OtherSide(pPierSide);
1903 UpdatePierSideLabel();
1904 }
1905
UpdatePierSideLabel()1906 void SimCamDialog::UpdatePierSideLabel()
1907 {
1908 pPiersideLabel->SetLabel(wxString::Format(_("Side of pier: %s"), pPierSide == PIER_SIDE_EAST ? _("East") : _("West")));
1909 }
1910
1911 struct UpdateChecker
1912 {
1913 bool updated;
UpdateCheckerUpdateChecker1914 UpdateChecker() : updated(false) { }
1915 template<typename T, typename U>
UpdateUpdateChecker1916 void Update(T& val, const U& newval) {
1917 if (val != newval)
1918 {
1919 val = newval;
1920 updated = true;
1921 }
1922 }
WasModifiedUpdateChecker1923 bool WasModified() const { return updated; }
1924 };
1925
ShowPropertyDialog()1926 void CameraSimulator::ShowPropertyDialog()
1927 {
1928 SimCamDialog dlg(pFrame);
1929 double imageScale = pFrame->GetCameraPixelScale(); // arc-sec/pixel, defaults to 1.0 if no user specs
1930 SimCamParams::image_scale = imageScale; // keep current - might have gotten changed in brain dialog
1931 if (dlg.ShowModal() == wxID_OK)
1932 {
1933 UpdateChecker upd; // keep track of whether any values changed
1934 upd.Update(SimCamParams::nr_stars, dlg.pStarsSlider->GetValue());
1935 upd.Update(SimCamParams::nr_hot_pixels, dlg.pHotpxSlider->GetValue());
1936 SimCamParams::noise_multiplier = (double) dlg.pNoiseSlider->GetValue() * NOISE_MAX / 100.0;
1937 upd.Update(SimCamParams::dec_backlash, dlg.pBacklashSpin->GetValue() / imageScale); // a-s -> px
1938
1939 bool use_pe = dlg.pUsePECbx->GetValue();
1940 SimCamParams::use_pe = use_pe;
1941 SimCamParams::use_stiction = dlg.pUseStiction->GetValue();
1942 bool use_default_pe_params = dlg.pPEDefaultRb->GetValue();
1943 SimCamParams::use_default_pe_params = use_default_pe_params;
1944 if (SimCamParams::use_default_pe_params)
1945 {
1946 SimCamParams::pe_scale = dlg.pPEDefScale->GetValue();
1947 }
1948 else
1949 {
1950 dlg.pPECustomAmp->GetValue().ToDouble(&SimCamParams::custom_pe_amp);
1951 dlg.pPECustomPeriod->GetValue().ToDouble(&SimCamParams::custom_pe_period);
1952 }
1953 SimCamParams::dec_drift_rate = dlg.pDriftSpin->GetValue() / (imageScale * 60.0); // a-s per min to px per second
1954 SimCamParams::seeing_scale = dlg.pSeeingSpin->GetValue(); // already in a-s
1955 upd.Update(SimCamParams::cam_angle, dlg.pCameraAngleSpin->GetValue());
1956 SimCamParams::guide_rate = dlg.pGuideRateSpin->GetValue() * 15.0;
1957 SimCamParams::pier_side = dlg.pPierSide;
1958 SimCamParams::reverse_dec_pulse_on_west_side = dlg.pReverseDecPulseCbx->GetValue();
1959 SimCamParams::show_comet = dlg.showComet->GetValue();
1960 SimCamParams::clouds_opacity = dlg.pCloudSlider->GetValue() / 100.0;
1961 save_sim_params();
1962
1963 if (upd.WasModified())
1964 sim.Initialize();
1965 }
1966 }
1967
MakeCamSimulator()1968 GuideCamera *GearSimulator::MakeCamSimulator()
1969 {
1970 return new CameraSimulator();
1971 }
1972
FlipPierSide(GuideCamera * camera)1973 void GearSimulator::FlipPierSide(GuideCamera *camera)
1974 {
1975 if (camera && camera->Name == _T("Simulator"))
1976 {
1977 CameraSimulator *simcam = static_cast<CameraSimulator *>(camera);
1978 simcam->FlipPierSide();
1979 }
1980 }
1981
MakeAOSimulator()1982 StepGuider *GearSimulator::MakeAOSimulator()
1983 {
1984 return new StepGuiderSimulator();
1985 }
1986
MakeRotatorSimulator()1987 Rotator *GearSimulator::MakeRotatorSimulator()
1988 {
1989 return new RotatorSimulator();
1990 }
1991
1992 #endif // SIMULATOR
1993