You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

_core.py 46KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966
  1. import time as t
  2. import os, pickle
  3. import numpy as np
  4. import shutil as sh
  5. from pigpio import OUTPUT
  6. import threading, warnings
  7. from pigpio import pi as dev
  8. from w1thermsensor import W1ThermSensor as W1
  9. from picamera import PiCamera as imdev
  10. from ._treeparser import GenerateTree
  11. from . import _dataparser as dp
  12. import cv2
  13. # Default buffer size in bytes
  14. BUFFER_SIZE = 64
  15. # Temperature sampling frequency in seconds.
  16. TEMPERATURE_SAMPLING_FREQ = 3.0
  17. TEMPERATURE_TOLERANCE = 1.0
  18. # Location of hardware settings:
  19. hw_fName = "/hwsettings.hws"
  20. Path = os.getcwd() + "/.srv"
  21. # Load settings defined by in GUI, otherwise use defaults:
  22. if os.path.exists(Path + hw_fName):
  23. hwSettings = pickle.load(open(Path + hw_fName, "rb"))
  24. # Set PINs (MODE == BCM).
  25. GPIO_PIN_RGB = hwSettings["GPIO_PIN_RGB"] # RGB mode, simulated white Ws = R + G + B.
  26. GPIO_PIN_M = hwSettings["GPIO_PIN_M"] # Monocolour mode (white).
  27. FILTER_PIN = hwSettings["FILTER_PIN"] # Servo holding filters.
  28. HEATER_PIN = hwSettings["HEATER_PIN"] # Heating unit.
  29. __FILTER_PWM__ = hwSettings["filterSet"] # Filter locations
  30. # Default camera settings
  31. __SHUTTER_SPEED__ = hwSettings["SHUTTER_SPEED"] # Shutter speed time in SECONDS. Defaults to 0.25s
  32. DEFAULT_ISO = hwSettings["DEFAULT_ISO"]
  33. ISO_F1 = hwSettings["ISO_F1"]
  34. ISO_F2 = hwSettings["ISO_F2"]
  35. ISO_F3 = hwSettings["ISO_F3"]
  36. # Light frequencies
  37. DEFAULT_LIGHT_FREQ = hwSettings["DEFAULT_LIGHT_FREQ"]
  38. LIGHT_FREQ_F1 = hwSettings["LIGHT_FREQ_F1"]
  39. LIGHT_FREQ_F2 = hwSettings["LIGHT_FREQ_F2"]
  40. LIGHT_FREQ_F3 = hwSettings["LIGHT_FREQ_F3"]
  41. # Replicates
  42. __REPLICATES__ = hwSettings["cameraRepetitions"]
  43. else:
  44. # Set PINs (MODE == BCM).
  45. GPIO_PIN_RGB = [17, 27, 22] # RGB mode, simulated white Ws = R + G + B.
  46. GPIO_PIN_M = 17 # Monocolour mode (white).
  47. FILTER_PIN = 3 # Servo holding filters.
  48. HEATER_PIN = 10 # Heating unit.
  49. # Filter locations. All set to 1460 µS to prevent possible damages to the
  50. # servo, and _force_ the user to set it up.
  51. __FILTER_PWM__ = dict()
  52. __FILTER_PWM__['Filter_1'] = 1460 # 475nm band-pass.
  53. __FILTER_PWM__['Filter_2'] = 1460 # 520nm band-pass (centre).
  54. __FILTER_PWM__['Filter_3'] = 1460 # 590 long-pass + 610nm short-pass.
  55. __FILTER_PWM__['No_Filter'] = 1460 # No filter (RGB image).
  56. # Default camera settings
  57. __SHUTTER_SPEED__ = 0.25 # Shutter speed time in SECONDS. Defaults to 0.25s
  58. DEFAULT_ISO = 10
  59. ISO_F1 = 100
  60. ISO_F2 = 100
  61. ISO_F3 = 400
  62. # Light frequencies
  63. DEFAULT_LIGHT_FREQ = 20000
  64. LIGHT_FREQ_F1 = 200
  65. LIGHT_FREQ_F2 = 200
  66. LIGHT_FREQ_F3 = 200
  67. # Replicates
  68. __REPLICATES__ = 3
  69. def _img_settings(Camera, Filter=None):
  70. """
  71. Define camera settings. Auto-exposure or auto-white-balance result
  72. in aleatory camera settings depending upon light conditions. _WE DO
  73. NOT WANT THIS_. Turn them off and fix the camera values, making the
  74. images independent from light conditions.\n
  75. The default camera resolution in imdev V2 is HD720p (1280x720), but
  76. it is not able to capture the plate (partial sensor area). The
  77. smallest resolution using full sensor area is 1640x922, which offers
  78. a good compromise between quality (data) and speed. HD1080p
  79. resolution takes longer to acquire and process, and it also is
  80. unable to capture a full microplate due to a different partial
  81. sensor area used. Information about these limitations can be found
  82. at https://imdev.readthedocs.io/en/release-1.13/fov.html#sensor-modes
  83. """
  84. Camera.exposure_mode = 'auto' # Unlock Settings.
  85. Camera.hflip = True # Ensures well A1 is in the bottom-left.
  86. Camera.awb_mode = 'off'
  87. Camera.awb_gains = (1.45, 1.45)
  88. SHUTTER_SPEED = __SHUTTER_SPEED__
  89. if Filter == "Filter_1":
  90. Camera.iso = ISO_F1
  91. elif Filter == "Filter_2":
  92. SHUTTER_SPEED = 5
  93. Camera.iso = ISO_F2
  94. elif Filter == "Filter_3":
  95. Camera.iso = ISO_F3
  96. else:
  97. Camera.iso = DEFAULT_ISO
  98. Camera.shutter_speed = int(SHUTTER_SPEED * 10**6) # Translate SHUTTER_SPEED to µs.
  99. # max_res = Camera.MAX_RESOLUTION # How big can the image be?
  100. # Camera.resolution = (int(max_res[0]), int(max_res[1]))
  101. Camera.resolution = (1640, 1232) # Smallest resolution with full FoV (partial FoV results in plate not fully captured).
  102. # imdev v2 has issues with 'exposure_mode = off' causing black img.
  103. t.sleep(0.1) # Solves those issues (WTF?) as long as it's _BEFORE_ exposure_mode.
  104. Camera.exposure_mode = 'off' # Lock Settings.
  105. return Camera
  106. def img_acquisition_routine(self, light_range, wavelength, photo_path,
  107. file_name, flag=False, calibrate=False):
  108. """
  109. Main image acquisition routine.\n
  110. This method returns a generator that screens through `light_range` and
  111. changes the LED intensity accordingly for each `wavelength`. A picture
  112. is taken at every iteration. The resulting `file_name` is a composite
  113. name, that includes metadata such as time, that here complemented by
  114. adding the corresponding `light_intensity` value (i.e. `Read_0s_255`).
  115. This image is stored locally in `photo_path`.
  116. `flag=False`:
  117. Additionaly, an extra picture is saved if the highest light intensity is
  118. used or else, `flag=True`. Defaults to FALSE.\n
  119. `calibrate=False':
  120. The flag `calibrate' generates images at all intensities and filters
  121. that are stored locally in the device, used for blank correction in the
  122. case data is processed locally. Defaults to FALSE.
  123. """
  124. for intensity in light_range:
  125. if calibrate is True:
  126. self._modulate_LED_intensity(intensity, channel=wavelength)
  127. t.sleep(0.5) # Get's rid of French flag issue... why?
  128. yield photo_path + file_name +\
  129. str(intensity).zfill(3) + self._pic_ext
  130. else:
  131. self._device._modulate_LED_intensity(intensity, channel=wavelength)
  132. t.sleep(0.5) # Get's rid of French flag issue... why?
  133. yield photo_path + file_name +\
  134. str(intensity).zfill(3) + self._pic_ext
  135. if intensity == 255 or flag is True:
  136. yield photo_path + 'findWells' + self._pic_ext
  137. # When finished, switch OFF
  138. if calibrate is True:
  139. self._modulate_LED_intensity(0, channel=wavelength)
  140. else:
  141. self._device._modulate_LED_intensity(0, channel=wavelength)
  142. def img_acquisition_routine_replicates(self, light_intensity, wavelength,
  143. photo_path, file_name, flag=False,
  144. Replicates=__REPLICATES__):
  145. """
  146. # FIX: CHANGE DOC.
  147. # Main image acquisition routine.\n
  148. # This method returns a generator that screens through `light_range` and
  149. # changes the LED intensity accordingly for each `wavelength`. A picture
  150. # is taken at every iteration. The resulting `file_name` is a composite
  151. # name, that includes metadata such as time, that here complemented by
  152. # adding the corresponding `light_intensity` value (i.e. `Read_0s_255`).
  153. # This image is stored locally in `photo_path`.
  154. # `flag=False`:
  155. # Additionaly, an extra picture is saved if the highest light intensity is
  156. # used or else, `flag=True`. Defaults to FALSE.\n
  157. # `calibrate=False':
  158. # The flag `calibrate' generates images at all intensities and filters
  159. # that are stored locally in the device, used for blank correction in the
  160. # case data is processed locally. Defaults to FALSE.
  161. """
  162. for r in range(Replicates):
  163. self._device._modulate_LED_intensity(light_intensity, channel=wavelength)
  164. # if wavelength == 1:
  165. # t.sleep(1)
  166. # else:
  167. # t.sleep(0.1) # Get's rid of French flag issue... why?
  168. t.sleep(__SHUTTER_SPEED__)
  169. yield photo_path + file_name + str(r) + self._pic_ext
  170. if light_intensity == 255 or flag is True:
  171. yield photo_path + "findWells" + self._pic_ext
  172. # When finished, switch OFF
  173. self._device._modulate_LED_intensity(0, channel=wavelength)
  174. def _average_image(self, photo_path, Replicate_name, Replicates=__REPLICATES__):
  175. """
  176. Load the relicate
  177. """
  178. for r in range(Replicates):
  179. file_name = Replicate_name + str(r) + self._pic_ext
  180. r_img = cv2.imread(photo_path + file_name)
  181. if r == 0:
  182. img_container = np.zeros_like(r_img.astype('int')) # WORKAROUND: np.mean wraps up uint8, so 256 becomes 0.
  183. img_container += r_img.astype('int')
  184. os.remove(photo_path + file_name) # Removes physical copy.
  185. del r_img, file_name # Free up memory.
  186. return np.uint8(img_container / Replicates)
  187. def _get_response(Connection, BUFFER_SIZE):
  188. return Connection.recv(BUFFER_SIZE)
  189. class SetupDevice:
  190. """
  191. Initialise all components and allows the instrument to be programmed.
  192. and the components are initialised in an OFF state.\n
  193. 'panel_type' defines the type of light panel depending on the
  194. availability of wavelengths. Possible values are 'rgb', if the light
  195. panel provides multiple (3) wavelengths, or 'white' if only one is
  196. available.\n
  197. 'default_filter' defines the filter set during the instrument's set up.
  198. It defaults to 'No_Filter'.
  199. """
  200. def set_filter(self, selected_filter, current_pwm=0):
  201. """
  202. Rotate the servo to position defined by dictionary
  203. 'selected_filter'. PWM's in `selected_filter' have been set to
  204. centre the filter with respect to the camera, facilitating the
  205. downstream pipeline.\n
  206. To avoid wear of the servo, the program remembers its last PWM
  207. and only modulates the servo's locartion if the new PWM is
  208. different from its past position.
  209. """
  210. # Define servo pulsewidth for each filter position.
  211. # __FILTER_PWM__ = dict()
  212. # if len(self._GPIO_PIN) == 1:
  213. # __FILTER_PWM__['No_Filter'] = 0 # Monowavelength contains no servo (yet?).
  214. # elif len(self._GPIO_PIN) == 3:
  215. # __FILTER_PWM__['Filter_1'] = 1775 # 475nm band-pass.
  216. # __FILTER_PWM__['Filter_2'] = 1460 # 520nm band-pass (centre).
  217. # __FILTER_PWM__['Filter_3'] = 1130 # 590 long-pass + 610nm short-pass.
  218. # __FILTER_PWM__['No_Filter'] = 850 # No filter (RGB image).
  219. # else:
  220. # raise ValueError("Channels must be 1 < chnl < 3, but ",
  221. # str(len(self._GPIO_PIN)), " were specified.")
  222. # Define servo pulsewidth for each filter position.
  223. if len(self._GPIO_PIN) == 1:
  224. FILTER_PWM = dict()
  225. FILTER_PWM['No_Filter'] = 0 # Monowavelength contains no servo (yet?).
  226. else:
  227. FILTER_PWM = __FILTER_PWM__
  228. # Set filter.
  229. if selected_filter != "expose_filters": # Helps with calibration.
  230. new_pwm = __FILTER_PWM__[selected_filter]
  231. if new_pwm != current_pwm:
  232. self._servo.set_servo_pulsewidth(self._FILTER_PIN, new_pwm) # Adjust.
  233. t.sleep(1) # Gives the servo time to move....
  234. self._servo.set_PWM_dutycycle(self._FILTER_PIN, 0) # Turn back OFF.
  235. current_pwm = new_pwm
  236. return current_pwm
  237. elif selected_filter == "expose_filters":
  238. return sorted(FILTER_PWM)
  239. return 0
  240. def _set_temperature(self, current_temperature, target_temperature,
  241. TEMPERATURE_TOLERANCE, TEMPERATURE_SAMPLING_FREQ,
  242. stop_flag):
  243. """
  244. Monitor temperature to make sure it remains as set by the target
  245. temperature. The method is run as a different thread and therefore
  246. allows the rest of the program to carry on whilst monitoring the
  247. temperature.\n
  248. The mechanism whereby temperature is regulate is simple: turn heater
  249. unit ON or OFF whenever the average temperature is below or above
  250. `target_temperature'.\n
  251. TODO: Implement a more sophisticated algorithm.
  252. """
  253. current_temperature = np.array(current_temperature)
  254. # Run forever (whilst the protocol is running).
  255. MAX_POWER = 255
  256. while True:
  257. if stop_flag.is_set():
  258. break
  259. # If current_temperature < target_temperature, heat up.
  260. if current_temperature.mean() < target_temperature - TEMPERATURE_TOLERANCE:
  261. self._heater.set_PWM_dutycycle(self._HEATER_PIN, MAX_POWER)
  262. elif target_temperature - TEMPERATURE_TOLERANCE <= current_temperature.mean() < target_temperature:
  263. self._heater.set_PWM_dutycycle(self._HEATER_PIN, int(MAX_POWER*0.7))
  264. else:
  265. self._heater.set_PWM_dutycycle(self._HEATER_PIN, 0)
  266. temperature = self.report_temperature()
  267. current_temperature = np.array(temperature)
  268. t.sleep(1+TEMPERATURE_SAMPLING_FREQ)
  269. # If stop_flag.is_set()... stop heating.
  270. self._heater.write(self._HEATER_PIN, 0)
  271. return 0
  272. def _get_temperatures(self, sensor_id, report):
  273. """ Retrieve temperature information from sensor_id and include it in
  274. the report. """
  275. return report.append(self._sensors[sensor_id].get_temperature())
  276. def report_temperature(self):
  277. """ Read temperature from all sensors simultaneously (parallel) and
  278. report it in degrees Celsius. """
  279. report = list()
  280. thread_list = list()
  281. # Avoid 1s delay "per read" hardcoded in w1 library by running
  282. # a thread per sensor.
  283. for sensor_id in self._sensors.keys():
  284. process = threading.Thread(target=self._get_temperatures,
  285. args=(sensor_id, report))
  286. process.start()
  287. thread_list.append(process)
  288. # Once data retrieved, halt threads.
  289. for process in thread_list:
  290. process.join()
  291. return np.array(report)
  292. def _init_device(self, dev, OUTPUT, imdev, default_filter):
  293. """
  294. Initialise devices `dev' and `imdev'. GPIO_PIN is set up
  295. automatically depending on panel_type. Servo defaults to PWM given
  296. by `default_filter'.
  297. """
  298. err_list = list()
  299. status_list = list()
  300. # Light Panel
  301. try:
  302. light_panel = dev()
  303. if len(self._GPIO_PIN) == 1:
  304. light_panel.set_mode(self._GPIO_PIN[0], OUTPUT)
  305. light_panel.set_PWM_frequency(self._GPIO_PIN[0], DEFAULT_LIGHT_FREQ) # Hz (20KHz max with -s 2).
  306. light_panel.write(self._GPIO_PIN[0], 0) # Init off state.
  307. elif len(self._GPIO_PIN) == 3:
  308. for pin in self._GPIO_PIN:
  309. light_panel.set_mode(pin, OUTPUT)
  310. light_panel.set_PWM_frequency(pin, DEFAULT_LIGHT_FREQ) # Hz (20KHz max with -s 2).
  311. # light_panel.write(pin, 0) # Init off state.
  312. else:
  313. raise ValueError("Channels must be 1 < chnl < 3, but ",
  314. str(len(self._GPIO_PIN)), " were specified.")
  315. log = str("Panel... [OK]")
  316. status = str("OK")
  317. except:
  318. log = str("Panel... [FAILED]")
  319. status = str("FAILED")
  320. err_list.append(log)
  321. status_list.append(tuple(["Panel:", status]))
  322. print(log)
  323. # Heater
  324. try:
  325. heater = dev()
  326. heater.set_mode(self._HEATER_PIN, OUTPUT)
  327. heater.write(self._HEATER_PIN, 0) # Init off state.
  328. log = str("Heater... [OK]")
  329. status = str("OK")
  330. except:
  331. log = str("Heater... [FAILED]")
  332. status = str("FAILED")
  333. err_list.append(log)
  334. status_list.append(tuple(["Heater:", status]))
  335. print(log)
  336. # Sensors
  337. try:
  338. w1_init = W1()
  339. w1_init.RETRY_DELAY_SECONDS = 0.0
  340. w1_init.RETRY_ATTEMPTS = 3
  341. n_sensors = len(w1_init.get_available_sensors())
  342. if n_sensors == 0:
  343. log = str("No sensors found.")
  344. status = str("None")
  345. else:
  346. # Report sensors.
  347. log = str(str(n_sensors) + " sensors " +
  348. w1_init.type_name + " detected.")
  349. status = str("OK")
  350. # Aggregate into one dictionary.
  351. sensors = dict()
  352. for s in range(n_sensors):
  353. exec("sensors['sensor" + str(s) +
  354. "'] = w1_init.get_available_sensors()[s]")
  355. except:
  356. log = str("Sensors... [FAILED]")
  357. status = str("FAILED")
  358. err_list.append(log)
  359. sensors = dict()
  360. # Report sensors found.
  361. status_list.append(tuple(["Sensors:", status]))
  362. print(log)
  363. # Camera
  364. try:
  365. camera = imdev()
  366. camera = _img_settings(camera) # Set camera defaults.
  367. log = str("Camera... [OK]")
  368. status = str("OK")
  369. except:
  370. log = str("Camera... [FAILED]")
  371. status = str("FAILED")
  372. err_list.append(log)
  373. status_list.append(tuple(["Camera:", status]))
  374. print(log)
  375. # Filters
  376. try:
  377. servo = dev()
  378. servo.set_mode(self._FILTER_PIN, OUTPUT) # BCM mode!
  379. servo.set_PWM_frequency(self._FILTER_PIN, 50) # Set servo at 50Hz (**hardware constrain**)
  380. servo.set_PWM_dutycycle(self._FILTER_PIN, 0) # Init servo in an OFF state (**AVOID USING WRITE.(PIN, 0)**)
  381. log = str("Servo ready... [OK]")
  382. status = str("OK")
  383. except:
  384. log = str("Servo ready... [FAILED]")
  385. status = str("FAILED")
  386. err_list.append(log)
  387. status_list.append(tuple(["Servo:", status]))
  388. # Device status
  389. print("The device has been initialised with " +
  390. str(len(err_list)) + " errors.\n")
  391. return light_panel, heater, sensors,\
  392. camera, servo, err_list, status_list
  393. def terminate_device(self, Connection, ConnectionStatus):
  394. """
  395. Safe release of the hardware. Failure to do so will require the
  396. instrument to be restarted and release the resources needed to
  397. control it.
  398. """
  399. # Light Panel
  400. if len(self._GPIO_PIN) == 1:
  401. self._light_panel.write(self._GPIO_PIN[0], 0)
  402. elif len(self._GPIO_PIN) == 3:
  403. for PIN in self._GPIO_PIN:
  404. self._light_panel.write(PIN, 0)
  405. else:
  406. raise ValueError("Channels must be 1 < chnl < 3, but ",
  407. str(len(self._GPIO_PIN)), " were specified.")
  408. self._light_panel.stop()
  409. # Heater
  410. self._heater.stop()
  411. # Sensors ? can they be stopped?
  412. # Camera
  413. self._camera.close()
  414. # Filters
  415. self._servo.stop()
  416. # Device status
  417. log = str("The device has been shut down with " +
  418. str(len(self._err_list)) + " errors.\n")
  419. if len(self._err_list) == 0:
  420. status = "READY"
  421. else:
  422. status = "FAILED"
  423. # Updata status
  424. self._status_list.append(tuple(["Status:", status]))
  425. print(log)
  426. # Report to GUI
  427. if ConnectionStatus:
  428. t.sleep(0.1) # Prevents mixing msg. TODO: Fix.
  429. pSize = len(pickle.dumps(self._status_list))
  430. Connection.sendall(str(pSize).encode()) # Send size of pickle.
  431. cli = _get_response(Connection, BUFFER_SIZE)
  432. if cli.decode() == "Acknowledged":
  433. Connection.sendall(pickle.dumps(self._status_list))
  434. def _modulate_LED_intensity(self, light_intensity, channel=None):
  435. """
  436. Change LED intensity for a given filter (channel). 'filter' is a
  437. function from python's core library, hence the use of 'channel'
  438. instead. Light panel is switched off prior to changing the filter
  439. to avoid the overlap of multiple wavelengths (LEDs). This step is
  440. fast enough to pass unnotices) *** NOT IT DOES NOT!!! IT
  441. IS NOTICEABLE!!!. STEP REMOVED ***\n
  442. In case of an RGB panel, the white light is emulated by reducing
  443. the green LED by 40% (visually checked).\n
  444. GPIO settings for 'rgb' plate_type : R (600nm, 0), G (GFP/YFP, 1)
  445. and B (CFP, 2).
  446. """
  447. if channel is None:
  448. # Single LED light panel, only one wavelength.
  449. self._light_panel.set_PWM_dutycycle(self._GPIO_PIN[0],
  450. light_intensity)
  451. else:
  452. if channel == 0: # CFP
  453. self._light_panel.set_PWM_dutycycle(self._GPIO_PIN[2],
  454. light_intensity)
  455. elif channel == 1: # GFP/YFP
  456. if light_intensity > 200:
  457. light_intensity = 200 # Dutycycle is 255 means permanently ON, regardless of frequency.
  458. self._light_panel.set_PWM_dutycycle(self._GPIO_PIN[1],
  459. light_intensity)
  460. elif channel == 2: # OD
  461. self._light_panel.set_PWM_dutycycle(self._GPIO_PIN[0],
  462. light_intensity)
  463. elif channel == 3:
  464. # Simulated white light based on RGB LEDs
  465. self._light_panel.set_PWM_dutycycle(self._GPIO_PIN[0],
  466. light_intensity)
  467. self._light_panel.set_PWM_dutycycle(self._GPIO_PIN[1],
  468. light_intensity)
  469. self._light_panel.set_PWM_dutycycle(self._GPIO_PIN[2],
  470. light_intensity)
  471. ### def calibrate(self): # TODO: include independent wavelengths.
  472. ### """
  473. ### Calibrate device (ABSENCE OF PLATE _REQUIRED_). Record the light
  474. ### panel at different light intensities using all available filters.
  475. ### This will generate an array of images that will later be use to
  476. ### blank correct the data.
  477. ### """
  478. ### light_range = np.arange(0, 256, 1)
  479. ### self._dir_info = GenerateTree(calibrate=True, filters=self._filter_set) # TODO: change how calibration is detected.
  480. ### if self._dir_info._argout == 0: # 0 = tree created, 1 = already exists.
  481. ### # Begin calibration routine
  482. ### for flt in self._filter_set:
  483. ### self._current_state = self.set_filter(flt)
  484. ### photo_path = self._dir_info.root_path +\
  485. ### self._dir_info.ref_path + flt + "/"
  486. ### # This method optimises photography time by 1) using JPEG
  487. ### # hardware acceleration, 2) avoiding the overheads of
  488. ### # initialising cameras' still pipeline (init still port +
  489. ### # encoder for every picture) and 3) eliminating overheads of
  490. ### # initialising preview port. use_video_port can accelerate this
  491. ### # further, but at the expense of a lower quality image.
  492. ### self._camera.capture_sequence(img_acquisition_routine(self,
  493. ### light_range,
  494. ### flt,
  495. ### photo_path,
  496. ### self._pic_file_name,
  497. ### flag=False,
  498. ### calibrate=True),
  499. ### format='jpeg', burst=True,
  500. ### use_video_port=False, quality=90)
  501. ### return self
  502. def __init__(self, panel_type="rgb", default_filter="No_Filter",
  503. connection=None):
  504. """
  505. Initialise the class SetupDevice. """
  506. self._pic_file_name = 'Calibration_Int_'
  507. self._pic_ext = '.jpg' # JPEG is lossy but hardware-accelerated.
  508. # Initialise box and filter wheel
  509. if panel_type.lower() == "white":
  510. self._GPIO_PIN = list([GPIO_PIN_M]) # `PIN = int' is not iterable.
  511. elif panel_type.lower() == "rgb":
  512. self._GPIO_PIN = GPIO_PIN_RGB
  513. else:
  514. raise ValueError("Panel type not recognised.")
  515. self._FILTER_PIN = FILTER_PIN
  516. self._HEATER_PIN = HEATER_PIN
  517. # Initialise hardware.
  518. self._light_panel, self._heater,\
  519. self._sensors, self._camera,\
  520. self._servo, self._err_list,\
  521. self._status_list = self._init_device(dev, OUTPUT, imdev,
  522. default_filter)
  523. # Default filter location.
  524. self._current_state = [] # Avoids moving the servo unnecessarily. PREV: self.set_filter(default_filter)
  525. # Available filters.
  526. self._filter_set = self.set_filter("expose_filters")
  527. class SetProtocol:
  528. """
  529. This class allows to define the steps needed to perform a complete
  530. laboratory assay. It requires a device, filter set, microtitre plate
  531. type and a protocol name. The information is channeled by the Run()
  532. program which is public, meaning the user will interface with Run() to
  533. initiate the assay.
  534. """
  535. def _data_acquisition(self, light_range, time, flt, wavelength, singleRead):
  536. """ Takes a picture at a given light_intensity. Files are named based
  537. on light intensity, time and filter used. """
  538. photo_path = self._dir_info.root_path + self._dir_info.protocol_path +\
  539. self._dir_info.img_data_path + flt + "/"
  540. # If no custom names provided, use a preset name.
  541. if self._notes == '':
  542. file_name = self._pic_file_name + str(time) + "s_"
  543. else:
  544. file_name = self._pic_file_name + str(time) + "s_" + self._notes
  545. # This method optimises photography time by 1) using JPEG hardware
  546. # acceleration, 2) avoiding the overheads of initialising cameras'
  547. # still pipeline (init still port + encoder for every picture) and 3)
  548. # eliminating overheads of initialising preview port. use_video_port
  549. # can accelerate this further, but at the expense of a lower quality
  550. # image.
  551. self._device._camera.capture_sequence(img_acquisition_routine(self,
  552. light_range,
  553. wavelength,
  554. photo_path,
  555. file_name,
  556. flag=singleRead),
  557. format='jpeg', burst=True,
  558. use_video_port=False, quality=90)
  559. return 0
  560. def _data_acquisition_triplicate(self, light_range, time, flt, wavelength,
  561. singleRead):
  562. """ Takes a picture at a given light_intensity. Files are named based
  563. on light intensity, time and filter used. """
  564. photo_path = self._dir_info.root_path + self._dir_info.protocol_path +\
  565. self._dir_info.img_data_path + flt + "/"
  566. Replicate_name = "Replicate_"
  567. # This method optimises photography time by 1) using JPEG hardware
  568. # acceleration, 2) avoiding the overheads of initialising cameras'
  569. # still pipeline (init still port + encoder for every picture) and 3)
  570. # eliminating overheads of initialising preview port. use_video_port
  571. # can accelerate this further, but at the expense of a lower quality
  572. # image.
  573. for light_intensity in light_range:
  574. self._device._camera.capture_sequence(img_acquisition_routine_replicates(self,
  575. light_intensity,
  576. wavelength,
  577. photo_path,
  578. Replicate_name,
  579. flag=singleRead),
  580. format='jpeg', burst=True,
  581. use_video_port=False, quality=90)
  582. # Now generate `average' image
  583. mean_img = _average_image(self, photo_path, Replicate_name)
  584. # If no custom names provided, use a preset name.
  585. if self._notes == '':
  586. file_name = self._pic_file_name + str(time) + "s_" +\
  587. str(light_intensity).zfill(3) + self._pic_ext
  588. else:
  589. file_name = self._pic_file_name + str(time) + "s_" +\
  590. str(light_intensity).zfill(3)+ self._notes +\
  591. self._pic_ext
  592. cv2.imwrite(photo_path + file_name, mean_img)
  593. del mean_img # Free memory.
  594. return 0
  595. def _detect_well_info(self, recalculate_wells,
  596. plate_reference="findWells.jpg"):
  597. """
  598. Scan the microtitre plate to detect the wells and store their
  599. location for downstream processing. This can also be used to
  600. calibrate microtitre plates black plates and then use translucid
  601. plates (far more common in the laboratory). This routine simplifies
  602. the well detection algorithm and speed-up the program.
  603. """
  604. well_info_file = self._dir_info.root_path +\
  605. "/.well_" + str(self._well_number) + "_info.npz"
  606. if recalculate_wells is True:
  607. os.remove(well_info_file)
  608. if not os.path.exists(well_info_file):
  609. # Detect well location only once, re-use afterwards.
  610. col_labels, row_labels,\
  611. img_limits, wells = dp.wells_id(self._dir_info,
  612. plate_reference,
  613. self._filters[0])
  614. # Store coordinates for future re-use. This command does not support
  615. # cv2, so 'wells' has to be manipulated so that it can be both
  616. # stored and loaded correctly by means of the variable
  617. # 'wells_temp_container'.
  618. wells_temp_container = list() # cannot save cv2.KeyPoint, workaround.
  619. [wells_temp_container.append((well.pt, well.size))
  620. for well in wells]
  621. np.savez(well_info_file, col_labels, row_labels,
  622. img_limits, wells_temp_container)
  623. else:
  624. # If microplate info exists, load well locations. Array order is
  625. # that used when saving the arrays with np.savez. This command does
  626. # not support cv2, so 'wells' has to be manipulated so that it can
  627. # be both stored and loaded correctly by means of the variable
  628. # 'wells_temp_container'.
  629. npzfile = np.load(well_info_file)
  630. col_labels = npzfile['arr_0']
  631. row_labels = npzfile['arr_1']
  632. img_limits = npzfile['arr_2']
  633. wells_temp_container = npzfile['arr_3']
  634. wells = list()
  635. [wells.append(cv2.KeyPoint(x=well[0][0], y=well[0][1],
  636. _size=well[1]))
  637. for well in wells_temp_container]
  638. return col_labels, row_labels, img_limits, wells
  639. def _update_queue(self, Queue, QueueInfo, QueueFile, Read):
  640. QueueInfo[Read].append(str(Queue[Read]))
  641. fIn = open(QueueFile).read().split('\n')
  642. fIn[Read+1] = ', '.join(QueueInfo[Read])
  643. with open(QueueFile, "w") as fOut:
  644. for line in fIn:
  645. fOut.writelines(line + "\n")
  646. print("QueueFile updated.")
  647. def _check_temperature_file(self):
  648. path = self._dir_info.root_path + self._dir_info.protocol_path + "/"
  649. filename = "temperature.csv"
  650. if os.path.exists(path + filename):
  651. os.remove(path + filename)
  652. def _update_temperature(self, current_time):
  653. # Create file to store temperature record.
  654. report_path = self._dir_info.root_path +\
  655. self._dir_info.protocol_path + "/"
  656. report_fName = "temperature.csv"
  657. current_temperature = self._device.report_temperature()
  658. with open(report_path + report_fName, 'a+') as fOut:
  659. info = current_temperature.tolist()
  660. info.insert(0, current_time)
  661. fOut.writelines(','.join(str(entry) for entry in info) + "\n")
  662. def Run(self, assay_length="00:00:00", read_every="00:00:00",
  663. temperature=None, light_range=(0, 255), recalculate_wells=False,
  664. process_data=True, Queue=None, event=None):
  665. """
  666. Run assay. The routine will go through 'light_range' at aleatory
  667. intervals set by 'read_every' for the duration of the assay (set by
  668. the 'assay_length' variable). It will calculate well locations once,
  669. unless stated otherwise by 'recalculate_wells', and will let L|MO™
  670. process the data.\n\n
  671. `temperature' sets the temperature of the assay. Defaults to NONE
  672. (runs at ambient temperature).\n\n
  673. `Queue' contains protocol data concerning the number of jobs to do,
  674. the waiting time between each job, and their status (queued,
  675. completed). Used to keep track of progress from the GUI.\n\n
  676. `event' is a switch to cancel the current `Run' thread and is linked
  677. to a button in the GUI. **This switch has a limitation:** once set
  678. to TRUE, the thread will remain active until the following iteration
  679. when it is killed. When the following iteration occurs will depend
  680. on the frequency set by the variable `read_every'.
  681. """
  682. # Initialise Queue. If no Queue provided, just read once.
  683. if Queue is not None:
  684. """ QueueInfo is a list of tuples: (init_time, status)"""
  685. [Queue, QueueInfo, QueueFile] = Queue
  686. ReadsNum = len(Queue)
  687. else:
  688. ReadsNum = 1
  689. StatusID = 2 # QueueFile field to modify (0=Date, 1=Time, 2=Status, 3=Inc. time)
  690. # Transform assay_length into seconds.
  691. HH, MM, SS = assay_length.split(sep=":")
  692. ASSAY_LENGTH = int(HH) * 3600 + int(MM) * 60 + int(SS)
  693. # Transform read_every into seconds.
  694. HH, MM, SS = read_every.split(sep=":")
  695. READ_FREQUENCY = int(HH) * 3600 + int(MM) * 60 + int(SS)
  696. # Run protocol. MAYBE USE A VIDEO INSTEAD OF 256 PICTURES? *NOPE*. Video
  697. # port uses soft-touch de-noise algorithm and red light stills looks
  698. # highly aliased.
  699. servo_state = self._device._current_state
  700. finished = False
  701. if ASSAY_LENGTH == 0:
  702. singleRead = True
  703. else:
  704. singleRead = False
  705. if ReadsNum > 1:
  706. self._check_temperature_file()
  707. # Set temperature.
  708. if "temperature_event" not in locals():
  709. temperature_event = threading.Event()
  710. if temperature is not 0:
  711. current_temperature = self._device.report_temperature()
  712. temperature_modulation =\
  713. threading.Thread(target=self._device._set_temperature,
  714. args=(current_temperature, int(temperature),
  715. TEMPERATURE_TOLERANCE,
  716. TEMPERATURE_SAMPLING_FREQ,
  717. temperature_event))
  718. temperature_modulation.start()
  719. # Loop until protocol is finished.
  720. initiation_time = t.time()
  721. reading_time = 0.0 # Protocols with delay will fail without this.
  722. for read in range(ReadsNum): # Queue
  723. # Wait/Incubate
  724. if ReadsNum > 1:
  725. if Queue[read] == 0.0:
  726. t.sleep(Queue[read])
  727. else:
  728. t.sleep(Queue[read] - reading_time)
  729. current_time = int(t.time() - initiation_time) # Makes t0 = ~0s.
  730. # Update temperature (even with `ambient', to keep track of it).
  731. self._update_temperature(current_time)
  732. # Handle cancel_protocol event.
  733. if event.is_set():
  734. # cancel_protocol.is_set() is now True. STOP.
  735. if temperature is not 0:
  736. temperature_event.set()
  737. temperature_modulation.join(timout=2.0)
  738. log = str("Protocol cancelled.")
  739. print(log)
  740. # Retrieve data available until this point.
  741. # for flt in self._filters:
  742. # Retrieve a reference IMG (highest light intensity by default).
  743. # Use well location routine to crop images. Force `No_Filter'
  744. # setting to ensure wells are detected correctly.
  745. # if 'img_limits' not in locals():
  746. # _, _, _, _ = dp.wells_id(self._dir_info,
  747. # 'findWells.jpg',
  748. # 'No_Filter')
  749. # TODO: CROP IMG HERE BEFORE SENDING TO MINIMISE DATA TRANSFERRED. [DONE]
  750. # dp.export_img_data(self._dir_info, flt, connection,
  751. # BUFFER_SIZE, status)
  752. # export all data to client's GUI.
  753. # DO NOT COMPRESS the data before sending. IT-TAKES-AGES.
  754. # Send images over the network and delete them from the pi.
  755. return 1
  756. # Read plate
  757. init_time = t.time() # Used to account for plate reading time.
  758. for flt, wavelength in zip(self._filters, self._wavelengths):
  759. warmup_init_t = t.time()
  760. if flt == "Filter_2":
  761. # Check if `wavelenght' is WHITE, as it is produced by combining mutiple PIN
  762. if wavelength == 3:
  763. for PIN in range(len(self._device._GPIO_PIN)):
  764. self._device._light_panel.set_PWM_frequency(self._device._GPIO_PIN[PIN], LIGHT_FREQ_F2)
  765. else:
  766. self._device._light_panel.set_PWM_frequency(self._device._GPIO_PIN[wavelength], LIGHT_FREQ_F2)
  767. # Set camera on-the-fly to adapt to different wavelenghts.
  768. # Because this step happens with any filter, it does not require
  769. # a post-read reset.
  770. self._device._camera = _img_settings(self._device._camera,
  771. Filter=flt)
  772. servo_state = self._device.set_filter(flt, current_pwm=servo_state)
  773. # self._data_acquisition(light_range, current_time, flt,
  774. # wavelength, singleRead)
  775. print("Time between reads: " + str(round(t.time() - warmup_init_t, 3)) + "s.")
  776. self._data_acquisition_triplicate(light_range, current_time, flt,
  777. wavelength, singleRead)
  778. if flt == "Filter_2":
  779. # Check if `wavelenght' is WHITE, as it is produced by combining mutiple PIN.
  780. if wavelength == 3:
  781. for PIN in range(len(self._device._GPIO_PIN)):
  782. self._device._light_panel.set_PWM_frequency(self._device._GPIO_PIN[PIN], DEFAULT_LIGHT_FREQ)
  783. else:
  784. self._device._light_panel.set_PWM_frequency(self._device._GPIO_PIN[wavelength], DEFAULT_LIGHT_FREQ)
  785. # Turn LED off
  786. self._device._modulate_LED_intensity(0, channel=wavelength)
  787. # Process data (inc. exporting data)
  788. if process_data is True:
  789. # TODO: Compare this to when flag is FALSE.
  790. log = str("Data acquisition at " + str(current_time) +
  791. "s completed. Exporting numeric data to client...")
  792. print(log)
  793. # Does well location info exists for microplate? Is it loaded?
  794. if 'col_labels' not in locals():
  795. col_labels, row_labels,\
  796. img_limits, wells = self._detect_well_info(recalculate_wells)
  797. for flt in self._filters:
  798. raw_data = dp.read_plate(self._dir_info, wells, flt,
  799. row_labels, col_labels,
  800. img_limits, current_time)
  801. dp.export_numeric_data(self._dir_info, raw_data, flt,
  802. current_time, connection,
  803. BUFFER_SIZE, status)
  804. else:
  805. pass
  806. # Wait for the next read. Queue waiting time _already_ in seconds.
  807. # Account for reading time, otherwise waits incrementally longer.
  808. if ReadsNum > 1:
  809. QueueInfo[read][StatusID] = "Completed" # Update Queue
  810. self._update_queue(Queue, QueueInfo, QueueFile, read)
  811. reading_time = t.time() - init_time
  812. # if process_data is False:
  813. # for flt in self._filters:
  814. # Retrieve a reference IMG (highest light intensity by default).
  815. # Use well location routine to crop images. Force `No_Filter'
  816. # setting to ensure wells are detected correctly.
  817. # if 'img_limits' not in locals():
  818. # _, _, _, _ = dp.wells_id(self._dir_info,
  819. # 'findWells.jpg',
  820. # 'No_Filter')
  821. # TODO: CROP IMG HERE BEFORE SENDING TO MINIMISE DATA TRANSFERRED. [DONE]
  822. # dp.export_img_data(self._dir_info, flt, connection,
  823. # BUFFER_SIZE, status)
  824. # export all data to client's GUI.
  825. # DO NOT COMPRESS the data before sending. IT-TAKES-AGES.
  826. # Send images over the network and delete them from the pi.
  827. # PROTOCOL FINISHED:
  828. if temperature is not 0:
  829. temperature_event.set() # Stop temperature control thread
  830. temperature_modulation.join()
  831. # End msg.
  832. log = str("::Protocol run successfully::")
  833. print(log)
  834. return 0
  835. def __init__(self, with_device=None, filters=("No_Filter",),
  836. and_wavelenghts=(3,), well_number=96,
  837. protocol_name="default_protocol", **kwargs):
  838. """ Main program for SetProtocol(). Initiate class. """
  839. if with_device is not None:
  840. self._device = with_device
  841. self._filters = filters
  842. self._wavelengths = and_wavelenghts
  843. # Additional notes, if they exist.
  844. if 'notes' in kwargs.keys():
  845. self._notes = kwargs.get('notes')
  846. else:
  847. self._notes = ""
  848. # Set filename pattern
  849. self._pic_file_name = 'Read_'
  850. self._pic_ext = '.jpg'
  851. # Check directory does not exist. TODO: rewrite to remove try/except.
  852. try:
  853. self._dir_info = GenerateTree(label=protocol_name,
  854. filters=filters,
  855. annotations=self._notes)
  856. except FileExistsError as err:
  857. print("The directory already exists.")
  858. # possible_answers = ['o', 'r']
  859. # while True:
  860. # answer = input("Do you want to [o]verwrite it or [r]ename it? (o/R): ")
  861. # if answer in possible_answers:
  862. # break
  863. # else:
  864. # print("Input not recognised, try again.")
  865. # Take action
  866. # if answer.lower() == 'o': # If overwrite... overwrite.
  867. # Isolate protocol path (useful when multiple filters used)
  868. path_strings = err.filename.split(sep="/")
  869. idx = path_strings.index("data") + 2
  870. delete_path = "/".join(path_strings[:idx])
  871. # Regenerate.
  872. sh.rmtree(delete_path)
  873. self._dir_info = GenerateTree(label=protocol_name,
  874. filters=filters,
  875. annotations=self._notes)
  876. # else:
  877. # annotations = input("Enter new name: ")
  878. # annotations.replace(" ", "_").lower() # No spaces.
  879. # self._notes = annotations
  880. # self._dir_info = GenerateTree(label=protocol_name,
  881. # filters=and_filters,
  882. # annotations=self._notes)
  883. self._well_number = int(well_number)
  884. self._resolution = self._device._camera.resolution
  885. self._cam_rev = str(self._device._camera.revision)
  886. else:
  887. raise ValueError("Instrument not specified.")