labdata 0.0.24__tar.gz → 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. {labdata-0.0.24 → labdata-0.1.0}/PKG-INFO +2 -2
  2. labdata-0.1.0/labdata/__init__.py +11 -0
  3. {labdata-0.0.24 → labdata-0.1.0}/labdata/cli.py +4 -0
  4. {labdata-0.0.24 → labdata-0.1.0}/labdata/dashboard/compute.py +2 -2
  5. labdata-0.1.0/labdata/dashboard/console.py +166 -0
  6. {labdata-0.0.24 → labdata-0.1.0}/labdata/dashboard/decisiontask.py +26 -19
  7. labdata-0.1.0/labdata/dashboard/index.py +233 -0
  8. labdata-0.1.0/labdata/dashboard/notes.py +184 -0
  9. labdata-0.1.0/labdata/dashboard/procedures.py +284 -0
  10. labdata-0.1.0/labdata/dashboard/segmentation.py +564 -0
  11. {labdata-0.0.24 → labdata-0.1.0}/labdata/dashboard/sorting.py +1 -1
  12. {labdata-0.0.24 → labdata-0.1.0}/labdata/dashboard/video.py +1 -1
  13. {labdata-0.0.24 → labdata-0.1.0}/labdata/stacks.py +18 -0
  14. {labdata-0.0.24 → labdata-0.1.0}/labdata/utils.py +42 -3
  15. {labdata-0.0.24 → labdata-0.1.0}/labdata.egg-info/PKG-INFO +2 -2
  16. {labdata-0.0.24 → labdata-0.1.0}/labdata.egg-info/SOURCES.txt +3 -0
  17. {labdata-0.0.24 → labdata-0.1.0}/labdata.egg-info/requires.txt +1 -1
  18. {labdata-0.0.24 → labdata-0.1.0}/pyproject.toml +1 -1
  19. labdata-0.0.24/labdata/__init__.py +0 -16
  20. labdata-0.0.24/labdata/dashboard/index.py +0 -408
  21. labdata-0.0.24/labdata/dashboard/segmentation.py +0 -49
  22. {labdata-0.0.24 → labdata-0.1.0}/LICENSE +0 -0
  23. {labdata-0.0.24 → labdata-0.1.0}/README.md +0 -0
  24. {labdata-0.0.24 → labdata-0.1.0}/labdata/compute/__init__.py +0 -0
  25. {labdata-0.0.24 → labdata-0.1.0}/labdata/compute/caiman.py +0 -0
  26. {labdata-0.0.24 → labdata-0.1.0}/labdata/compute/ec2.py +0 -0
  27. {labdata-0.0.24 → labdata-0.1.0}/labdata/compute/ephys.py +0 -0
  28. {labdata-0.0.24 → labdata-0.1.0}/labdata/compute/pose.py +0 -0
  29. {labdata-0.0.24 → labdata-0.1.0}/labdata/compute/schedulers.py +0 -0
  30. {labdata-0.0.24 → labdata-0.1.0}/labdata/compute/singularity.py +0 -0
  31. {labdata-0.0.24 → labdata-0.1.0}/labdata/compute/suite2p.py +0 -0
  32. {labdata-0.0.24 → labdata-0.1.0}/labdata/compute/utils.py +0 -0
  33. {labdata-0.0.24 → labdata-0.1.0}/labdata/copy.py +0 -0
  34. {labdata-0.0.24 → labdata-0.1.0}/labdata/dashboard/__init__.py +0 -0
  35. {labdata-0.0.24 → labdata-0.1.0}/labdata/dashboard/explorer/__init__.py +0 -0
  36. {labdata-0.0.24 → labdata-0.1.0}/labdata/dashboard/explorer/cellsegmentation.py +0 -0
  37. {labdata-0.0.24 → labdata-0.1.0}/labdata/dashboard/explorer/spikesorting.py +0 -0
  38. {labdata-0.0.24 → labdata-0.1.0}/labdata/export.py +0 -0
  39. {labdata-0.0.24 → labdata-0.1.0}/labdata/rules/__init__.py +0 -0
  40. {labdata-0.0.24 → labdata-0.1.0}/labdata/rules/ephys.py +0 -0
  41. {labdata-0.0.24 → labdata-0.1.0}/labdata/rules/imaging.py +0 -0
  42. {labdata-0.0.24 → labdata-0.1.0}/labdata/rules/utils.py +0 -0
  43. {labdata-0.0.24 → labdata-0.1.0}/labdata/s3.py +0 -0
  44. {labdata-0.0.24 → labdata-0.1.0}/labdata/schema/__init__.py +0 -0
  45. {labdata-0.0.24 → labdata-0.1.0}/labdata/schema/ephys.py +0 -0
  46. {labdata-0.0.24 → labdata-0.1.0}/labdata/schema/general.py +0 -0
  47. {labdata-0.0.24 → labdata-0.1.0}/labdata/schema/histology.py +0 -0
  48. {labdata-0.0.24 → labdata-0.1.0}/labdata/schema/onephoton.py +0 -0
  49. {labdata-0.0.24 → labdata-0.1.0}/labdata/schema/procedures.py +0 -0
  50. {labdata-0.0.24 → labdata-0.1.0}/labdata/schema/tasks.py +0 -0
  51. {labdata-0.0.24 → labdata-0.1.0}/labdata/schema/twophoton.py +0 -0
  52. {labdata-0.0.24 → labdata-0.1.0}/labdata/schema/utils.py +0 -0
  53. {labdata-0.0.24 → labdata-0.1.0}/labdata/schema/video.py +0 -0
  54. {labdata-0.0.24 → labdata-0.1.0}/labdata/widgets.py +0 -0
  55. {labdata-0.0.24 → labdata-0.1.0}/labdata.egg-info/dependency_links.txt +0 -0
  56. {labdata-0.0.24 → labdata-0.1.0}/labdata.egg-info/entry_points.txt +0 -0
  57. {labdata-0.0.24 → labdata-0.1.0}/labdata.egg-info/top_level.txt +0 -0
  58. {labdata-0.0.24 → labdata-0.1.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labdata
3
- Version: 0.0.24
3
+ Version: 0.1.0
4
4
  Summary: Package to manage data in experimental neuroscience labs
5
5
  Author-email: Joao Couto <jpcouto@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/jcouto/labdata
@@ -23,7 +23,7 @@ Requires-Dist: dash; extra == "dashboard"
23
23
  Requires-Dist: dash_bootstrap_components; extra == "dashboard"
24
24
  Requires-Dist: dash-extensions; extra == "dashboard"
25
25
  Requires-Dist: plotly; extra == "dashboard"
26
- Requires-Dist: streamlit>1.34; extra == "dashboard"
26
+ Requires-Dist: streamlit>1.54; extra == "dashboard"
27
27
  Requires-Dist: streamlit-bokeh; extra == "dashboard"
28
28
  Requires-Dist: scipy; extra == "dashboard"
29
29
  Requires-Dist: scikit-image; extra == "dashboard"
@@ -0,0 +1,11 @@
1
+ VERSION = "0.1.0"
2
+
3
+ from .utils import *
4
+ from .copy import copy_to_upload_server
5
+ from .s3 import copy_to_s3
6
+
7
+ _default_project = (prefs.get('database', {}) or {}).get('database.project', None)
8
+ plugins = load_plugins(project=_default_project)
9
+ for _name, _mod in plugins.items():
10
+ globals()[_name] = _mod
11
+
@@ -60,6 +60,8 @@ Server commands (don't run on experimental computers):
60
60
  parser.add_argument('--spike-sorting' ,action = 'store_true', default = False)
61
61
  parser.add_argument('--cell-segmentation' ,action = 'store_true', default = False)
62
62
  parser.add_argument('--debug' ,action = 'store_true', default = False, help = 'To debug the dash (dashboard) errors.')
63
+ parser.add_argument('--console', action = 'store_true', default = False,
64
+ help = 'Include the IPython console tab in the dashboard.')
63
65
  project = _get_project()
64
66
  parser.add_argument('-p','--project',
65
67
  default = project,
@@ -91,6 +93,8 @@ Server commands (don't run on experimental computers):
91
93
  command = f'streamlit run {str(main_webpage_path)}'
92
94
  if not args.project is None:
93
95
  command += f' project={args.project}'
96
+ if args.console:
97
+ command += ' console=1'
94
98
  sub.run(command.split(' '))
95
99
 
96
100
  def subjects(self):
@@ -44,7 +44,7 @@ def _jobs_panel(table, status_field, target_field, cols, delete_key):
44
44
  x='Jobs:Q',
45
45
  color=alt.Color('color:N').scale(None)
46
46
  ).configure_axis(labelLimit=3000))
47
- st.altair_chart(ch, use_container_width=True)
47
+ st.altair_chart(ch, width="stretch")
48
48
 
49
49
  # filtered table
50
50
  selected_states = st.multiselect('Select status:', STATES, default=STATES[1:], key=f'{delete_key}_filter')
@@ -58,7 +58,7 @@ def _jobs_panel(table, status_field, target_field, cols, delete_key):
58
58
  display_df,
59
59
  column_config={'Select': st.column_config.CheckboxColumn(required=True)},
60
60
  disabled=[c for c in display_df.columns if c != 'Select'],
61
- use_container_width=True,
61
+ width="stretch",
62
62
  key=f'{delete_key}_editor',
63
63
  )
64
64
  selected_jobs = edited[edited['Select'] == True].index.tolist()
@@ -0,0 +1,166 @@
1
+ import streamlit as st
2
+ import base64
3
+
4
+
5
+ def _execute_silent(kc, code):
6
+ """Fire-and-forget execution (used for kernel init)."""
7
+ kc.execute(code, silent=True)
8
+ while True:
9
+ try:
10
+ msg = kc.get_iopub_msg(timeout=60)
11
+ except Exception:
12
+ break
13
+ if (msg['msg_type'] == 'status' and
14
+ msg['content'].get('execution_state') == 'idle'):
15
+ break
16
+
17
+
18
+ def _execute(kc, code):
19
+ """Execute code and collect text outputs, inline images, and errors."""
20
+ kc.execute(code)
21
+ outputs = [] # list of (kind, text) kind in ('stream','result')
22
+ images = [] # list of base-64 PNG strings
23
+ error = None
24
+ while True:
25
+ try:
26
+ msg = kc.get_iopub_msg(timeout=60)
27
+ except Exception:
28
+ break
29
+ mtype = msg['msg_type']
30
+ content = msg['content']
31
+ if mtype == 'stream':
32
+ outputs.append(('stream', content['text']))
33
+ elif mtype in ('execute_result', 'display_data'):
34
+ img = content['data'].get('image/png')
35
+ if img:
36
+ images.append(img)
37
+ else:
38
+ text = content['data'].get('text/plain', '')
39
+ if text:
40
+ outputs.append(('result', text))
41
+ elif mtype == 'error':
42
+ error = '\n'.join(content['traceback'])
43
+ elif mtype == 'status' and content.get('execution_state') == 'idle':
44
+ break
45
+ return outputs, images, error
46
+
47
+
48
+ def _start_kernel(project_name):
49
+ import jupyter_client
50
+ km = jupyter_client.KernelManager(kernel_name='python3')
51
+ km.start_kernel()
52
+ kc = km.client()
53
+ kc.start_channels()
54
+ kc.wait_for_ready(timeout=60)
55
+
56
+ # Configure matplotlib for inline PNG output, then preload labdata
57
+ init_lines = [
58
+ "import matplotlib",
59
+ "matplotlib.use('Agg')",
60
+ "import pylab as plt",
61
+ "%matplotlib inline",
62
+ ]
63
+ if project_name:
64
+ init_lines += [
65
+ "import os",
66
+ f"os.environ['LABDATA_DATABASE_PROJECT'] = {project_name!r}",
67
+ ]
68
+ init_lines += [
69
+ "from labdata import *",
70
+ "from labdata.schema import *",
71
+ ]
72
+ _execute_silent(kc, '\n'.join(init_lines))
73
+
74
+ st.session_state['_console_km'] = km
75
+ st.session_state['_console_kc'] = kc
76
+ st.session_state['_console_hist'] = []
77
+ st.session_state['_console_n'] = 0
78
+
79
+
80
+ def _shutdown_kernel():
81
+ km = st.session_state.pop('_console_km', None)
82
+ st.session_state.pop('_console_kc', None)
83
+ st.session_state.pop('_console_hist', None)
84
+ st.session_state.pop('_console_n', None)
85
+ if km is not None:
86
+ try:
87
+ km.shutdown_kernel(now=True)
88
+ except Exception:
89
+ pass
90
+
91
+
92
+ def console_tab(schema=None):
93
+ project_name = getattr(schema, 'schema_project', None) if schema else None
94
+
95
+ # kernel status bar
96
+ alive = '_console_km' in st.session_state
97
+ status_col, btn_col = st.columns([4, 1])
98
+ with status_col:
99
+ if alive:
100
+ st.success("Kernel running • `from labdata import *` and `from labdata.schema import *` pre-loaded")
101
+ else:
102
+ st.info("Kernel not started. Click **Start kernel** to launch an IPython kernel.")
103
+ with btn_col:
104
+ if not alive:
105
+ if st.button("Start kernel", type="primary", use_container_width=True):
106
+ with st.spinner("Starting IPython kernel…"):
107
+ _start_kernel(project_name)
108
+ st.rerun()
109
+ else:
110
+ if st.button("Restart kernel", type="secondary", use_container_width=True):
111
+ _shutdown_kernel()
112
+ with st.spinner("Restarting…"):
113
+ _start_kernel(project_name)
114
+ st.rerun()
115
+
116
+ if not alive:
117
+ return
118
+
119
+ kc = st.session_state['_console_kc']
120
+ history = st.session_state['_console_hist']
121
+
122
+ # cell history
123
+ for entry in history:
124
+ n = entry['n']
125
+ st.code(f"In [{n}]:\n{entry['input']}", language='python')
126
+ for kind, text in entry['outputs']:
127
+ if kind == 'result':
128
+ st.text(f"Out [{n}]: {text.rstrip()}")
129
+ else:
130
+ st.text(text.rstrip())
131
+ for b64 in entry['images']:
132
+ st.image(base64.b64decode(b64))
133
+ if entry['error']:
134
+ st.error(entry['error'])
135
+
136
+ if history:
137
+ st.divider()
138
+
139
+ # input area
140
+ code = st.text_area(
141
+ "code_input",
142
+ height=130,
143
+ placeholder="Python / IPython code — plots are displayed inline",
144
+ label_visibility='collapsed',
145
+ key='_console_code',
146
+ )
147
+
148
+ run_col, clear_col, _ = st.columns([1, 1, 6])
149
+ with run_col:
150
+ run_pressed = st.button("▶ Run", type="primary", use_container_width=True)
151
+ with clear_col:
152
+ if st.button("Clear", use_container_width=True):
153
+ st.session_state['_console_hist'] = []
154
+ st.session_state['_console_n'] = 0
155
+ st.rerun()
156
+
157
+ if run_pressed and code.strip():
158
+ with st.spinner("Running…"):
159
+ outputs, images, error = _execute(kc, code)
160
+ n = st.session_state['_console_n'] + 1
161
+ st.session_state['_console_n'] = n
162
+ history.append(dict(n=n, input=code,
163
+ outputs=outputs, images=images, error=error))
164
+ st.rerun()
165
+
166
+
@@ -106,11 +106,15 @@ def decisionmaking_tab(schema = None):
106
106
  lambda x: x.strftime("%y-%m-%d"))
107
107
  return summarysource
108
108
 
109
+ if st.button('Refresh', icon=':material/refresh:'):
110
+ get_subjects_last_ran.clear()
111
+ get_summary_data.clear()
112
+
109
113
  all_subjects,subjects,max_date = get_subjects_last_ran()
110
114
  if not len(subjects):
111
115
  st.write('There are no sessions.')
112
116
  return
113
-
117
+
114
118
  selected_indices = st.multiselect('Select subjects:',all_subjects,default = [s['subject_name'] for s in subjects])
115
119
  if len(selected_indices):
116
120
  # cache data to run faster
@@ -121,11 +125,11 @@ def decisionmaking_tab(schema = None):
121
125
  point_selector = alt.selection_point(on="click")
122
126
  chart = alt.Chart(summarysource).mark_rect().encode(
123
127
  x=alt.X('session_datetime:T',timeUnit = 'yearmonthdate',
124
- scale=alt.Scale(nice={'interval': 'day', 'step': 1})).axis(labelAngle=45,format='%Y-%m-%d').title('Date'),
128
+ scale=alt.Scale(nice={'interval': 'day', 'step': 1})).axis(labelAngle=45,format='%Y-%m-%d').title('Date'),
125
129
  y=alt.Y('subject_name:O').title('Subject name'),
126
130
  color=alt.Color('performance_easy:Q',
127
- scale=alt.Scale(scheme='spectral', reverse=True,domain = [0.5,1])).title('Perf easy'),
128
- tooltip = ['subject_name','session_name','session_datetime','performance_easy','performance','n_trials']
131
+ scale=alt.Scale(scheme='spectral', reverse=True, domain=[0.5,1], clamp=True)).title('Perf easy'),
132
+ tooltip = ['subject_name:N','session_name:N','session_datetime:T','performance_easy:Q','performance:Q','n_trials:Q']
129
133
 
130
134
  ).properties(width=1000, title ='Behavioral performance').add_params(point_selector)
131
135
  event_data = st.altair_chart(chart.interactive(), on_select='rerun')
@@ -160,6 +164,10 @@ def decisionmaking_tab(schema = None):
160
164
  & dict(subject_name=subject_name, session_name=session_name)
161
165
  ).fetch(*cols, as_dict=True)
162
166
 
167
+ if st.button('Refresh sessions', icon=':material/refresh:'):
168
+ get_subject_data.clear()
169
+ get_session_trials.clear()
170
+
163
171
  trialset, weights, watering = get_subject_data(subject_name)
164
172
  sessions = sorted(trialset['session_datetime'].unique())
165
173
 
@@ -192,15 +200,15 @@ def decisionmaking_tab(schema = None):
192
200
 
193
201
  with col_summary:
194
202
  black, orange = '#000000','#ff7f0e'
195
- perf_val = trialset.dropna(subset=['performance_easy','performance']).sort_values('session_datetime').drop_duplicates('session_datetime').iloc[[-20]]
203
+ perf_val = trialset.dropna(subset=['performance_easy','performance']).sort_values('session_datetime').drop_duplicates('session_datetime').iloc[[-1]]
196
204
  x_enc = alt.X('session_datetime:T', axis=axis).title('Session date')
197
205
  perf_chart = (
198
206
  alt.Chart(trialset).mark_line(color=black).encode(x=x_enc, y=alt.Y('performance_easy:Q', axis=axis).title('Performance')).properties(width=1000) +
199
- alt.Chart(trialset).mark_point(color=black).encode(x=x_enc, y=alt.Y('performance_easy:Q'), tooltip=['session_name','session_datetime','performance_easy','performance']) +
207
+ alt.Chart(trialset).mark_point(color=black).encode(x=x_enc, y=alt.Y('performance_easy:Q'), tooltip=['session_name:N','session_datetime:T','performance_easy:Q','performance:Q']) +
200
208
  alt.Chart(perf_val).mark_text(align='left', dx=6, dy=100, color=black).encode(x=x_enc, y=alt.Y('performance_easy:Q'), text=alt.value('easy')) +
201
209
  alt.Chart(trialset).mark_line(color=orange).encode(x=x_enc, y=alt.Y('performance:Q', axis=axis)) +
202
210
  alt.Chart(trialset).mark_point(color=orange).encode(x=x_enc, y=alt.Y('performance:Q'),
203
- tooltip=['session_name','session_datetime','performance_easy','performance']) +
211
+ tooltip=['session_name:N','session_datetime:T','performance_easy:Q','performance:Q']) +
204
212
  alt.Chart(perf_val).mark_text(align='left', dx=6, dy=100, color=orange).encode(x=x_enc, y=alt.Y('performance:Q'), text=alt.value('all trials'))
205
213
  )
206
214
  ruler_df = pd.DataFrame({'t': [selected_dt]})
@@ -224,7 +232,7 @@ def decisionmaking_tab(schema = None):
224
232
  chartw + make_ruler('weighing_datetime'),
225
233
  charth2o + make_ruler('watering_datetime'),
226
234
  ).resolve_scale(x='shared')
227
- st.altair_chart(linked.interactive(), use_container_width=True)
235
+ st.altair_chart(linked.interactive(), width="stretch")
228
236
 
229
237
  with col_psych:
230
238
 
@@ -251,7 +259,7 @@ def decisionmaking_tab(schema = None):
251
259
  lines = base.mark_line().encode(y=alt.Y('proportion:Q'))
252
260
  st.altair_chart((lines + errorbars + pts).properties(
253
261
  width=400, height=500, title=f'{pd.Timestamp(selected_dt).strftime("%Y-%m-%d %H:%M")}'
254
- ).interactive(), use_container_width=True)
262
+ ).interactive(), width="stretch")
255
263
 
256
264
  # reaction time per condition
257
265
  rt_rows = []
@@ -291,9 +299,9 @@ def decisionmaking_tab(schema = None):
291
299
  rt_lines = rt_base.mark_line().encode(y=alt.Y('median_rt:Q'))
292
300
  st.altair_chart((rt_lines + rt_err + rt_pts).properties(
293
301
  width=400, height=300, title='Reaction time per condition'
294
- ).interactive(), use_container_width=True)
302
+ ).interactive(), width="stretch")
295
303
  else:
296
- st.write('No response data available for this session.')
304
+ st.error('No data available for this session.')
297
305
 
298
306
  if len(snames) > 1:
299
307
  st.slider('Session', min_value=0, max_value=len(snames)-1,
@@ -312,12 +320,11 @@ def decisionmaking_tab(schema = None):
312
320
  st.caption(f"*{n['notetaker']} — {pd.Timestamp(n['note_datetime']).strftime('%Y-%m-%d %H:%M')}*")
313
321
  st.write(n['notes'])
314
322
 
315
- def line_point_plot(data,x,y,tooltip,color):
316
- scatter = alt.Chart(data).mark_point(color = color).encode(
317
- x=x,
318
- y=y,
319
- tooltip=tooltip)
323
+ def line_point_plot(data, x, y, tooltip, color):
324
+ if data is None or not len(data):
325
+ return alt.Chart(pd.DataFrame()).mark_point()
326
+ scatter = alt.Chart(data).mark_point(color=color).encode(
327
+ x=x, y=y, tooltip=tooltip)
320
328
  line = alt.Chart(data).mark_line(color=color).encode(
321
- x=x,
322
- y=y,).properties(width=1000)
323
- return scatter+line
329
+ x=x, y=y).properties(width=1000)
330
+ return scatter + line
@@ -0,0 +1,233 @@
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import numpy as np
4
+ from functools import partial
5
+
6
+ import sys
7
+
8
+ def intro_tab(user_name = None, schema = None):
9
+ @st.cache_data
10
+ def get_subjects():
11
+ if user_name is not None:
12
+ df = pd.DataFrame((schema.Subject() & f'user_name = "{user_name}"').fetch())
13
+ else:
14
+ df = pd.DataFrame(schema.Subject().fetch())
15
+ return df.sort_values("subject_name").reset_index(drop=True)
16
+
17
+ @st.cache_data
18
+ def get_sessions(subject_names):
19
+ dfs = [pd.DataFrame((schema.Session() * schema.Dataset() &
20
+ f'subject_name = "{s}"').fetch()) for s in subject_names]
21
+ dfs = [d for d in dfs if len(d)]
22
+ if not dfs:
23
+ return None
24
+ return pd.concat(dfs).sort_values("session_datetime").reset_index(drop=True)
25
+
26
+ @st.cache_data
27
+ def get_files(dataset_keys_frozen):
28
+ keys = [dict(k) for k in dataset_keys_frozen]
29
+ rows = (schema.Dataset.DataFiles * schema.File & keys).fetch(
30
+ 'subject_name', 'session_name', 'dataset_name',
31
+ 'file_path', 'file_size', 'file_datetime', 'storage', as_dict=True)
32
+ if not rows:
33
+ return pd.DataFrame()
34
+ df = pd.DataFrame(rows)
35
+ df['size'] = df['file_size'].apply(
36
+ lambda b: f'{b/1e9:.2f} GB' if b >= 1e9 else f'{b/1e6:.1f} MB' if b >= 1e6 else f'{b/1e3:.0f} KB')
37
+ return df.drop(columns='file_size')
38
+
39
+ @st.cache_data
40
+ def get_users():
41
+ return list(schema.LabMember.fetch('user_name'))
42
+
43
+ @st.cache_data
44
+ def get_strains():
45
+ return list(schema.Strain().fetch('strain_name'))
46
+
47
+ list_tab, add_tab, edit_tab = st.tabs(['Subjects/Sessions', 'New', 'Update'])
48
+
49
+ with list_tab:
50
+ subjects = get_subjects()
51
+ event_subjects = st.dataframe(
52
+ subjects,
53
+ hide_index=True,
54
+ width="stretch",
55
+ on_select='rerun',
56
+ selection_mode='multi-row',
57
+ key='subjects_table',
58
+ )
59
+ sel_subj_idx = (event_subjects.selection or {}).get('rows', [])
60
+ selected_names = list(subjects.iloc[sel_subj_idx]['subject_name']) if sel_subj_idx else []
61
+ if selected_names:
62
+ st.session_state['subjects_selected'] = selected_names
63
+
64
+ sessions = get_sessions(selected_names) if selected_names else None
65
+ if sessions is None:
66
+ st.write('No subjects selected.')
67
+ else:
68
+ dtype_vals = [s if s is not None else 'None' for s in sessions.dataset_type.values]
69
+ uniqueds = np.unique(dtype_vals)
70
+ metric_cols = st.columns(max(1, len(uniqueds)))
71
+ for col, d in zip(metric_cols, uniqueds):
72
+ col.metric(d, sum(1 for v in dtype_vals if v == d))
73
+
74
+ # color palette for dataset types (light backgrounds, readable text)
75
+ type_palette = ['#d0e8ff', '#ffd6d6', '#d4f5d4', '#fff0cc',
76
+ '#e8d4f5', '#ffd9b3', '#d4f0f5', '#f5d4e8',
77
+ '#e8f5d4', '#f5e8d4']
78
+ dtype_list = [s if s is not None else 'None' for s in sessions['dataset_type'].values]
79
+ unique_types = list(dict.fromkeys(dtype_list)) # preserve order, deduplicate
80
+ type_color = {t: type_palette[i % len(type_palette)] for i, t in enumerate(unique_types)}
81
+
82
+ def _color_rows(row):
83
+ dt = row.get('dataset_type')
84
+ dt = dt if dt is not None else 'None'
85
+ bg = type_color.get(dt, '')
86
+ return [f'background-color: {bg}'] * len(row)
87
+
88
+ sess_styled = sessions.style.apply(_color_rows, axis=1)
89
+ event_sessions = st.dataframe(
90
+ sess_styled,
91
+ hide_index=True,
92
+ width="stretch",
93
+ on_select='rerun',
94
+ selection_mode='multi-row',
95
+ key='sessions_table',
96
+ )
97
+ sel_indices = (event_sessions.selection or {}).get('rows', [])
98
+ sel_datasets = sessions.iloc[sel_indices] if sel_indices else pd.DataFrame()
99
+ if len(sel_datasets):
100
+ dataset_keys = sel_datasets[['subject_name', 'session_name', 'dataset_name']].to_dict('records')
101
+ file_stats = get_files(tuple(frozenset(d.items()) for d in dataset_keys))
102
+ if len(file_stats):
103
+ st.write('### Files')
104
+ ds_order = list(dict.fromkeys(file_stats['dataset_name'].values))
105
+ even_color, odd_color = '#dceeff', '#fff3dc'
106
+ ds_color = {ds: (even_color if i % 2 == 0 else odd_color)
107
+ for i, ds in enumerate(ds_order)}
108
+
109
+ def _color_file_rows(row):
110
+ bg = ds_color.get(row['dataset_name'], '')
111
+ return [f'background-color: {bg}'] * len(row)
112
+
113
+ file_styled = file_stats.style.apply(_color_file_rows, axis=1)
114
+ event_files = st.dataframe(
115
+ file_styled,
116
+ hide_index=True,
117
+ width="stretch",
118
+ on_select='rerun',
119
+ selection_mode='multi-row',
120
+ key='files_table',
121
+ )
122
+ sel_file_idx = (event_files.selection or {}).get('rows', [])
123
+ if sel_file_idx:
124
+ sel_files = file_stats.iloc[sel_file_idx]
125
+ st.caption(f'{len(sel_file_idx)} file(s) selected')
126
+ if st.button('Download files', key='download_files_btn'):
127
+ keys = sel_files[['file_path', 'storage']].to_dict('records')
128
+ with st.spinner(f'Downloading {len(sel_file_idx)} file(s)…'):
129
+ local = (schema.File & keys).get()
130
+ if local:
131
+ st.success(f'Downloaded {len(local)} file(s).')
132
+ for p in local:
133
+ st.code(str(p))
134
+ else:
135
+ st.write('No files found for the selected datasets.')
136
+
137
+ with add_tab:
138
+ users = get_users()
139
+ available_strains = get_strains()
140
+ project_users = list((schema.Project.User() & f'project_name = "{schema.schema_project}"').fetch('user_name'))
141
+ userindex = users.index(project_users[0]) if project_users and project_users[0] in users else None
142
+ with st.form('add_subject'):
143
+ insert_dict = dict()
144
+ insert_dict['user_name'] = st.selectbox('User Name', users, index=userindex)
145
+ insert_dict['subject_name'] = st.text_input('Subject ID', value=None)
146
+ insert_dict['subject_dob'] = st.date_input('Date of Birth')
147
+ sex = st.selectbox('Sex', ['M', 'F', 'Unknown'])
148
+ insert_dict['subject_sex'] = 'U' if sex == 'Unknown' else sex
149
+ insert_dict['strain_name'] = st.selectbox('Strain', available_strains)
150
+ if st.form_submit_button('Add Subject', type='primary'):
151
+ schema.Subject().insert1(insert_dict)
152
+ get_subjects.clear()
153
+ st.success(f'Added subject {insert_dict["subject_name"]}.')
154
+ st.rerun()
155
+
156
+ with edit_tab:
157
+ users = get_users()
158
+ available_strains = get_strains()
159
+ selected_names = st.session_state.get('subjects_selected', [])
160
+ if not selected_names:
161
+ st.info('Select a subject in the Subjects/Sessions tab first.')
162
+ else:
163
+ selected_subject = selected_names[0]
164
+ st.write(f'### Edit subject: {selected_subject}')
165
+ with st.form('edit_subject'):
166
+ edit_dict = (schema.Subject & dict(subject_name=selected_subject)).fetch1()
167
+ edit_dict['subject_dob'] = st.date_input('Date of Birth', value=edit_dict['subject_dob'])
168
+ edit_dict['subject_sex'] = st.selectbox('Sex', ['M', 'F', 'U'],
169
+ index=['M', 'F', 'U'].index(edit_dict['subject_sex']))
170
+ edit_dict['strain_name'] = st.selectbox('Strain', available_strains,
171
+ index=available_strains.index(edit_dict['strain_name']))
172
+ if st.form_submit_button('Save changes', type='primary'):
173
+ schema.Subject().update1(edit_dict)
174
+ get_subjects.clear()
175
+ st.success(f'Updated {selected_subject}.')
176
+ st.rerun()
177
+ st.set_page_config(
178
+ page_title="labdata dashboard",
179
+ page_icon="🧪",
180
+ layout="wide",
181
+ initial_sidebar_state="auto")
182
+
183
+
184
+ from procedures import procedures_tab
185
+ from notes import notes_tab
186
+ from sorting import sorting_tab
187
+ from segmentation import segmentation_tab
188
+ from video import video_tab
189
+ from decisiontask import decisionmaking_tab
190
+ from compute import compute_tab
191
+
192
+ @st.cache_resource
193
+ def load_schema(project):
194
+ # load project
195
+ from labdata import load_project_schema
196
+ return load_project_schema(project)
197
+
198
+ project = None
199
+ show_console = False
200
+ for k in sys.argv:
201
+ if 'project=' in k:
202
+ project = k.replace('project=','')
203
+ if k == 'console=1':
204
+ from console import console_tab
205
+ show_console = True
206
+ with st.spinner(f'Connecting to {"project " + project if project else "database"}…'):
207
+ schema = load_schema(project)
208
+
209
+ page_names_to_funcs = {
210
+ "**Subjects**": partial(intro_tab, schema = schema),
211
+ "**Procedures**": partial(procedures_tab, schema = schema),
212
+ "**Notes**": partial(notes_tab, schema = schema),
213
+ "**Spike sorting**": partial(sorting_tab, schema = schema),
214
+ "**Cell Segmentation**": partial(segmentation_tab, schema = schema),
215
+ "**Video**": partial(video_tab, schema = schema),
216
+ "**Decision-making behavior**": partial(decisionmaking_tab, schema = schema),
217
+ "**Compute/Upload**": partial(compute_tab, schema = schema),
218
+ }
219
+
220
+ if show_console:
221
+ page_names_to_funcs["**Console**"] = partial(console_tab, schema=schema)
222
+
223
+ from labdata import plugins
224
+ for p in plugins.keys():
225
+ if hasattr(plugins[p],'dashboard_function'):
226
+ page_names_to_funcs[plugins[p].dashboard_name] = partial(plugins[p].dashboard_function, schema = schema)
227
+
228
+
229
+ tab_name = st.sidebar.radio(
230
+ "``labdata dashboard``",
231
+ page_names_to_funcs.keys())
232
+
233
+ page_names_to_funcs[tab_name]()