API
 
Loading...
Searching...
No Matches
core.py
Go to the documentation of this file.
1import sys
2import logging
3from enum import Enum
4import time
5import numpy as np
6
7import xconf
8
9from magaox.indi.device import XDevice, BaseConfig
10from magaox.camera import XCam
11from magaox.constants import StateCodes
12
13from purepyindi2 import device, properties, constants
14from purepyindi2.messages import DefNumber, DefSwitch, DefLight, DefText
15
16import os
17import datetime
18import hcipy as hp
19from astropy.io import fits
20from scipy.optimize import minimize
21
22from camtipSR.utils import *
23
24@xconf.config
26 """
27 """
28 shmim : str = xconf.field(help="Name of the camera device (specifically, the associated shmim, if different)")
29 dark_shmim : str = xconf.field(help="Name of the dark frame shmim associated with this camera device")
30 #TODO: Does camtip have a dark frame?
31
32@xconf.config
33class camtipSRConfig(BaseConfig):
34 """ Configure """
35 # if want default need to be a cam config obj
36 camera : CameraConfig = xconf.field(help="Camera to use")
37
38class States(Enum):
39 IDLE = 0
40 CLOSED_LOOP = 1
41 ONESHOT = 2
42
43class camtipSR(XDevice):
44 config: camtipSRConfig
45
46 def setup(self):
47 # Finite State Machine (FSM) current state (I think)
48 fsm = properties.TextVector(name='fsm')
49 fsm.add_element(DefText(name='state', _value=StateCodes.INITIALIZED.name))
50 self.add_property(fsm)
51
52 # init INDI PROPERTY: loop state
53 sv = properties.SwitchVector(
54 name='state',
55 rule=constants.SwitchRule.ONE_OF_MANY,
56 perm=constants.PropertyPerm.READ_WRITE,
57 )
58 sv.add_element(DefSwitch(name="idle", _value=constants.SwitchState.ON))
59 sv.add_element(DefSwitch(name="SRLoop", _value=constants.SwitchState.OFF))
60 sv.add_element(DefSwitch(name="oneshot", _value=constants.SwitchState.OFF))
61 self.add_property(sv, callback=self.handle_state)
62
63 # init INDI PROPERTY: n_avg
64 nv = properties.NumberVector(name='n_avg')
65 nv.add_element(DefNumber(
66 name='current', label='Number of frames', format='%i',
67 min=1, max=150, step=1, _value=1
68 ))
69 nv.add_element(DefNumber(
70 name='target', label='Number of frames', format='%i',
71 min=1, max=150, step=1, _value=1
72 ))
73 self.add_property(nv, callback=self.handle_n_avg)
74
75 # init INDI PROPERTY: n_avg
76 nv = properties.NumberVector(name='n_jitter')
77 nv.add_element(DefNumber(
78 name='current', label='Number of frames', format='%i',
79 min=1, max=150, step=1, _value=1
80 ))
81 nv.add_element(DefNumber(
82 name='target', label='Number of frames', format='%i',
83 min=1, max=150, step=1, _value=1
84 ))
85 self.add_property(nv, callback=self.handle_n_jitter)
86
87 # init INDI PROPERTY: SR_est gaussian width method
88 nv = properties.NumberVector(name='SR_est')
89 nv.add_element(DefNumber(
90 name='current', label='SR_est', format='%i',
91 min=0, max=1, step=0.01, _value=1
92 ))
93 self.add_property(nv)
94
95 # init INDI PROPERTY: SR_EE encircled energy method
96 nv = properties.NumberVector(name='SR_EE')
97 nv.add_element(DefNumber(
98 name='current', label='SR_EE', format='%i',
99 min=0, max=1, step=0.01, _value=1
100 ))
101 self.add_property(nv)
102
103 # init INDI PROPERTY: jitter RMS x
104 nv = properties.NumberVector(name='jitter_RMS')
105 nv.add_element(DefNumber(
106 name='current', label='jitter_RMS', format='%i',
107 min=0, max=100, step=0.1, _value=0.0
108 ))
109 self.add_property(nv)
110
111 # init INDI PROPERTY: jitter RMS x
112 nv = properties.NumberVector(name='jitter_x')
113 nv.add_element(DefNumber(
114 name='current', label='jitter_x', format='%i',
115 min=-20, max=20, step=0.1, _value=0.0
116 ))
117 self.add_property(nv)
118
119 # init INDI PROPERTY: jitter rms y
120 nv = properties.NumberVector(name='jitter_y')
121 nv.add_element(DefNumber(
122 name='current', label='jitter_y', format='%i',
123 min=-20, max=20, step=0.1, _value=0.0
124 ))
125 self.add_property(nv)
126
127 # get INDI PROPERTIES
128
129 #TODO: Get camwfs to not crash - hiding for now
130 # Modulator for determining lab calibration file
131 #self.client.get_properties_and_wait(['modwfs'])
132 self.client.get_properties(['modwfs'])
133 time.sleep(10.0) # this takes a long time to load
134 self.modRadius = self.client['modwfs.modRadius.current']
135 self.log.info(f"Mod radius is {self.modRadius}")
136
137 # find the camera
138 self.log.info("Found camera: {:s}".format(self.config.camera.shmim))
139 self.camera = XCam(
140 self.config.camera.shmim,
141 pixel_size=6.0/21.0, #TODO: I need to redo that plate scale methinks
142 use_hcipy=True,
143 indi_client=self.client,
144 )
145
146 # Starting values for states
147 self._state = States.IDLE
148 self._dark = False
149 self._n_avg = 1
150 self._n_jitter = 100
151 self._SR_est = 0.0
152 self._SR_est_list = []
153 self._SR_EE = 0.0
154 self._SR_EE_list = []
155 self._x0_list = []
156 self._y0_list = []
157 self._jitter_RMS = 0.0
158 self._jitter_x = 0.0
159 self._jitter_y = 0.0
160 self.data_directory = '/opt/MagAOX/rawimages/camtipSR/'
161 self.lab_directory = '/opt/MagAOX/calib/camtipSR/'
162 self.dark_directory = '/opt/MagAOX/calib/camtip-dark/'
163 # SETUP: for the camtip fitter
165 # SETTING UP LAB
166 # TODO: pick the correct lab file, rn it's on vibes
167 if self.modRadius == 3:
168 self.labf = 'lab_2000_3ld_ND2.fits'
169 elif self.modRadius == 2:
170 self.labf = 'lab_3000_2ld_ND2.fits'
171 else:
172 self.log.exception(" Not a calibrated mod radius, applying a bogus lab file!")
173 self.labf = 'lab_2000_3ld_ND2.fits'
174 self.camFit.setup_lab(self.lab_directory, self.labf)
175 #lab_fit = [6.776e+00, 3.333e+01, 2.870e+00, -4.846e-01, 4.571e+00]
176 #self.camFit.set_lab(lab_fit)
177 self.properties['fsm']['state'] = StateCodes.READY.name
178 self.update_property(self.properties['fsm'])
179
180 def handle_state(self, existing_property, new_message):
181 target_list = ['idle', 'SRLoop', 'oneshot']
182 current_state = None
183 for key in target_list:
184 if existing_property[key] == constants.SwitchState.ON:
185 current_state = key
186
187 if current_state not in new_message:
188
189 for key in target_list:
190 existing_property[key] = constants.SwitchState.OFF
191 if key in new_message:
192 existing_property[key] = new_message[key]
193
194 if key == 'idle':
195 self._state = States.IDLE
196 self.properties['fsm']['state'] = StateCodes.READY.name
197 self._command = 0
198 self.log.debug('State changed to idle')
199 elif key == 'SRLoop':
200 self._state = States.CLOSED_LOOP
201 #self.modRadius = self.client['modwfs.modRadius.current'] # TODO: uncomment when modwfs is correct
202 self.properties['fsm']['state'] = StateCodes.OPERATING.name
203 self.log.debug('State changed to continuous')
204 elif key == 'oneshot':
205 self._state = States.ONESHOT
206 #self.modRadius = self.client['modwfs.modRadius.current'] # TODO: uncomment when modwfs is correct
207 self.properties['fsm']['state'] = StateCodes.OPERATING.name
208 self.log.debug('State changed to oneshot')
209
210 self.update_property(existing_property)
211 self.update_property(self.properties['fsm'])
212
213 def handle_n_avg(self, existing_property, new_message):
214 if 'target' in new_message and new_message['target'] != existing_property['current']:
215 existing_property['current'] = new_message['target']
216 existing_property['target'] = new_message['target']
217 self._n_avg = int(new_message['target'])
218 self.update_property(existing_property)
219
220 def handle_n_jitter(self, existing_property, new_message):
221 if 'target' in new_message and new_message['target'] != existing_property['current']:
222 existing_property['current'] = new_message['target']
223 existing_property['target'] = new_message['target']
224 self._n_jitter = int(new_message['target'])
225 self.update_property(existing_property)
226
228 self._command = 0
229 self.log.info(f"Transitioning IDLE")
230 self.properties['state']['oneshot'] = constants.SwitchState.OFF
231 self.properties['state']['SRLoop'] = constants.SwitchState.OFF
232 self.properties['state']['idle'] = constants.SwitchState.ON
233 self.update_property(self.properties['state'])
234 self._state = States.IDLE
235
236 def fit_SR_gauss(self, img):
237 # Set the data in the camtipFitter
238 self.camFit.set_data(img) # background subtracted here
239 #self.log.info(f"Image has been set. ")
240 # Fit the data
241 self.camFit.fit_data()
242 #self.log.info(f"Image has been fit.")
243 # Calculate the SR
244 SR_est_new = self.camFit.calc_SR()
245 self._SR_est_list.append(SR_est_new)
246 #self.log.info(f"SR estimate: {self._SR_est}.")
247 idx = np.min([len(self._SR_est_list), self._n_avg])
248 self._SR_est = np.mean(self._SR_est_list[-idx:])
249 # Set the SR
250 self.properties['SR_est']['current'] = self._SR_est
251 self.update_property(self.properties['SR_est'])
252 #self.log.info(f"SR has been set.")
253 return
254
255 def fit_SR_EE(self, img):
256 # Set the data in the camtipFitter
257 self.camFit.set_data(img) # background subtracted here
258 #self.log.info(f"Image has been set. ")
259 # Calculate the SR
260 SR_EE_new = self.camFit.calc_SR_EE()
261 self._SR_EE_list.append(SR_EE_new)
262 #self.log.info(f"SR EE estimate: {self._SR_EE}.")
263 idx = np.min([len(self._SR_EE_list), self._n_avg])
264 self._SR_EE = np.mean(self._SR_EE_list[-idx:])
265 # Set the SR
266 self.properties['SR_EE']['current'] = self._SR_EE
267 self.update_property(self.properties['SR_EE'])
268 #self.log.info(f"SR EE has been set.")
269 return
270
272 # this needs to be run AFTER SR_EE
273 self._x0_list.append(self.camFit.x0)
274 self._y0_list.append(self.camFit.y0)
275 # need to filter to make sure only the proper amount of the list is used in a calculation
276 idx = np.min([len(self._x0_list), self._n_jitter])
277 x0_all = self._x0_list[-idx:]
278 y0_all = self._y0_list[-idx:]
279 # x and Y are single pixels
280 self._jitter_x = (x0_all[-1] - np.mean(x0_all)) * self.camFit.px_scale
281 self._jitter_y = (y0_all[-1] - np.mean(y0_all)) * self.camFit.px_scale
282 mean_sqr = np.mean(((x0_all - np.mean(x0_all))**2 + (y0_all - np.mean(y0_all))**2))
283 self._jitter_RSM = np.sqrt(mean_sqr)
284 # set the new jitter values
285 self.properties['jitter_RMS']['current'] = self._jitter_RSM
286 self.update_property(self.properties['jitter_RMS'])
287 self.properties['jitter_x']['current'] = self._jitter_x
288 self.update_property(self.properties['jitter_x'])
289 self.properties['jitter_y']['current'] = self._jitter_y
290 self.update_property(self.properties['jitter_y'])
291 return
292
293 def save_ex_img(self, img, name='testframe'):
294 #TODO: need to make this directory if it doesn't already exist?
295 timestamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H%M%S")
296 self.last_image_filename = f"camtipSR_{name}_{timestamp}.fits"
297 outpath = f"{self.data_directory}/{self.last_image_filename}"
298 #self.log.info(f"Saving to {outpath}")
299 try:
300 hdu = fits.PrimaryHDU(data=img)
301 hdul = fits.HDUList([hdu])
302 hdul.writeto(outpath, overwrite=True)
303 self.log.info(f" File has been saved to {outpath}")
304 except Exception:
305 self.log.exception(f" Unable to save frame!")
306 return
307
308 def grab_img(self):
309 # start of any loop will look for files
310 try:
311 img_t = self.camera.grab(subtract_dark=True)
312 self._dark = True
313 except RuntimeError:
314 if self._dark == True:
315 self.log.info("Error finidng files, likely the dark.")
316 img_t = self.camera.grab(subtract_dark=False)
317 self._dark = False
318 img = img_t.shaped # vs. how it would be in a jupyter notebook
319 # enforce cropping
320 if img.shape == (512, 672):
321 #self.log.info(f"Image size is {img.shape}, cropping...")
322 cx, cy = 244, 414
323 m = 64
324 img = img[cx-m:cx+m, cy-m:cy+m]
325 if img.shape != (128, 128):
326 #self.log.error(f"Image size is {img.shape}, expected (128, 128), set proper ROI")
327 self.transition_to_idle()
328 return #TODO: need to figure out how to break the loop here
329 return img
330
331 def grab_stack(self, n):
332 try:
333 img_t = self.camera.grab_stack(n, subtract_dark=True)
334 self._dark = True
335 except RuntimeError as e:
336 if self._dark == True:
337 self.log.warning("Error finding files, likely the dark.")
338 img_t = self.camera.grab_stack(n, subtract_dark=False)
339 self._dark = False
340 img = img_t.shaped # vs. how it would be in a jupyter notebook
341 # enforce cropping
342 if img.shape == (512, 672):
343 #self.log.info(f"Image size is {img.shape}, cropping...")
344 cx, cy = 244, 414
345 m = 64
346 img = img[cx-m:cx+m, cy-m:cy+m]
347 if img.shape != (128, 128):
348 #self.log.error(f"Image size is {img.shape}, expected (128, 128), set proper ROI")
349 self.transition_to_idle()
350 return #TODO: need to figure out how to break the loop here
351 return img
352
353 def loop(self):
354
355 if self._state == States.CLOSED_LOOP:
356 img = self.grab_img()
357 ## CALL FIT FUNCTION
358 self.fit_SR_gauss(img)
359 self.fit_SR_EE(img)
360 self.calc_jitter_RMS()
361 # will then continue to loop
362
363 elif self._state == States.ONESHOT:
364 img = self.grab_img()
365 ## CALL FIT FUNCTION
366 self.fit_SR_gauss(img)
367 self.fit_SR_EE(img)
368 self.calc_jitter_RMS()
369 # check to see the image
370 self.save_ex_img(self.camFit.data_bg_sub)
371 # will now exit out
372 self.transition_to_idle()
373 return
camtipSRConfig config
Definition core.py:44
handle_state(self, existing_property, new_message)
Definition core.py:180
grab_stack(self, n)
Definition core.py:331
fit_SR_gauss(self, img)
Definition core.py:236
save_ex_img(self, img, name='testframe')
Definition core.py:293
handle_n_jitter(self, existing_property, new_message)
Definition core.py:220
transition_to_idle(self)
Definition core.py:227
handle_n_avg(self, existing_property, new_message)
Definition core.py:213
_state
CALL FIT FUNCTION.
Definition core.py:147
fit_SR_EE(self, img)
Definition core.py:255