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.
- {labdata-0.0.24 → labdata-0.1.0}/PKG-INFO +2 -2
- labdata-0.1.0/labdata/__init__.py +11 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/cli.py +4 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/dashboard/compute.py +2 -2
- labdata-0.1.0/labdata/dashboard/console.py +166 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/dashboard/decisiontask.py +26 -19
- labdata-0.1.0/labdata/dashboard/index.py +233 -0
- labdata-0.1.0/labdata/dashboard/notes.py +184 -0
- labdata-0.1.0/labdata/dashboard/procedures.py +284 -0
- labdata-0.1.0/labdata/dashboard/segmentation.py +564 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/dashboard/sorting.py +1 -1
- {labdata-0.0.24 → labdata-0.1.0}/labdata/dashboard/video.py +1 -1
- {labdata-0.0.24 → labdata-0.1.0}/labdata/stacks.py +18 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/utils.py +42 -3
- {labdata-0.0.24 → labdata-0.1.0}/labdata.egg-info/PKG-INFO +2 -2
- {labdata-0.0.24 → labdata-0.1.0}/labdata.egg-info/SOURCES.txt +3 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata.egg-info/requires.txt +1 -1
- {labdata-0.0.24 → labdata-0.1.0}/pyproject.toml +1 -1
- labdata-0.0.24/labdata/__init__.py +0 -16
- labdata-0.0.24/labdata/dashboard/index.py +0 -408
- labdata-0.0.24/labdata/dashboard/segmentation.py +0 -49
- {labdata-0.0.24 → labdata-0.1.0}/LICENSE +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/README.md +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/compute/__init__.py +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/compute/caiman.py +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/compute/ec2.py +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/compute/ephys.py +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/compute/pose.py +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/compute/schedulers.py +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/compute/singularity.py +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/compute/suite2p.py +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/compute/utils.py +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/copy.py +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/dashboard/__init__.py +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/dashboard/explorer/__init__.py +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/dashboard/explorer/cellsegmentation.py +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/dashboard/explorer/spikesorting.py +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/export.py +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/rules/__init__.py +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/rules/ephys.py +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/rules/imaging.py +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/rules/utils.py +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/s3.py +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/schema/__init__.py +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/schema/ephys.py +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/schema/general.py +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/schema/histology.py +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/schema/onephoton.py +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/schema/procedures.py +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/schema/tasks.py +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/schema/twophoton.py +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/schema/utils.py +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/schema/video.py +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata/widgets.py +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata.egg-info/dependency_links.txt +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata.egg-info/entry_points.txt +0 -0
- {labdata-0.0.24 → labdata-0.1.0}/labdata.egg-info/top_level.txt +0 -0
- {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
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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
|
|
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[[-
|
|
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(),
|
|
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(),
|
|
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(),
|
|
302
|
+
).interactive(), width="stretch")
|
|
295
303
|
else:
|
|
296
|
-
st.
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
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]()
|