labdata 0.1.9__tar.gz → 0.1.11__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.1.9 → labdata-0.1.11}/PKG-INFO +1 -1
- {labdata-0.1.9 → labdata-0.1.11}/labdata/__init__.py +1 -1
- {labdata-0.1.9 → labdata-0.1.11}/labdata/compute/pose.py +4 -1
- labdata-0.1.11/labdata/dashboard/docstrings.py +265 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/dashboard/index.py +32 -12
- {labdata-0.1.9 → labdata-0.1.11}/labdata/dashboard/schema_graph.py +49 -17
- {labdata-0.1.9 → labdata-0.1.11}/labdata/dashboard/sorting.py +149 -52
- {labdata-0.1.9 → labdata-0.1.11}/labdata/schema/ephys.py +24 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/schema/onephoton.py +1 -1
- {labdata-0.1.9 → labdata-0.1.11}/labdata.egg-info/PKG-INFO +1 -1
- {labdata-0.1.9 → labdata-0.1.11}/labdata.egg-info/SOURCES.txt +1 -0
- {labdata-0.1.9 → labdata-0.1.11}/LICENSE +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/README.md +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/cli.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/compute/__init__.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/compute/caiman.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/compute/ec2.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/compute/ephys.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/compute/schedulers.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/compute/singularity.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/compute/suite2p.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/compute/utils.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/copy.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/dashboard/__init__.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/dashboard/admin.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/dashboard/ai_utils.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/dashboard/compute.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/dashboard/console.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/dashboard/decisiontask.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/dashboard/explorer/__init__.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/dashboard/explorer/cellsegmentation.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/dashboard/explorer/spikesorting.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/dashboard/notes.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/dashboard/procedures.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/dashboard/segmentation.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/dashboard/video.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/export.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/rules/__init__.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/rules/ephys.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/rules/imaging.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/rules/utils.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/s3.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/schema/__init__.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/schema/general.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/schema/histology.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/schema/procedures.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/schema/tasks.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/schema/twophoton.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/schema/utils.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/schema/video.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/stacks.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/utils.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata/widgets.py +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata.egg-info/dependency_links.txt +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata.egg-info/entry_points.txt +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata.egg-info/requires.txt +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/labdata.egg-info/top_level.txt +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/pyproject.toml +0 -0
- {labdata-0.1.9 → labdata-0.1.11}/setup.cfg +0 -0
|
@@ -63,7 +63,10 @@ class DeeplabcutCompute(BaseCompute):
|
|
|
63
63
|
if self.parameters['mode'] == 'train':
|
|
64
64
|
for i,r in parameters.iterrows():
|
|
65
65
|
# go through every parameter and label_set
|
|
66
|
-
|
|
66
|
+
flag = False
|
|
67
|
+
if type(r.parameters_dict) in [str]:
|
|
68
|
+
flag = (self.model_parameters == json.loads(r.parameters_dict))
|
|
69
|
+
if (flag and
|
|
67
70
|
self.parameters['model_num'] is None and
|
|
68
71
|
self.parameters['pose_label_set_num'] == r['pose_label_set_num']):
|
|
69
72
|
model_num = r.model_num
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""Docstring viewer tab — browse DataJoint table docstrings, definitions and column comments."""
|
|
2
|
+
import re
|
|
3
|
+
import inspect
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import streamlit as st
|
|
6
|
+
|
|
7
|
+
from schema_graph import (
|
|
8
|
+
_enumerate_tables,
|
|
9
|
+
_get_tier,
|
|
10
|
+
_get_schema_type,
|
|
11
|
+
_get_category,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@st.cache_data(ttl=600)
|
|
16
|
+
def _collect_tables(_schema):
|
|
17
|
+
"""Walk the schema and collect per-table metadata for browsing.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
_schema : module
|
|
22
|
+
Loaded project schema module (underscore prefix skips Streamlit hashing).
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
pandas.DataFrame
|
|
27
|
+
One row per master and part table with columns ``key``, ``schema``,
|
|
28
|
+
``tier``, ``category`` and ``full_table_name``.
|
|
29
|
+
"""
|
|
30
|
+
masters, parts_map = _enumerate_tables(_schema)
|
|
31
|
+
rows = []
|
|
32
|
+
|
|
33
|
+
def _add(key, cls):
|
|
34
|
+
try:
|
|
35
|
+
full_name = cls.full_table_name
|
|
36
|
+
except Exception:
|
|
37
|
+
return
|
|
38
|
+
sql_name = full_name.split('.')[-1].strip('`')
|
|
39
|
+
rows.append(dict(
|
|
40
|
+
key=key,
|
|
41
|
+
schema=_get_schema_type(full_name, _schema),
|
|
42
|
+
tier=_get_tier(sql_name),
|
|
43
|
+
category=_get_category(key),
|
|
44
|
+
full_table_name=full_name,
|
|
45
|
+
))
|
|
46
|
+
|
|
47
|
+
for key, cls in masters.items():
|
|
48
|
+
_add(key, cls)
|
|
49
|
+
for part_key, part_cls in parts_map.get(key, {}).items():
|
|
50
|
+
_add(part_key, part_cls)
|
|
51
|
+
|
|
52
|
+
df = pd.DataFrame(rows)
|
|
53
|
+
if len(df):
|
|
54
|
+
df = df.sort_values(['schema', 'category', 'key']).reset_index(drop=True)
|
|
55
|
+
return df
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _resolve_table(schema, key):
|
|
59
|
+
"""Resolve a master or ``Master.Part`` key to its table class."""
|
|
60
|
+
if '.' in key:
|
|
61
|
+
master_name, part_name = key.split('.', 1)
|
|
62
|
+
master = getattr(schema, master_name, None)
|
|
63
|
+
return getattr(master, part_name, None) if master is not None else None
|
|
64
|
+
return getattr(schema, key, None)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _class_docstring(cls):
|
|
68
|
+
"""Return the authored class docstring, without datajoint's appended definition.
|
|
69
|
+
|
|
70
|
+
On ``declare`` DataJoint appends ``\\nTable definition:\\n\\n<definition>`` to
|
|
71
|
+
``__doc__`` (datajoint/schemas.py). We strip that here so it isn't rendered
|
|
72
|
+
twice and so the leftover column-0 line doesn't defeat ``cleandoc``'s dedent
|
|
73
|
+
(which would render the real docstring as a code block).
|
|
74
|
+
"""
|
|
75
|
+
raw = (cls.__doc__ or '').split('\nTable definition:\n\n', 1)[0]
|
|
76
|
+
return inspect.cleandoc(raw).strip()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _rst_to_md(text):
|
|
80
|
+
"""Convert the few reStructuredText constructs we see to markdown."""
|
|
81
|
+
return re.sub(r'``([^`]+)``', r'`\1`', text) # ``literal`` -> `literal`
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _render_docstring(doc):
|
|
85
|
+
"""Render a numpydoc/RST docstring as markdown for ``st.markdown``.
|
|
86
|
+
|
|
87
|
+
Uses the ``numpydoc`` parser so every section (Parameters, Returns,
|
|
88
|
+
Raises, Notes, See Also, …) renders consistently and prose is never
|
|
89
|
+
swallowed into an indented code block. Falls back to a plain dedent if
|
|
90
|
+
``numpydoc`` is unavailable or the parse fails.
|
|
91
|
+
"""
|
|
92
|
+
try:
|
|
93
|
+
from numpydoc.docscrape import NumpyDocString
|
|
94
|
+
nd = NumpyDocString(doc)
|
|
95
|
+
except Exception:
|
|
96
|
+
return _rst_to_md(inspect.cleandoc(doc))
|
|
97
|
+
|
|
98
|
+
parts = []
|
|
99
|
+
summary = '\n'.join(nd['Summary']).strip()
|
|
100
|
+
if summary:
|
|
101
|
+
parts.append(summary)
|
|
102
|
+
extended = '\n'.join(nd['Extended Summary']).strip()
|
|
103
|
+
if extended:
|
|
104
|
+
parts.append(extended)
|
|
105
|
+
|
|
106
|
+
for section in ('Parameters', 'Returns', 'Yields', 'Raises', 'Attributes'):
|
|
107
|
+
items = nd[section]
|
|
108
|
+
if not items:
|
|
109
|
+
continue
|
|
110
|
+
bullets = []
|
|
111
|
+
for it in items:
|
|
112
|
+
head = f'**{it.name}**' if it.name else ''
|
|
113
|
+
if it.type:
|
|
114
|
+
head += f' — *{it.type}*' if head else f'*{it.type}*'
|
|
115
|
+
desc = ' '.join(l.strip() for l in it.desc).strip()
|
|
116
|
+
bullets.append(f'- {head}' + (f': {desc}' if desc else ''))
|
|
117
|
+
parts.append(f'**{section}**\n\n' + '\n'.join(bullets))
|
|
118
|
+
|
|
119
|
+
see_also = nd['See Also']
|
|
120
|
+
if see_also:
|
|
121
|
+
bullets = []
|
|
122
|
+
for refs, desc in see_also:
|
|
123
|
+
names = ', '.join(f'`{n}`' for n, _ in refs)
|
|
124
|
+
desc = ' '.join(d.strip() for d in desc).strip()
|
|
125
|
+
bullets.append(f'- {names}' + (f': {desc}' if desc else ''))
|
|
126
|
+
parts.append('**See Also**\n\n' + '\n'.join(bullets))
|
|
127
|
+
|
|
128
|
+
for section in ('Notes', 'References', 'Examples'):
|
|
129
|
+
body = '\n'.join(nd[section]).strip()
|
|
130
|
+
if body:
|
|
131
|
+
parts.append(f'**{section}**\n\n{body}')
|
|
132
|
+
|
|
133
|
+
return _rst_to_md('\n\n'.join(parts))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _methods_table(cls):
|
|
137
|
+
"""Return [(name, signature, doc)] for methods defined on the table.
|
|
138
|
+
|
|
139
|
+
Only methods implemented in the project (not inherited from DataJoint base
|
|
140
|
+
classes) are included — e.g. ``File.get``.
|
|
141
|
+
"""
|
|
142
|
+
rows = []
|
|
143
|
+
for name in sorted(dir(cls)):
|
|
144
|
+
if name.startswith('_'):
|
|
145
|
+
continue
|
|
146
|
+
try:
|
|
147
|
+
attr = inspect.getattr_static(cls, name)
|
|
148
|
+
except Exception:
|
|
149
|
+
continue
|
|
150
|
+
func = attr.__func__ if isinstance(attr, (classmethod, staticmethod)) else attr
|
|
151
|
+
if not (inspect.isfunction(func) or inspect.ismethod(func)):
|
|
152
|
+
continue
|
|
153
|
+
if (getattr(func, '__module__', '') or '').startswith('datajoint'):
|
|
154
|
+
continue # skip dj-provided methods
|
|
155
|
+
try:
|
|
156
|
+
sig = str(inspect.signature(func))
|
|
157
|
+
except (ValueError, TypeError):
|
|
158
|
+
sig = '(...)'
|
|
159
|
+
rows.append((name, sig, inspect.getdoc(func) or ''))
|
|
160
|
+
return rows
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _attribute_table(cls):
|
|
164
|
+
"""Return a DataFrame of (column, key, type, null, default, comment) or None."""
|
|
165
|
+
try:
|
|
166
|
+
attrs = cls.heading.attributes
|
|
167
|
+
except Exception:
|
|
168
|
+
return None
|
|
169
|
+
rows = []
|
|
170
|
+
for name, a in attrs.items():
|
|
171
|
+
rows.append(dict(
|
|
172
|
+
column=name,
|
|
173
|
+
key='🔑' if a.in_key else '',
|
|
174
|
+
type=a.type,
|
|
175
|
+
null='✓' if a.nullable else '',
|
|
176
|
+
default='' if a.default is None else str(a.default),
|
|
177
|
+
comment=a.comment or '',
|
|
178
|
+
))
|
|
179
|
+
return pd.DataFrame(rows) if rows else None
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def docstrings_tab(schema=None):
|
|
183
|
+
"""Streamlit tab: browse table docstrings, DataJoint definitions and column comments.
|
|
184
|
+
|
|
185
|
+
Parameters
|
|
186
|
+
----------
|
|
187
|
+
schema : module or None, optional
|
|
188
|
+
Project schema module. Loaded from session state when ``None``.
|
|
189
|
+
"""
|
|
190
|
+
if schema is None:
|
|
191
|
+
schema = st.session_state.get('schema')
|
|
192
|
+
if schema is None:
|
|
193
|
+
st.error('No schema available.')
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
st.write('## Table reference')
|
|
197
|
+
|
|
198
|
+
tables = _collect_tables(schema)
|
|
199
|
+
if not len(tables):
|
|
200
|
+
st.info('No tables found in the loaded schema.')
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
filt_col, cat_col, search_col = st.columns([1, 1, 2])
|
|
204
|
+
with filt_col:
|
|
205
|
+
schemas = ['(all)'] + sorted(tables['schema'].unique())
|
|
206
|
+
sel_schema = st.selectbox('Schema', schemas, key='docstr_schema')
|
|
207
|
+
with cat_col:
|
|
208
|
+
cats = ['(all)'] + sorted(tables['category'].unique())
|
|
209
|
+
sel_cat = st.selectbox('Category', cats, key='docstr_cat')
|
|
210
|
+
with search_col:
|
|
211
|
+
query = st.text_input('Search', key='docstr_search',
|
|
212
|
+
placeholder='Filter table names…').strip().lower()
|
|
213
|
+
|
|
214
|
+
view = tables
|
|
215
|
+
if sel_schema != '(all)':
|
|
216
|
+
view = view[view['schema'] == sel_schema]
|
|
217
|
+
if sel_cat != '(all)':
|
|
218
|
+
view = view[view['category'] == sel_cat]
|
|
219
|
+
if query:
|
|
220
|
+
view = view[view['key'].str.lower().str.contains(query)]
|
|
221
|
+
|
|
222
|
+
if not len(view):
|
|
223
|
+
st.info('No tables match the current filters.')
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
sel_key = st.selectbox(
|
|
227
|
+
f'Table ({len(view)})',
|
|
228
|
+
list(view['key']),
|
|
229
|
+
key='docstr_table',
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
cls = _resolve_table(schema, sel_key)
|
|
233
|
+
if cls is None:
|
|
234
|
+
st.error(f'Could not resolve table `{sel_key}`.')
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
meta = tables[tables['key'] == sel_key].iloc[0]
|
|
238
|
+
st.caption(f'`{meta["full_table_name"]}` — {meta["schema"]} schema · '
|
|
239
|
+
f'{meta["tier"]} · {meta["category"]}')
|
|
240
|
+
|
|
241
|
+
doc = _class_docstring(cls)
|
|
242
|
+
if doc:
|
|
243
|
+
st.markdown(_render_docstring(doc))
|
|
244
|
+
else:
|
|
245
|
+
st.caption('_No class docstring._')
|
|
246
|
+
|
|
247
|
+
definition = getattr(cls, 'definition', None)
|
|
248
|
+
if definition:
|
|
249
|
+
st.write('### Definition')
|
|
250
|
+
st.code(inspect.cleandoc(definition), language='text')
|
|
251
|
+
|
|
252
|
+
attr_df = _attribute_table(cls)
|
|
253
|
+
if attr_df is not None:
|
|
254
|
+
st.write('### Columns')
|
|
255
|
+
st.dataframe(attr_df, hide_index=True, width='stretch')
|
|
256
|
+
|
|
257
|
+
methods = _methods_table(cls)
|
|
258
|
+
if methods:
|
|
259
|
+
st.write('### Methods')
|
|
260
|
+
for name, sig, mdoc in methods:
|
|
261
|
+
st.markdown(f'**`{name}{sig}`**')
|
|
262
|
+
if mdoc:
|
|
263
|
+
st.markdown(_render_docstring(mdoc))
|
|
264
|
+
else:
|
|
265
|
+
st.caption('_No docstring._')
|
|
@@ -294,6 +294,7 @@ from segmentation import segmentation_tab
|
|
|
294
294
|
from video import video_tab
|
|
295
295
|
from decisiontask import decisionmaking_tab
|
|
296
296
|
from compute import compute_tab
|
|
297
|
+
from docstrings import docstrings_tab
|
|
297
298
|
|
|
298
299
|
@st.cache_resource
|
|
299
300
|
def load_schema(project):
|
|
@@ -365,16 +366,17 @@ if show_admin:
|
|
|
365
366
|
get_schema_sizes(schema)
|
|
366
367
|
get_project_users(schema)
|
|
367
368
|
st.session_state['project'] = selected
|
|
368
|
-
|
|
369
|
+
for _k in ('active_tab', 'main_nav', 'ref_nav'):
|
|
370
|
+
st.session_state.pop(_k, None)
|
|
369
371
|
st.rerun()
|
|
370
372
|
|
|
371
373
|
page_names_to_funcs = {}
|
|
372
374
|
|
|
373
375
|
if show_admin:
|
|
374
376
|
from admin import admin_tab
|
|
375
|
-
from schema_graph import schema_graph_tab
|
|
376
377
|
page_names_to_funcs["**Admin**"] = partial(admin_tab, schema=schema)
|
|
377
|
-
|
|
378
|
+
|
|
379
|
+
from schema_graph import schema_graph_tab
|
|
378
380
|
|
|
379
381
|
page_names_to_funcs.update({
|
|
380
382
|
"**Subjects**": partial(intro_tab, schema = schema),
|
|
@@ -395,16 +397,34 @@ for p in plugins.keys():
|
|
|
395
397
|
if hasattr(plugins[p],'dashboard_function'):
|
|
396
398
|
page_names_to_funcs[plugins[p].dashboard_name] = partial(plugins[p].dashboard_function, schema = schema)
|
|
397
399
|
|
|
400
|
+
# Reference tabs live at the bottom of the selector, below the docs link.
|
|
401
|
+
page_names_to_funcs["**Table reference**"] = partial(docstrings_tab, schema = schema)
|
|
402
|
+
page_names_to_funcs["**Tables graph**"] = partial(schema_graph_tab, schema = schema)
|
|
403
|
+
|
|
404
|
+
# Split the selector into the main tabs and the reference tabs (bottom group).
|
|
405
|
+
_ref_names = [n for n in ("**Table reference**", "**Tables graph**") if n in page_names_to_funcs]
|
|
406
|
+
_main_names = [n for n in page_names_to_funcs if n not in _ref_names]
|
|
407
|
+
|
|
408
|
+
if st.session_state.get('active_tab') not in page_names_to_funcs:
|
|
409
|
+
st.session_state['active_tab'] = _main_names[0]
|
|
410
|
+
st.session_state['main_nav'] = _main_names[0]
|
|
411
|
+
st.session_state['ref_nav'] = None
|
|
412
|
+
|
|
413
|
+
def _nav_main():
|
|
414
|
+
if st.session_state.get('main_nav') is not None:
|
|
415
|
+
st.session_state['active_tab'] = st.session_state['main_nav']
|
|
416
|
+
st.session_state['ref_nav'] = None
|
|
417
|
+
|
|
418
|
+
def _nav_ref():
|
|
419
|
+
if st.session_state.get('ref_nav') is not None:
|
|
420
|
+
st.session_state['active_tab'] = st.session_state['ref_nav']
|
|
421
|
+
st.session_state['main_nav'] = None
|
|
422
|
+
|
|
423
|
+
st.sidebar.radio("``labdata dashboard``", _main_names, key='main_nav', on_change=_nav_main)
|
|
424
|
+
st.sidebar.radio("Reference · [📖 Documentation](https://jcouto.github.io/labdata-docs/)",
|
|
425
|
+
_ref_names, key='ref_nav', index=None, on_change=_nav_ref)
|
|
398
426
|
|
|
399
|
-
|
|
400
|
-
_tab_default = st.session_state.get('sidebar_tab', _tab_options[0])
|
|
401
|
-
if _tab_default not in _tab_options:
|
|
402
|
-
_tab_default = _tab_options[0]
|
|
403
|
-
tab_name = st.sidebar.radio(
|
|
404
|
-
"``labdata dashboard``",
|
|
405
|
-
_tab_options,
|
|
406
|
-
index=_tab_options.index(_tab_default),
|
|
407
|
-
key='sidebar_tab')
|
|
427
|
+
tab_name = st.session_state['active_tab']
|
|
408
428
|
|
|
409
429
|
try:
|
|
410
430
|
page_names_to_funcs[tab_name]()
|
|
@@ -30,11 +30,11 @@ _CATEGORY_RULES = [
|
|
|
30
30
|
'choice', 'running']),
|
|
31
31
|
]
|
|
32
32
|
# Horizontal band center per category (pixels); spacing between bands
|
|
33
|
-
_CAT_SPACING =
|
|
33
|
+
_CAT_SPACING = 700
|
|
34
34
|
_CATEGORY_X = {name: i * _CAT_SPACING for i, (name, _) in enumerate(_CATEGORY_RULES)}
|
|
35
35
|
_CATEGORY_X['other'] = len(_CATEGORY_RULES) * _CAT_SPACING
|
|
36
|
-
_LEVEL_SPACING =
|
|
37
|
-
_NODE_JITTER =
|
|
36
|
+
_LEVEL_SPACING = 220 # vertical pixels between topological levels
|
|
37
|
+
_NODE_JITTER = 170 # horizontal spread when multiple nodes share category+level
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
def _get_category(key):
|
|
@@ -95,8 +95,24 @@ def _enumerate_tables(schema_module):
|
|
|
95
95
|
return masters, parts
|
|
96
96
|
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
@st.cache_data(ttl=600)
|
|
99
|
+
def _approx_row_count(full_table_name):
|
|
100
|
+
"""Fast (approximate) row count from information_schema.
|
|
101
|
+
|
|
102
|
+
Gets the estimate from ``TABLE_ROWS`` which is faster than ``SELECT COUNT(*)``.
|
|
103
|
+
Returns the estimate, or ``None`` if it can't be determined.
|
|
104
|
+
"""
|
|
105
|
+
try:
|
|
106
|
+
db, tbl = (p.strip('`') for p in full_table_name.split('.'))
|
|
107
|
+
res = dj.conn().query(
|
|
108
|
+
"SELECT table_rows FROM information_schema.tables "
|
|
109
|
+
"WHERE table_schema=%s AND table_name=%s", (db, tbl)).fetchone()
|
|
110
|
+
return int(res[0]) if res and res[0] is not None else None
|
|
111
|
+
except Exception:
|
|
112
|
+
return None
|
|
99
113
|
|
|
114
|
+
|
|
115
|
+
# Cached graph data
|
|
100
116
|
@st.cache_data(ttl=600)
|
|
101
117
|
def build_graph_data(_schema, included_schemas, included_tiers):
|
|
102
118
|
"""
|
|
@@ -195,6 +211,16 @@ def build_graph_data(_schema, included_schemas, included_tiers):
|
|
|
195
211
|
x_offset = (i - (n - 1) / 2) * _NODE_JITTER
|
|
196
212
|
positions[k] = (cx + x_offset, cy)
|
|
197
213
|
|
|
214
|
+
# Mean-center coordinates around the origin so the graph renders centered
|
|
215
|
+
# (all-positive x otherwise puts it to the right and vis.js auto-fit slides
|
|
216
|
+
# it to center on every rerun, which reads as a jump on click).
|
|
217
|
+
if positions:
|
|
218
|
+
xs = [p[0] for p in positions.values()]
|
|
219
|
+
ys = [p[1] for p in positions.values()]
|
|
220
|
+
ox = (min(xs) + max(xs)) / 2
|
|
221
|
+
oy = (min(ys) + max(ys)) / 2
|
|
222
|
+
positions = {k: (x - ox, y - oy) for k, (x, y) in positions.items()}
|
|
223
|
+
|
|
198
224
|
nodes = [
|
|
199
225
|
{**node_meta[n], 'key': n, 'level': levels.get(n, 0),
|
|
200
226
|
'x': positions[n][0], 'y': positions[n][1]}
|
|
@@ -219,7 +245,7 @@ def build_graph_data(_schema, included_schemas, included_tiers):
|
|
|
219
245
|
continue
|
|
220
246
|
n_parts = len(part_dict)
|
|
221
247
|
px = master_x + (idx - (n_parts - 1) / 2) * _NODE_JITTER
|
|
222
|
-
py =
|
|
248
|
+
py = positions[master_key][1] + _LEVEL_SPACING
|
|
223
249
|
pn.append(dict(
|
|
224
250
|
key=part_key,
|
|
225
251
|
display=part_key.split('.')[-1],
|
|
@@ -242,13 +268,7 @@ def build_graph_data(_schema, included_schemas, included_tiers):
|
|
|
242
268
|
# Tab entry point
|
|
243
269
|
|
|
244
270
|
def schema_graph_tab(schema, **kwargs):
|
|
245
|
-
'''Streamlit tab: interactive
|
|
246
|
-
|
|
247
|
-
Renders a physics-enabled force-directed graph (via ``streamlit-agraph``)
|
|
248
|
-
where nodes are tables colour-coded by category (lab, subject, ephys,
|
|
249
|
-
imaging, behaviour…) and edges show foreign-key dependencies. Nodes can
|
|
250
|
-
be filtered by schema type (Manual, Computed, Imported, Lookup) and by
|
|
251
|
-
category.
|
|
271
|
+
'''Streamlit tab: interactive datajoint schema dependency graph.
|
|
252
272
|
|
|
253
273
|
Parameters
|
|
254
274
|
----------
|
|
@@ -341,7 +361,9 @@ def schema_graph_tab(schema, **kwargs):
|
|
|
341
361
|
color='#555555', arrows='to',
|
|
342
362
|
))
|
|
343
363
|
|
|
344
|
-
# Config
|
|
364
|
+
# Config. Seed coordinates are mean-centered (see build_graph_data) so the
|
|
365
|
+
# graph starts centered and vis.js's auto-fit has nothing to slide on a
|
|
366
|
+
# click-triggered rerun (that was the "jump from right to center").
|
|
345
367
|
config = Config(
|
|
346
368
|
width='100%',
|
|
347
369
|
height=500,
|
|
@@ -349,6 +371,17 @@ def schema_graph_tab(schema, **kwargs):
|
|
|
349
371
|
physics=True,
|
|
350
372
|
hierarchical=False,
|
|
351
373
|
)
|
|
374
|
+
# barnesHut force tuning: with physics on, these (not the seed spacing)
|
|
375
|
+
# determine the final layout density. Weaker repulsion + stronger central
|
|
376
|
+
# gravity + shorter springs => a more compact cluster.
|
|
377
|
+
config.physics['barnesHut'] = {
|
|
378
|
+
'gravitationalConstant': -800,
|
|
379
|
+
'centralGravity': 0.5,
|
|
380
|
+
'springLength': 90,
|
|
381
|
+
'springConstant': 0.05,
|
|
382
|
+
'damping': 0.3,
|
|
383
|
+
'avoidOverlap': 0.2,
|
|
384
|
+
}
|
|
352
385
|
|
|
353
386
|
# Render
|
|
354
387
|
clicked = agraph(nodes=agraph_nodes, edges=agraph_edges, config=config)
|
|
@@ -381,10 +414,9 @@ def schema_graph_tab(schema, **kwargs):
|
|
|
381
414
|
if cls:
|
|
382
415
|
break
|
|
383
416
|
if cls:
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
pass
|
|
417
|
+
approx = _approx_row_count(node['full_table_name'])
|
|
418
|
+
if approx is not None:
|
|
419
|
+
st.metric('Rows (approx.)', f'~{approx:,}')
|
|
388
420
|
|
|
389
421
|
|
|
390
422
|
st.caption(
|
|
@@ -20,9 +20,10 @@ def _fetch_unit_data(_schema, subject_name, session_name, dataset_name,
|
|
|
20
20
|
probe_num=probe_num)
|
|
21
21
|
unit_keys = [dict(unit_id=u) for u in unit_ids_tuple]
|
|
22
22
|
|
|
23
|
-
# spike times + amplitudes from SpikeSorting.Unit
|
|
23
|
+
# spike times + amplitudes + positions from SpikeSorting.Unit
|
|
24
24
|
unit_rows = (_schema.SpikeSorting.Unit & key & unit_keys).fetch(
|
|
25
|
-
'unit_id', 'spike_times', 'spike_amplitudes',
|
|
25
|
+
'unit_id', 'spike_times', 'spike_amplitudes', 'spike_positions',
|
|
26
|
+
as_dict=True)
|
|
26
27
|
|
|
27
28
|
# sampling-rate / sync conversion (matches get_spike_times logic)
|
|
28
29
|
try:
|
|
@@ -39,6 +40,7 @@ def _fetch_unit_data(_schema, subject_name, session_name, dataset_name,
|
|
|
39
40
|
|
|
40
41
|
spike_times = {}
|
|
41
42
|
spike_amplitudes = {}
|
|
43
|
+
spike_positions = {}
|
|
42
44
|
for r in unit_rows:
|
|
43
45
|
arr = np.asarray(r['spike_times'], dtype=np.float64)
|
|
44
46
|
if interp is not None:
|
|
@@ -49,6 +51,11 @@ def _fetch_unit_data(_schema, subject_name, session_name, dataset_name,
|
|
|
49
51
|
if r['spike_amplitudes'] is not None:
|
|
50
52
|
spike_amplitudes[int(r['unit_id'])] = np.asarray(
|
|
51
53
|
r['spike_amplitudes'], dtype=np.float64)
|
|
54
|
+
if r['spike_positions'] is not None:
|
|
55
|
+
pos = np.asarray(r['spike_positions'], dtype=np.float64)
|
|
56
|
+
# column 1 is depth along the probe (µm)
|
|
57
|
+
spike_positions[int(r['unit_id'])] = (
|
|
58
|
+
pos[:, 1] if pos.ndim == 2 and pos.shape[1] > 1 else pos.ravel())
|
|
52
59
|
|
|
53
60
|
# waveforms
|
|
54
61
|
wf_rows = (_schema.SpikeSorting.Waveforms & key & unit_keys).fetch(
|
|
@@ -65,7 +72,7 @@ def _fetch_unit_data(_schema, subject_name, session_name, dataset_name,
|
|
|
65
72
|
(_schema.SpikeSorting & key).fetch1('sorting_channel_coords'),
|
|
66
73
|
dtype=np.float64)
|
|
67
74
|
|
|
68
|
-
return spike_times, spike_amplitudes, waveforms, chmap
|
|
75
|
+
return spike_times, spike_amplitudes, spike_positions, waveforms, chmap
|
|
69
76
|
|
|
70
77
|
|
|
71
78
|
def _xcorr(x, y, maxlag):
|
|
@@ -206,8 +213,11 @@ def _unit_panel(schema, plot_df, sel_key, unit_id_col, amp_col,
|
|
|
206
213
|
_tdata = _tdata[_tdata['_pass'] == 'pass']
|
|
207
214
|
_sort_cols = (['shank'] if 'shank' in _tdata.columns else []) + ['_depth']
|
|
208
215
|
_tdata = (_tdata.sort_values(_sort_cols)
|
|
209
|
-
.drop(columns=['
|
|
216
|
+
.drop(columns=['_depth'])
|
|
210
217
|
.reset_index(drop=True))
|
|
218
|
+
# positional mask of rows that fail the SUA criteria (greyed out below)
|
|
219
|
+
_fail_mask = (_tdata['_pass'] == 'fail').values
|
|
220
|
+
_tdata = _tdata.drop(columns=['_pass'])
|
|
211
221
|
|
|
212
222
|
# Cursor: index of the currently active unit in _tdata (for n/b navigation)
|
|
213
223
|
_cur_key = (f'srt_tbl_cursor_{sel_key["subject_name"]}_{sel_key["session_name"]}_'
|
|
@@ -260,6 +270,8 @@ def _unit_panel(schema, plot_df, sel_key, unit_id_col, amp_col,
|
|
|
260
270
|
if unit_id_col and unit_id_col in row.index:
|
|
261
271
|
if int(row[unit_id_col]) in _selected_ids:
|
|
262
272
|
return ['background-color: #f5a623; color: #000000'] * len(row)
|
|
273
|
+
if row.name < len(_fail_mask) and _fail_mask[row.name]:
|
|
274
|
+
return ['background-color: #f0f0f0; color: #999999'] * len(row)
|
|
263
275
|
return [''] * len(row)
|
|
264
276
|
|
|
265
277
|
_tbl_key = (f'srt_unit_table_{sel_key["subject_name"]}_{sel_key["session_name"]}_'
|
|
@@ -428,6 +440,7 @@ def _unit_panel(schema, plot_df, sel_key, unit_id_col, amp_col,
|
|
|
428
440
|
# all_spike_times / all_spike_amps keyed by (prb_str, unit_id_int)
|
|
429
441
|
all_spike_times = {}
|
|
430
442
|
all_spike_amps = {}
|
|
443
|
+
all_spike_pos = {}
|
|
431
444
|
all_waveforms = [] # each dict has unit_id + probe keys
|
|
432
445
|
chmap_by_probe = {}
|
|
433
446
|
|
|
@@ -437,7 +450,7 @@ def _unit_panel(schema, plot_df, sel_key, unit_id_col, amp_col,
|
|
|
437
450
|
except ValueError:
|
|
438
451
|
probe_num_val = prb
|
|
439
452
|
try:
|
|
440
|
-
sp_map, amp_map, wf_data, chmap = _fetch_unit_data(
|
|
453
|
+
sp_map, amp_map, pos_map, wf_data, chmap = _fetch_unit_data(
|
|
441
454
|
schema,
|
|
442
455
|
sel_key['subject_name'], sel_key['session_name'],
|
|
443
456
|
sel_key['dataset_name'], sel_key['parameter_set_num'],
|
|
@@ -449,6 +462,8 @@ def _unit_panel(schema, plot_df, sel_key, unit_id_col, amp_col,
|
|
|
449
462
|
all_spike_times[(prb, uid)] = v
|
|
450
463
|
for uid, v in amp_map.items():
|
|
451
464
|
all_spike_amps[(prb, uid)] = v
|
|
465
|
+
for uid, v in pos_map.items():
|
|
466
|
+
all_spike_pos[(prb, uid)] = v
|
|
452
467
|
for w in wf_data:
|
|
453
468
|
all_waveforms.append(dict(w, probe=prb))
|
|
454
469
|
chmap_by_probe[prb] = chmap
|
|
@@ -481,6 +496,71 @@ def _unit_panel(schema, plot_df, sel_key, unit_id_col, amp_col,
|
|
|
481
496
|
value=0.1, step=0.01, format='%.3f',
|
|
482
497
|
key='srt_wf_scale')
|
|
483
498
|
|
|
499
|
+
# toggle + spike-count selector for the amplitude/depth over-time plots
|
|
500
|
+
dc1, dc2 = st.columns([1, 2], vertical_alignment='center')
|
|
501
|
+
show_drift = dc1.toggle(
|
|
502
|
+
'Spikes over time', key='srt_show_drift',
|
|
503
|
+
help='Plot spike amplitude and depth over time for the selected units.')
|
|
504
|
+
drift_n = dc2.number_input(
|
|
505
|
+
'Number of spikes to plot', min_value=100, max_value=200000,
|
|
506
|
+
value=5000, step=100, key='srt_drift_nspikes',
|
|
507
|
+
help='A random subset of this many spikes per unit is plotted.')
|
|
508
|
+
|
|
509
|
+
if show_drift and ordered_units:
|
|
510
|
+
_drift_rows = []
|
|
511
|
+
_rng = np.random.default_rng(0)
|
|
512
|
+
for prb, uid in ordered_units:
|
|
513
|
+
t = all_spike_times.get((prb, uid))
|
|
514
|
+
if t is None or not len(t):
|
|
515
|
+
continue
|
|
516
|
+
n = len(t)
|
|
517
|
+
idx = (np.sort(_rng.choice(n, int(drift_n), replace=False))
|
|
518
|
+
if n > drift_n else np.arange(n))
|
|
519
|
+
a = all_spike_amps.get((prb, uid))
|
|
520
|
+
p = all_spike_pos.get((prb, uid))
|
|
521
|
+
_drift_rows.append(pd.DataFrame({
|
|
522
|
+
'time_s': np.asarray(t)[idx],
|
|
523
|
+
'unit': _ulabel(prb, uid),
|
|
524
|
+
'amplitude': (np.asarray(a)[idx]
|
|
525
|
+
if a is not None and len(a) >= n
|
|
526
|
+
else np.full(len(idx), np.nan)),
|
|
527
|
+
'depth': (np.asarray(p)[idx]
|
|
528
|
+
if p is not None and len(p) >= n
|
|
529
|
+
else np.full(len(idx), np.nan)),
|
|
530
|
+
}))
|
|
531
|
+
if _drift_rows:
|
|
532
|
+
drift_df = pd.concat(_drift_rows, ignore_index=True)
|
|
533
|
+
alt.data_transformers.disable_max_rows()
|
|
534
|
+
_t_dom = [float(drift_df['time_s'].min()), float(drift_df['time_s'].max())]
|
|
535
|
+
_drift_base = alt.Chart(drift_df).mark_circle(size=10, opacity=0.4).encode(
|
|
536
|
+
x=alt.X('time_s:Q', title='Time (s)', axis=_tax,
|
|
537
|
+
scale=alt.Scale(domain=_t_dom)),
|
|
538
|
+
color=alt.Color('unit:N', scale=unit_color_scale, title='Unit'),
|
|
539
|
+
tooltip=['unit:N',
|
|
540
|
+
alt.Tooltip('time_s:Q', format='.1f', title='Time (s)'),
|
|
541
|
+
alt.Tooltip('amplitude:Q', format='.0f', title='Amplitude'),
|
|
542
|
+
alt.Tooltip('depth:Q', format='.0f', title='Depth (µm)')])
|
|
543
|
+
_dcol1, _dcol2 = st.columns(2)
|
|
544
|
+
if drift_df['amplitude'].notna().any():
|
|
545
|
+
_dcol1.altair_chart(
|
|
546
|
+
_drift_base.encode(
|
|
547
|
+
y=alt.Y('amplitude:Q', title='Amplitude', axis=_tax))
|
|
548
|
+
.add_params(alt.selection_interval(bind='scales',
|
|
549
|
+
name='drift_amp_zoom'))
|
|
550
|
+
.properties(height=280, title='Amplitude over time'),
|
|
551
|
+
width='stretch')
|
|
552
|
+
if drift_df['depth'].notna().any():
|
|
553
|
+
_d_dom = [float(drift_df['depth'].min()) - 50,
|
|
554
|
+
float(drift_df['depth'].max()) + 50]
|
|
555
|
+
_dcol2.altair_chart(
|
|
556
|
+
_drift_base.encode(
|
|
557
|
+
y=alt.Y('depth:Q', title='Depth (µm)', axis=_tax,
|
|
558
|
+
scale=alt.Scale(domain=_d_dom)))
|
|
559
|
+
.add_params(alt.selection_interval(bind='scales',
|
|
560
|
+
name='drift_depth_zoom'))
|
|
561
|
+
.properties(height=280, title='Depth over time'),
|
|
562
|
+
width='stretch')
|
|
563
|
+
|
|
484
564
|
col_left, col_right = st.columns([3, 2])
|
|
485
565
|
|
|
486
566
|
with col_left:
|
|
@@ -815,23 +895,39 @@ def sorting_tab(schema=None):
|
|
|
815
895
|
default_param = _get_default_param(schema)
|
|
816
896
|
default_params = [default_param] if default_param and default_param in param_options else []
|
|
817
897
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
898
|
+
# Global parameter-set and subject selection (apply to every tab). Apply any
|
|
899
|
+
# pending change requested by a chart click before the widgets are instantiated.
|
|
900
|
+
if '_srt_pending_param' in st.session_state:
|
|
901
|
+
st.session_state['srt_param_sel'] = st.session_state.pop('_srt_pending_param')
|
|
902
|
+
if '_srt_pending_subjects' in st.session_state:
|
|
903
|
+
st.session_state['srt_subjects'] = st.session_state.pop('_srt_pending_subjects')
|
|
904
|
+
|
|
905
|
+
_all_sorted = _get_all_sorting_sessions(schema)
|
|
906
|
+
subject_options = (sorted(_all_sorted['subject_name'].unique().tolist())
|
|
907
|
+
if not _all_sorted.empty else [])
|
|
908
|
+
|
|
909
|
+
_gp, _gs = st.columns(2)
|
|
910
|
+
with _gp:
|
|
911
|
+
st.session_state.setdefault('srt_param_sel', default_params)
|
|
912
|
+
sel_params = st.multiselect(
|
|
823
913
|
'Parameter sets',
|
|
824
914
|
options=list(param_options.keys()),
|
|
825
915
|
format_func=lambda k: param_options[k],
|
|
826
|
-
|
|
827
|
-
key='srt_param_sel_sessions',
|
|
916
|
+
key='srt_param_sel',
|
|
828
917
|
)
|
|
829
|
-
|
|
918
|
+
with _gs:
|
|
919
|
+
selected_subjects = st.multiselect(
|
|
920
|
+
'Subjects', options=subject_options, key='srt_subjects')
|
|
921
|
+
selected_params = sel_params if sel_params else list(param_options.keys())
|
|
922
|
+
|
|
923
|
+
tab_sessions, tab_explorer, tab_units = st.tabs(['Sessions', 'Unit explorer', 'Unit counts'])
|
|
830
924
|
|
|
925
|
+
# Tab 1: Sessions
|
|
926
|
+
with tab_sessions:
|
|
831
927
|
# histogram: session counts per subject
|
|
832
928
|
counts_df = _get_session_counts(schema)
|
|
833
929
|
if not counts_df.empty:
|
|
834
|
-
counts_filt = counts_df[counts_df['parameter_set_num'].isin(
|
|
930
|
+
counts_filt = counts_df[counts_df['parameter_set_num'].isin(selected_params)]
|
|
835
931
|
if not counts_filt.empty:
|
|
836
932
|
param_sets_h = sorted(counts_filt['parameter_set_num'].unique().tolist())
|
|
837
933
|
bar_color_scale = alt.Scale(
|
|
@@ -856,17 +952,22 @@ def sorting_tab(schema=None):
|
|
|
856
952
|
bar_pts = (bar_event.selection or {}).get('bar_sel', [])
|
|
857
953
|
if bar_pts and bar_pts[0].get('subject_name'):
|
|
858
954
|
bar_pt = bar_pts[0]
|
|
859
|
-
bar_subject = bar_pt.get('subject_name')
|
|
860
955
|
bar_param = str(bar_pt.get('parameter_set_num', ''))
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
956
|
+
# act only once per distinct selection, otherwise the sticky
|
|
957
|
+
# chart selection keeps overriding manual param/subject changes
|
|
958
|
+
_sig = ('bar', bar_pt.get('subject_name'), bar_param)
|
|
959
|
+
if st.session_state.get('_srt_last_click') != _sig:
|
|
960
|
+
st.session_state['_srt_last_click'] = _sig
|
|
961
|
+
st.session_state['_srt_pending_subjects'] = [bar_pt.get('subject_name')]
|
|
962
|
+
if bar_param in param_options:
|
|
963
|
+
st.session_state['_srt_pending_param'] = [bar_param]
|
|
964
|
+
st.rerun()
|
|
864
965
|
|
|
865
966
|
# all-sessions scatter
|
|
866
967
|
all_sessions_df = _get_all_sorting_sessions(schema)
|
|
867
968
|
if not all_sessions_df.empty:
|
|
868
969
|
scatter_df = all_sessions_df[
|
|
869
|
-
all_sessions_df['parameter_set_num'].isin(
|
|
970
|
+
all_sessions_df['parameter_set_num'].isin(selected_params)
|
|
870
971
|
].reset_index(drop=True)
|
|
871
972
|
if scatter_df.empty:
|
|
872
973
|
scatter_df = all_sessions_df.copy()
|
|
@@ -902,16 +1003,23 @@ def sorting_tab(schema=None):
|
|
|
902
1003
|
if scat_pts and scat_pts[0].get('subject_name'):
|
|
903
1004
|
pt = scat_pts[0]
|
|
904
1005
|
param_str = str(pt.get('parameter_set_num', ''))
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1006
|
+
# act only once per distinct selection, otherwise the sticky
|
|
1007
|
+
# chart selection keeps overriding manual param/subject changes
|
|
1008
|
+
_sig = ('scat', pt.get('subject_name'), pt.get('session_name'),
|
|
1009
|
+
pt.get('dataset_name'), param_str)
|
|
1010
|
+
if st.session_state.get('_srt_last_click') != _sig:
|
|
1011
|
+
st.session_state['_srt_last_click'] = _sig
|
|
1012
|
+
st.session_state['srt_selected_key'] = {
|
|
1013
|
+
'subject_name': pt.get('subject_name'),
|
|
1014
|
+
'session_name': pt.get('session_name'),
|
|
1015
|
+
'dataset_name': pt.get('dataset_name'),
|
|
1016
|
+
'parameter_set_num': param_str,
|
|
1017
|
+
}
|
|
1018
|
+
# sync global subject + param set
|
|
1019
|
+
st.session_state['_srt_pending_subjects'] = [pt.get('subject_name')]
|
|
1020
|
+
if param_str in param_options:
|
|
1021
|
+
st.session_state['_srt_pending_param'] = [param_str]
|
|
1022
|
+
st.rerun()
|
|
915
1023
|
|
|
916
1024
|
# Tab 2: Unit explorer
|
|
917
1025
|
with tab_explorer:
|
|
@@ -935,12 +1043,19 @@ def sorting_tab(schema=None):
|
|
|
935
1043
|
|
|
936
1044
|
if _exp_unit_criteria_id is not None:
|
|
937
1045
|
sua_df = _get_all_sua_counts(schema, _exp_unit_criteria_id)
|
|
1046
|
+
# restrict to the globally selected parameter set(s)
|
|
1047
|
+
sua_df = sua_df[sua_df['parameter_set_num'].isin(selected_params)]
|
|
938
1048
|
if not sua_df.empty:
|
|
939
1049
|
sel_key_now = st.session_state.get('srt_selected_key', {})
|
|
940
1050
|
_sel_subj = sel_key_now.get('subject_name')
|
|
941
|
-
|
|
1051
|
+
# follow the global Subjects selector: show every selected subject
|
|
1052
|
+
# (each gets its own colour); fall back to the clicked session's
|
|
1053
|
+
# subject when nothing is selected.
|
|
1054
|
+
if selected_subjects:
|
|
1055
|
+
sua_df = sua_df[sua_df['subject_name'].isin(selected_subjects)]
|
|
1056
|
+
elif _sel_subj and _sel_subj in sua_df['subject_name'].values:
|
|
942
1057
|
sua_df = sua_df[sua_df['subject_name'] == _sel_subj]
|
|
943
|
-
# One row per session: sum SUA across
|
|
1058
|
+
# One row per session: sum SUA across the selected parameter sets and probes.
|
|
944
1059
|
# Retain the parameter_set_num with the highest SUA for click-to-load.
|
|
945
1060
|
_best_param = (sua_df.sort_values('sua', ascending=False)
|
|
946
1061
|
.drop_duplicates(['subject_name', 'session_name', 'dataset_name'])
|
|
@@ -1071,24 +1186,6 @@ def sorting_tab(schema=None):
|
|
|
1071
1186
|
|
|
1072
1187
|
# Tab 3: Unit counts
|
|
1073
1188
|
with tab_units:
|
|
1074
|
-
sessions_all = schema.SpikeSorting().fetch(format='frame').reset_index()
|
|
1075
|
-
|
|
1076
|
-
st.session_state.setdefault('srt_param_sel_units', default_params)
|
|
1077
|
-
sel_params_2 = st.multiselect(
|
|
1078
|
-
'Parameter sets',
|
|
1079
|
-
options=list(param_options.keys()),
|
|
1080
|
-
format_func=lambda k: param_options[k],
|
|
1081
|
-
key='srt_param_sel_units',
|
|
1082
|
-
)
|
|
1083
|
-
selected_params_2 = sel_params_2 if sel_params_2 else list(param_options.keys())
|
|
1084
|
-
|
|
1085
|
-
sess2 = sessions_all[sessions_all['parameter_set_num'].isin(
|
|
1086
|
-
[int(p) if p.isdigit() else p for p in selected_params_2]
|
|
1087
|
-
)]
|
|
1088
|
-
subjects2 = np.unique(sess2['subject_name'].values) if len(sess2) else []
|
|
1089
|
-
|
|
1090
|
-
selected_subjects2 = st.multiselect('Subject:', subjects2, key='srt_subjects')
|
|
1091
|
-
|
|
1092
1189
|
criteria_ids2 = schema.UnitCountCriteria().fetch(as_dict=True)
|
|
1093
1190
|
if not criteria_ids2:
|
|
1094
1191
|
st.write('Add a UnitCountCriteria to the table: example '
|
|
@@ -1106,10 +1203,10 @@ def sorting_tab(schema=None):
|
|
|
1106
1203
|
unit_criteria2 = (int(selection2.split('->')[0].strip(' []'))
|
|
1107
1204
|
if '->' in selection2 else None)
|
|
1108
1205
|
|
|
1109
|
-
if
|
|
1206
|
+
if selected_subjects and unit_criteria2 is not None:
|
|
1110
1207
|
units2 = []
|
|
1111
|
-
for p in
|
|
1112
|
-
for s in
|
|
1208
|
+
for p in selected_params:
|
|
1209
|
+
for s in selected_subjects:
|
|
1113
1210
|
units2.append(_get_unit_counts(schema, s, p, unit_criteria2))
|
|
1114
1211
|
units2 = [df for df in units2 if not df.empty]
|
|
1115
1212
|
if units2:
|
|
@@ -697,6 +697,30 @@ class UnitCount(dj.Computed):
|
|
|
697
697
|
keys = [dict(a,unit_criteria_id = key['unit_criteria_id']) for i,a in allu.iterrows()]
|
|
698
698
|
self.Unit.insert(keys, ignore_extra_fields = True) # add to the Unit part table
|
|
699
699
|
|
|
700
|
+
|
|
701
|
+
def _apply_unit_criteria(unitmetrics,criteria):
|
|
702
|
+
parameters = [p.strip(' ') for p in criteria.split('&')]
|
|
703
|
+
operators = {'<': np.less, '>': np.greater, '<=':np.less_equal, '>=':np.greater_equal, '==': np.equal, '!=':np.not_equal}
|
|
704
|
+
idx = []
|
|
705
|
+
unitmetrics = unitmetrics.replace({None: np.nan})
|
|
706
|
+
for p in parameters:
|
|
707
|
+
for o in operators.keys():
|
|
708
|
+
if o in p:
|
|
709
|
+
n = [t.strip(' ') for t in p.split(o)]
|
|
710
|
+
try:
|
|
711
|
+
n[1] = float(n[1])
|
|
712
|
+
except:
|
|
713
|
+
print(f'Could not convert {n[1]} to float.')
|
|
714
|
+
pass
|
|
715
|
+
if '|' in n[0]: # then take the absolute value
|
|
716
|
+
n[0] = n[0].strip('|')
|
|
717
|
+
idx.append(operators[o](np.abs(unitmetrics[n[0]].values),n[1]))
|
|
718
|
+
else:
|
|
719
|
+
idx.append(operators[o](unitmetrics[n[0]].values,n[1]))
|
|
720
|
+
for i in idx:
|
|
721
|
+
idx[0] = np.logical_and(idx[0],i)
|
|
722
|
+
return idx[0]
|
|
723
|
+
|
|
700
724
|
from .histology import Atlas
|
|
701
725
|
|
|
702
726
|
@analysisschema
|
|
@@ -24,7 +24,7 @@ class Widefield(dj.Imported):
|
|
|
24
24
|
width : int # width of each frame
|
|
25
25
|
height : int # height of each frame
|
|
26
26
|
frame_rate : double # frame rate
|
|
27
|
-
[nullable]
|
|
27
|
+
-> [nullable] File # path to the stack
|
|
28
28
|
magnification = NULL : double # magnification
|
|
29
29
|
objective_angle = NULL : double # angle
|
|
30
30
|
objective = NULL : varchar(32) # objective
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|