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 for key in target_list:
183 if existing_property[key] == constants.SwitchState.ON:
184 current_state = key
185
186 if current_state not in new_message:
187
188 for key in target_list:
189 existing_property[key] = constants.SwitchState.OFF
190 if key in new_message:
191 existing_property[key] = new_message[key]
192
193 if key == 'idle':
194 self._state = States.IDLE
195 self.properties['fsm']['state'] = StateCodes.READY.name
196 self._command = 0
197 self.log.debug('State changed to idle')
198 elif key == 'SRLoop':
199 self._state = States.CLOSED_LOOP
200 #self.modRadius = self.client['modwfs.modRadius.current'] # TODO: uncomment when modwfs is correct
201 self.properties['fsm']['state'] = StateCodes.OPERATING.name
202 self.log.debug('State changed to continuous')
203 elif key == 'oneshot':
204 self._state = States.ONESHOT
205 #self.modRadius = self.client['modwfs.modRadius.current'] # TODO: uncomment when modwfs is correct
206 self.properties['fsm']['state'] = StateCodes.OPERATING.name
207 self.log.debug('State changed to oneshot')
208
209 self.update_property(existing_property)
210 self.update_property(self.properties['fsm'])
211
212 def handle_n_avg(self, existing_property, new_message):
213 if 'target' in new_message and new_message['target'] != existing_property['current']:
214 existing_property['current'] = new_message['target']
215 existing_property['target'] = new_message['target']
216 self._n_avg = int(new_message['target'])
217 self.update_property(existing_property)
218
219 def handle_n_jitter(self, existing_property, new_message):
220 if 'target' in new_message and new_message['target'] != existing_property['current']:
221 existing_property['current'] = new_message['target']
222 existing_property['target'] = new_message['target']
223 self._n_jitter = int(new_message['target'])
224 self.update_property(existing_property)
225
227 self._command = 0
228 self.log.info(f"Transitioning IDLE")
229 self.properties['state']['oneshot'] = constants.SwitchState.OFF
230 self.properties['state']['SRLoop'] = constants.SwitchState.OFF
231 self.properties['state']['idle'] = constants.SwitchState.ON
232 self.update_property(self.properties['state'])
233 self._state = States.IDLE
234
235 def fit_SR_gauss(self, img):
236 # Set the data in the camtipFitter
237 self.camFit.set_data(img) # background subtracted here
238 #self.log.info(f"Image has been set. ")
239 # Fit the data
240 self.camFit.fit_data()
241 #self.log.info(f"Image has been fit.")
242 # Calculate the SR
243 SR_est_new = self.camFit.calc_SR()
244 self._SR_est_list.append(SR_est_new)
245 #self.log.info(f"SR estimate: {self._SR_est}.")
246 idx = np.min([len(self._SR_est_list), self._n_avg])
247 self._SR_est = np.mean(self._SR_est_list[-idx:])
248 # Set the SR
249 self.properties['SR_est']['current'] = self._SR_est
250 self.update_property(self.properties['SR_est'])
251 #self.log.info(f"SR has been set.")
252 return
253
254 def fit_SR_EE(self, img):
255 # Set the data in the camtipFitter
256 self.camFit.set_data(img) # background subtracted here
257 #self.log.info(f"Image has been set. ")
258 # Calculate the SR
259 SR_EE_new = self.camFit.calc_SR_EE()
260 self._SR_EE_list.append(SR_EE_new)
261 #self.log.info(f"SR EE estimate: {self._SR_EE}.")
262 idx = np.min([len(self._SR_EE_list), self._n_avg])
263 self._SR_EE = np.mean(self._SR_EE_list[-idx:])
264 # Set the SR
265 self.properties['SR_EE']['current'] = self._SR_EE
266 self.update_property(self.properties['SR_EE'])
267 #self.log.info(f"SR EE has been set.")
268 return
269
271 # this needs to be run AFTER SR_EE
272 self._x0_list.append(self.camFit.x0)
273 self._y0_list.append(self.camFit.y0)
274 # need to filter to make sure only the proper amount of the list is used in a calculation
275 idx = np.min([len(self._x0_list), self._n_jitter])
276 x0_all = self._x0_list[-idx:]
277 y0_all = self._y0_list[-idx:]
278 # x and Y are single pixels
279 self._jitter_x = (x0_all[-1] - np.mean(x0_all)) * self.camFit.px_scale
280 self._jitter_y = (y0_all[-1] - np.mean(y0_all)) * self.camFit.px_scale
281 mean_sqr = np.mean(((x0_all - np.mean(x0_all))**2 + (y0_all - np.mean(y0_all))**2))
282 self._jitter_RSM = np.sqrt(mean_sqr)
283 # set the new jitter values
284 self.properties['jitter_RMS']['current'] = self._jitter_RSM
285 self.update_property(self.properties['jitter_RMS'])
286 self.properties['jitter_x']['current'] = self._jitter_x
287 self.update_property(self.properties['jitter_x'])
288 self.properties['jitter_y']['current'] = self._jitter_y
289 self.update_property(self.properties['jitter_y'])
290 return
291
292 def save_ex_img(self, img, name='testframe'):
293 #TODO: need to make this directory if it doesn't already exist?
294 timestamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H%M%S")
295 self.last_image_filename = f"camtipSR_{name}_{timestamp}.fits"
296 outpath = f"{self.data_directory}/{self.last_image_filename}"
297 #self.log.info(f"Saving to {outpath}")
298 try:
299 hdu = fits.PrimaryHDU(data=img)
300 hdul = fits.HDUList([hdu])
301 hdul.writeto(outpath, overwrite=True)
302 self.log.info(f" File has been saved to {outpath}")
303 except Exception:
304 self.log.exception(f" Unable to save frame!")
305 return
306
307 def grab_img(self):
308 # start of any loop will look for files
309 try:
310 img_t = self.camera.grab_stack(1, subtract_dark=True) # stack is already averaged
311 self._dark = True
312 except:
313 if self._dark == True:
314 self.log.info("Error finidng files, likely the dark.")
315 img_t = self.camera.grab_stack(1, subtract_dark=False) # stack is already averaged
316 self._dark = False
317 img = img_t.shaped # vs. how it would be in a jupyter notebook
318 # enforce cropping
319 if img.shape == (512, 672):
320 #self.log.info(f"Image size is {img.shape}, cropping...")
321 cx, cy = 244, 414
322 m = 64
323 img = img[cx-m:cx+m, cy-m:cy+m]
324 if img.shape != (128, 128):
325 #self.log.error(f"Image size is {img.shape}, expected (128, 128), set proper ROI")
326 self.transition_to_idle()
327 return #TODO: need to figure out how to break the loop here
328 return img
329
330 def grab_stack(self, n):
331 try:
332 img_t = self.camera.grab_stack(n, subtract_dark=True) # stack is already averaged
333 self._dark = True
334 except:
335 if self._dark == True:
336 self.log.info("Error finidng files, likely the dark.")
337 img_t = self.camera.grab_stack(n, subtract_dark=False) # stack is already averaged
338 self._dark = False
339 img = img_t.shaped # vs. how it would be in a jupyter notebook
340 # enforce cropping
341 if img.shape == (512, 672):
342 #self.log.info(f"Image size is {img.shape}, cropping...")
343 cx, cy = 244, 414
344 m = 64
345 img = img[cx-m:cx+m, cy-m:cy+m]
346 if img.shape != (128, 128):
347 #self.log.error(f"Image size is {img.shape}, expected (128, 128), set proper ROI")
348 self.transition_to_idle()
349 return #TODO: need to figure out how to break the loop here
350 return img
351
352 def loop(self):
353
354 if self._state == States.CLOSED_LOOP:
355 img = self.grab_img()
356 ## CALL FIT FUNCTION
357 self.fit_SR_gauss(img)
358 self.fit_SR_EE(img)
359 self.calc_jitter_RMS()
360 # will then continue to loop
361
362 elif self._state == States.ONESHOT:
363 img = self.grab_img()
364 ## CALL FIT FUNCTION
365 self.fit_SR_gauss(img)
366 self.fit_SR_EE(img)
367 self.calc_jitter_RMS()
368 # check to see the image
369 self.save_ex_img(self.camFit.data_bg_sub)
370 # will now exit out
371 self.transition_to_idle()
372 return
373
374
375
376
377
378
camtipSRConfig config
Definition core.py:44
handle_state(self, existing_property, new_message)
Definition core.py:180
grab_stack(self, n)
Definition core.py:330
fit_SR_gauss(self, img)
Definition core.py:235
save_ex_img(self, img, name='testframe')
Definition core.py:292
handle_n_jitter(self, existing_property, new_message)
Definition core.py:219
transition_to_idle(self)
Definition core.py:226
handle_n_avg(self, existing_property, new_message)
Definition core.py:212
_state
CALL FIT FUNCTION.
Definition core.py:147
fit_SR_EE(self, img)
Definition core.py:254