Skip to content

Commit ef4aee6

Browse files
authored
Merge pull request #368 from plotly/dash-ocean-optics
Dash ocean optics
2 parents 77fa75f + ae08800 commit ef4aee6

13 files changed

+1457
-0
lines changed

apps/dash-ocean-optics/.gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
venv
2+
*.pyc
3+
.DS_Store
4+
.env
5+
settings.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
import random
2+
import numpy
3+
4+
import dash_daq as daq
5+
import dash_html_components as html
6+
import dash_core_components as dcc
7+
8+
try:
9+
import seabreeze.spectrometers as sb
10+
from seabreeze.spectrometers import SeaBreezeError
11+
except Exception as e:
12+
print(e)
13+
14+
15+
# abstract base class to represent spectrometers
16+
class DashOceanOpticsSpectrometer:
17+
def __init__(self, specLock, commLock):
18+
self._spec = None # spectrometer
19+
self._specmodel = "" # model name for graph title
20+
self._lightSources = {} # dict of light sources, if any
21+
self._spectralData = [[], []] # wavelengths and intensities
22+
self._controlFunctions = {} # behaviour upon changing controls
23+
self._int_time_max = 650000000 # maximum integration time (ms)
24+
self._int_time_min = 1000 # minimum integration time (ms)
25+
self.comm_lock = commLock # for communicating with spectrometer
26+
self.spec_lock = specLock # for editing spectrometer values
27+
28+
# refreshes/populates spectrometer properties
29+
def assign_spec(self):
30+
return
31+
32+
# get data for graph
33+
def get_spectrum(self):
34+
return self._spectralData
35+
36+
# send each command; return successes and failures
37+
def send_control_values(self, commands):
38+
return ({}, {})
39+
40+
# live-update light intensity
41+
def send_light_intensity(self, val):
42+
return
43+
44+
# getter methods
45+
46+
def model(self):
47+
return self._specmodel
48+
49+
def light_sources(self):
50+
return self._lightSources
51+
52+
def int_time_max(self):
53+
return self._int_time_max
54+
55+
def int_time_min(self):
56+
return self._int_time_min
57+
58+
59+
# non-demo version
60+
class PhysicalSpectrometer(DashOceanOpticsSpectrometer):
61+
def __init__(self, specLock, commLock):
62+
super().__init__(specLock, commLock)
63+
try:
64+
self.spec_lock.acquire()
65+
self.assign_spec()
66+
except SeaBreezeError: # if the spec has already been connected
67+
pass
68+
finally:
69+
self.spec_lock.release()
70+
self._controlFunctions = {
71+
"integration-time-input": "self._spec.integration_time_micros",
72+
"nscans-to-average-input": "self._spec.scans_to_average",
73+
"continuous-strobe-toggle-input": "self._spec.continuous_strobe_set_enable",
74+
"continuous-strobe-period-input": "self._spec.continuous_strobe_set_period_micros",
75+
"light-source-input": "self.update_light_source",
76+
}
77+
78+
def assign_spec(self):
79+
try:
80+
self.comm_lock.acquire()
81+
devices = sb.list_devices()
82+
self._spec = sb.Spectrometer(devices[0])
83+
self._specmodel = self._spec.model
84+
self._lightSources = [
85+
{"label": ls.__repr__(), "value": ls}
86+
for ls in list(self._spec.light_sources())
87+
]
88+
self._int_time_min = self._spec.minimum_integration_time_micros()
89+
except Exception:
90+
pass
91+
finally:
92+
self.comm_lock.release()
93+
94+
def get_spectrum(self):
95+
if self._spec is None:
96+
try:
97+
self.spec_lock.acquire()
98+
self.assign_spec()
99+
except Exception:
100+
pass
101+
finally:
102+
self.spec_lock.release()
103+
try:
104+
self.comm_lock.acquire()
105+
self._spectralData = self._spec.spectrum(
106+
correct_dark_counts=True, correct_nonlinearity=True
107+
)
108+
except Exception:
109+
pass
110+
finally:
111+
self.comm_lock.release()
112+
113+
return self._spectralData
114+
115+
def send_control_values(self, commands):
116+
failed = {}
117+
succeeded = {}
118+
119+
for ctrl_id in commands:
120+
try:
121+
self.comm_lock.acquire()
122+
eval(self._controlFunctions[ctrl_id])(commands[ctrl_id])
123+
succeeded[ctrl_id] = str(commands[ctrl_id])
124+
except Exception as e:
125+
failed[ctrl_id] = str(e).strip("b")
126+
finally:
127+
self.comm_lock.release()
128+
129+
return (failed, succeeded)
130+
131+
def send_light_intensity(self, lightSource, intensity):
132+
try:
133+
self.comm_lock.acquire()
134+
lightSource.set_intensity(intensity)
135+
except Exception:
136+
pass
137+
finally:
138+
self.comm_lock.release()
139+
140+
def model(self):
141+
try:
142+
self.spec_lock.acquire()
143+
self.assign_spec()
144+
except SeaBreezeError:
145+
pass
146+
finally:
147+
self.spec_lock.release()
148+
return self._specmodel
149+
150+
def light_sources(self):
151+
try:
152+
self.spec_lock.acquire()
153+
self.assign_spec()
154+
except SeaBreezeError:
155+
pass
156+
finally:
157+
self.spec_lock.release()
158+
159+
return self._lightSources
160+
161+
def int_time_max(self):
162+
try:
163+
self.spec_lock.acquire()
164+
self.assign_spec()
165+
except SeaBreezeError:
166+
pass
167+
finally:
168+
self.spec_lock.release()
169+
170+
return self._int_time_max
171+
172+
def int_time_min(self):
173+
try:
174+
self.spec_lock.acquire()
175+
self.assign_spec()
176+
except SeaBreezeError:
177+
pass
178+
finally:
179+
self.spec_lock.release()
180+
181+
return self._int_time_min
182+
183+
def update_light_source(self, ls):
184+
if ls is not None and ls is not "":
185+
ls.set_enable(True)
186+
187+
188+
class DemoSpectrometer(DashOceanOpticsSpectrometer):
189+
def __init__(self, specLock, commLock):
190+
super().__init__(specLock, commLock)
191+
try:
192+
self.spec_lock.acquire()
193+
self.assign_spec()
194+
except Exception:
195+
pass
196+
finally:
197+
self.spec_lock.release()
198+
self.controlFunctions = {
199+
"integration-time-input": "self.integration_time_demo",
200+
"nscans-to-average-input": "self.empty_control_demo",
201+
"continuous-strobe-toggle-input": "self.empty_control_demo",
202+
"continuous-strobe-period-input": "self.empty_control_demo",
203+
"light-source-input": "self.exception_demo",
204+
}
205+
self._sample_data_scale = self._int_time_min
206+
self._sample_data_add = 0
207+
208+
def assign_spec(self):
209+
self._specmodel = "USB2000+"
210+
self._lightSources = [
211+
{"label": "Lamp 1 at 127.0.0.1", "value": "Lamp 1"},
212+
{"label": "Lamp 2 at 127.0.0.1", "value": "Lamp 2"},
213+
]
214+
215+
def get_spectrum(self, int_time_demo_val=1000):
216+
self._spectralData[0] = numpy.linspace(400, 900, 5000)
217+
self._spectralData[1] = [
218+
self.sample_spectrum(wl) for wl in self._spectralData[0]
219+
]
220+
221+
return self._spectralData
222+
223+
def send_control_values(self, commands):
224+
failed = {}
225+
succeeded = {}
226+
227+
for ctrl_id in commands:
228+
try:
229+
eval(self.controlFunctions[ctrl_id])(commands[ctrl_id])
230+
succeeded[ctrl_id] = str(commands[ctrl_id])
231+
except Exception as e:
232+
failed[ctrl_id] = str(e)
233+
234+
return (failed, succeeded)
235+
236+
def send_light_intensity(self, lightSource, intensity):
237+
if lightSource == "Lamp 1":
238+
return
239+
elif lightSource == "Lamp 2":
240+
self._sample_data_add = intensity
241+
else:
242+
self._sample_data_add = 0
243+
244+
def model(self):
245+
return self._specmodel
246+
247+
def light_sources(self):
248+
return self._lightSources
249+
250+
def int_time_max(self):
251+
return self._int_time_max
252+
253+
def int_time_min(self):
254+
return self._int_time_min
255+
256+
# demo-specific methods
257+
258+
# generates a sample spectrum that's normally distributed about 500 nm
259+
def sample_spectrum(self, x):
260+
return (
261+
self._sample_data_scale
262+
* (numpy.e ** (-1 * ((x - 500) / 5) ** 2) + 0.01 * random.random())
263+
+ self._sample_data_add * 10
264+
)
265+
266+
def integration_time_demo(self, x):
267+
self._sample_data_scale = x
268+
269+
def empty_control_demo(self, _):
270+
return
271+
272+
def exception_demo(self, x):
273+
if x == "l1":
274+
raise Exception("Lamp not found.")
275+
else:
276+
return
277+
278+
279+
# class to represent all controls
280+
class Control:
281+
def __init__(
282+
self, new_ctrl_id, new_ctrl_name, new_component_type, new_component_attr
283+
):
284+
self.ctrl_id = new_ctrl_id # id for callbacks
285+
self.ctrl_name = new_ctrl_name # name for label
286+
self.component_type = new_component_type # dash-daq component type
287+
self.component_attr = new_component_attr # component attributes
288+
289+
# creates a new control box with defined component, id, and name
290+
def create_ctrl_div(self, pwrOff):
291+
# create dash-daq components
292+
try:
293+
component_obj = getattr(daq, self.component_type)
294+
except AttributeError:
295+
component_obj = getattr(dcc, self.component_type)
296+
297+
# disable if power is off
298+
self.component_attr["disabled"] = pwrOff
299+
300+
component = component_obj(**self.component_attr)
301+
302+
# generate html code
303+
new_control = html.Div(
304+
id=self.ctrl_id,
305+
children=[
306+
html.Div(className="option-name", children=[self.ctrl_name]),
307+
component,
308+
],
309+
)
310+
return new_control
311+
312+
# gets whether we look for "value", "on", etc.
313+
def val_string(self):
314+
if "value" in self.component_attr:
315+
return "value"
316+
elif "on" in self.component_attr:
317+
return "on"
318+
319+
# changes value ('on' or 'value', etc.)
320+
def update_value(self, new_value):
321+
self.component_attr[self.val_string()] = new_value

apps/dash-ocean-optics/Procfile

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
web: gunicorn app:server --timeout 300

0 commit comments

Comments
 (0)