labdata 0.0.25__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 (56) hide show
  1. {labdata-0.0.25 → labdata-0.1.0}/PKG-INFO +1 -1
  2. labdata-0.1.0/labdata/__init__.py +11 -0
  3. {labdata-0.0.25 → labdata-0.1.0}/labdata/cli.py +4 -0
  4. labdata-0.1.0/labdata/dashboard/console.py +166 -0
  5. {labdata-0.0.25 → labdata-0.1.0}/labdata/dashboard/decisiontask.py +11 -12
  6. {labdata-0.0.25 → labdata-0.1.0}/labdata/dashboard/index.py +8 -0
  7. {labdata-0.0.25 → labdata-0.1.0}/labdata/dashboard/segmentation.py +159 -9
  8. {labdata-0.0.25 → labdata-0.1.0}/labdata/utils.py +42 -3
  9. {labdata-0.0.25 → labdata-0.1.0}/labdata.egg-info/PKG-INFO +1 -1
  10. {labdata-0.0.25 → labdata-0.1.0}/labdata.egg-info/SOURCES.txt +1 -0
  11. labdata-0.0.25/labdata/__init__.py +0 -16
  12. {labdata-0.0.25 → labdata-0.1.0}/LICENSE +0 -0
  13. {labdata-0.0.25 → labdata-0.1.0}/README.md +0 -0
  14. {labdata-0.0.25 → labdata-0.1.0}/labdata/compute/__init__.py +0 -0
  15. {labdata-0.0.25 → labdata-0.1.0}/labdata/compute/caiman.py +0 -0
  16. {labdata-0.0.25 → labdata-0.1.0}/labdata/compute/ec2.py +0 -0
  17. {labdata-0.0.25 → labdata-0.1.0}/labdata/compute/ephys.py +0 -0
  18. {labdata-0.0.25 → labdata-0.1.0}/labdata/compute/pose.py +0 -0
  19. {labdata-0.0.25 → labdata-0.1.0}/labdata/compute/schedulers.py +0 -0
  20. {labdata-0.0.25 → labdata-0.1.0}/labdata/compute/singularity.py +0 -0
  21. {labdata-0.0.25 → labdata-0.1.0}/labdata/compute/suite2p.py +0 -0
  22. {labdata-0.0.25 → labdata-0.1.0}/labdata/compute/utils.py +0 -0
  23. {labdata-0.0.25 → labdata-0.1.0}/labdata/copy.py +0 -0
  24. {labdata-0.0.25 → labdata-0.1.0}/labdata/dashboard/__init__.py +0 -0
  25. {labdata-0.0.25 → labdata-0.1.0}/labdata/dashboard/compute.py +0 -0
  26. {labdata-0.0.25 → labdata-0.1.0}/labdata/dashboard/explorer/__init__.py +0 -0
  27. {labdata-0.0.25 → labdata-0.1.0}/labdata/dashboard/explorer/cellsegmentation.py +0 -0
  28. {labdata-0.0.25 → labdata-0.1.0}/labdata/dashboard/explorer/spikesorting.py +0 -0
  29. {labdata-0.0.25 → labdata-0.1.0}/labdata/dashboard/notes.py +0 -0
  30. {labdata-0.0.25 → labdata-0.1.0}/labdata/dashboard/procedures.py +0 -0
  31. {labdata-0.0.25 → labdata-0.1.0}/labdata/dashboard/sorting.py +0 -0
  32. {labdata-0.0.25 → labdata-0.1.0}/labdata/dashboard/video.py +0 -0
  33. {labdata-0.0.25 → labdata-0.1.0}/labdata/export.py +0 -0
  34. {labdata-0.0.25 → labdata-0.1.0}/labdata/rules/__init__.py +0 -0
  35. {labdata-0.0.25 → labdata-0.1.0}/labdata/rules/ephys.py +0 -0
  36. {labdata-0.0.25 → labdata-0.1.0}/labdata/rules/imaging.py +0 -0
  37. {labdata-0.0.25 → labdata-0.1.0}/labdata/rules/utils.py +0 -0
  38. {labdata-0.0.25 → labdata-0.1.0}/labdata/s3.py +0 -0
  39. {labdata-0.0.25 → labdata-0.1.0}/labdata/schema/__init__.py +0 -0
  40. {labdata-0.0.25 → labdata-0.1.0}/labdata/schema/ephys.py +0 -0
  41. {labdata-0.0.25 → labdata-0.1.0}/labdata/schema/general.py +0 -0
  42. {labdata-0.0.25 → labdata-0.1.0}/labdata/schema/histology.py +0 -0
  43. {labdata-0.0.25 → labdata-0.1.0}/labdata/schema/onephoton.py +0 -0
  44. {labdata-0.0.25 → labdata-0.1.0}/labdata/schema/procedures.py +0 -0
  45. {labdata-0.0.25 → labdata-0.1.0}/labdata/schema/tasks.py +0 -0
  46. {labdata-0.0.25 → labdata-0.1.0}/labdata/schema/twophoton.py +0 -0
  47. {labdata-0.0.25 → labdata-0.1.0}/labdata/schema/utils.py +0 -0
  48. {labdata-0.0.25 → labdata-0.1.0}/labdata/schema/video.py +0 -0
  49. {labdata-0.0.25 → labdata-0.1.0}/labdata/stacks.py +0 -0
  50. {labdata-0.0.25 → labdata-0.1.0}/labdata/widgets.py +0 -0
  51. {labdata-0.0.25 → labdata-0.1.0}/labdata.egg-info/dependency_links.txt +0 -0
  52. {labdata-0.0.25 → labdata-0.1.0}/labdata.egg-info/entry_points.txt +0 -0
  53. {labdata-0.0.25 → labdata-0.1.0}/labdata.egg-info/requires.txt +0 -0
  54. {labdata-0.0.25 → labdata-0.1.0}/labdata.egg-info/top_level.txt +0 -0
  55. {labdata-0.0.25 → labdata-0.1.0}/pyproject.toml +0 -0
  56. {labdata-0.0.25 → 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.25
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
@@ -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):
@@ -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
+
@@ -125,11 +125,11 @@ def decisionmaking_tab(schema = None):
125
125
  point_selector = alt.selection_point(on="click")
126
126
  chart = alt.Chart(summarysource).mark_rect().encode(
127
127
  x=alt.X('session_datetime:T',timeUnit = 'yearmonthdate',
128
- 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'),
129
129
  y=alt.Y('subject_name:O').title('Subject name'),
130
130
  color=alt.Color('performance_easy:Q',
131
131
  scale=alt.Scale(scheme='spectral', reverse=True, domain=[0.5,1], clamp=True)).title('Perf easy'),
132
- tooltip = ['subject_name','session_name','session_datetime','performance_easy','performance','n_trials']
132
+ tooltip = ['subject_name:N','session_name:N','session_datetime:T','performance_easy:Q','performance:Q','n_trials:Q']
133
133
 
134
134
  ).properties(width=1000, title ='Behavioral performance').add_params(point_selector)
135
135
  event_data = st.altair_chart(chart.interactive(), on_select='rerun')
@@ -204,11 +204,11 @@ def decisionmaking_tab(schema = None):
204
204
  x_enc = alt.X('session_datetime:T', axis=axis).title('Session date')
205
205
  perf_chart = (
206
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) +
207
- 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']) +
208
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')) +
209
209
  alt.Chart(trialset).mark_line(color=orange).encode(x=x_enc, y=alt.Y('performance:Q', axis=axis)) +
210
210
  alt.Chart(trialset).mark_point(color=orange).encode(x=x_enc, y=alt.Y('performance:Q'),
211
- tooltip=['session_name','session_datetime','performance_easy','performance']) +
211
+ tooltip=['session_name:N','session_datetime:T','performance_easy:Q','performance:Q']) +
212
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'))
213
213
  )
214
214
  ruler_df = pd.DataFrame({'t': [selected_dt]})
@@ -320,12 +320,11 @@ def decisionmaking_tab(schema = None):
320
320
  st.caption(f"*{n['notetaker']} — {pd.Timestamp(n['note_datetime']).strftime('%Y-%m-%d %H:%M')}*")
321
321
  st.write(n['notes'])
322
322
 
323
- def line_point_plot(data,x,y,tooltip,color):
324
- scatter = alt.Chart(data).mark_point(color = color).encode(
325
- x=x,
326
- y=y,
327
- 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)
328
328
  line = alt.Chart(data).mark_line(color=color).encode(
329
- x=x,
330
- y=y,).properties(width=1000)
331
- return scatter+line
329
+ x=x, y=y).properties(width=1000)
330
+ return scatter + line
@@ -196,9 +196,13 @@ def load_schema(project):
196
196
  return load_project_schema(project)
197
197
 
198
198
  project = None
199
+ show_console = False
199
200
  for k in sys.argv:
200
201
  if 'project=' in k:
201
202
  project = k.replace('project=','')
203
+ if k == 'console=1':
204
+ from console import console_tab
205
+ show_console = True
202
206
  with st.spinner(f'Connecting to {"project " + project if project else "database"}…'):
203
207
  schema = load_schema(project)
204
208
 
@@ -213,11 +217,15 @@ page_names_to_funcs = {
213
217
  "**Compute/Upload**": partial(compute_tab, schema = schema),
214
218
  }
215
219
 
220
+ if show_console:
221
+ page_names_to_funcs["**Console**"] = partial(console_tab, schema=schema)
222
+
216
223
  from labdata import plugins
217
224
  for p in plugins.keys():
218
225
  if hasattr(plugins[p],'dashboard_function'):
219
226
  page_names_to_funcs[plugins[p].dashboard_name] = partial(plugins[p].dashboard_function, schema = schema)
220
227
 
228
+
221
229
  tab_name = st.sidebar.radio(
222
230
  "``labdata dashboard``",
223
231
  page_names_to_funcs.keys())
@@ -29,18 +29,31 @@ def segmentation_tab(schema=None):
29
29
  def get_subjects():
30
30
  return list((schema.Subject & schema.CellSegmentation).fetch('subject_name'))
31
31
 
32
+ @st.cache_data
33
+ def get_session_counts():
34
+ rows = (schema.Subject * schema.CellSegmentationParams).aggr(
35
+ schema.CellSegmentation,
36
+ n_sessions='count(session_name)'
37
+ ).fetch(as_dict=True)
38
+ if not rows:
39
+ return pd.DataFrame()
40
+ df = pd.DataFrame(rows)
41
+ df = df[df['n_sessions'] > 0].reset_index(drop=True)
42
+ df['parameter_set_num'] = df['parameter_set_num'].astype(str)
43
+ return df
44
+
32
45
  @st.cache_data
33
46
  def get_sessions(subject_name):
34
47
  rows = (schema.CellSegmentation * schema.Session * schema.CellSegmentationParams
35
48
  & dict(subject_name=subject_name)).fetch(
36
49
  'session_name', 'dataset_name', 'parameter_set_num',
37
- 'algorithm_name', 'n_rois', 'segmentation_datetime',
50
+ 'algorithm_name', 'algorithm_version', 'n_rois', 'segmentation_datetime',
38
51
  'session_datetime', as_dict=True)
39
52
  if not rows:
40
53
  return pd.DataFrame()
41
54
  df = pd.DataFrame(rows)
42
55
  cols = ['session_name', 'dataset_name', 'parameter_set_num', 'algorithm_name',
43
- 'n_rois', 'session_datetime', 'segmentation_datetime']
56
+ 'algorithm_version', 'n_rois', 'session_datetime', 'segmentation_datetime']
44
57
  return df[[c for c in cols if c in df.columns]].sort_values(
45
58
  'session_datetime', ascending=False).reset_index(drop=True)
46
59
 
@@ -121,7 +134,7 @@ def segmentation_tab(schema=None):
121
134
  pass
122
135
  return pd.DataFrame(traces)
123
136
 
124
- # get the seg_picked_rois from session state
137
+ # get the seg_picked_rois from session state
125
138
  if 'seg_picked_rois' not in st.session_state:
126
139
  st.session_state['seg_picked_rois'] = []
127
140
 
@@ -131,6 +144,53 @@ def segmentation_tab(schema=None):
131
144
  st.write('No cell segmentation data in the database.')
132
145
  return
133
146
 
147
+ # sessions per subject for each parameter_set_num
148
+ counts_df = get_session_counts()
149
+ bar_param_filter = st.session_state.get('seg_bar_param_filter')
150
+
151
+ if not counts_df.empty:
152
+ if st.session_state.pop('seg_bar_clear_pending', False):
153
+ st.session_state.pop('seg_bar_param_filter', None)
154
+ bar_param_filter = None
155
+
156
+ _colors = ['#000000', '#d62728', '#1f77b4', '#ff7f0e', '#2ca02c',
157
+ '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22']
158
+ param_sets = sorted(counts_df['parameter_set_num'].unique().tolist())
159
+ bar_color_scale = alt.Scale(
160
+ domain=param_sets,
161
+ range=[_colors[i % len(_colors)] for i in range(len(param_sets))])
162
+
163
+ bar_sel = alt.selection_point(
164
+ fields=['subject_name', 'parameter_set_num'], name='bar_sel')
165
+ chart = (
166
+ alt.Chart(counts_df).mark_bar().encode(
167
+ x=alt.X('subject_name:N', sort='ascending', title='Subject'),
168
+ xOffset=alt.XOffset('parameter_set_num:N', title='Param set'),
169
+ y=alt.Y('n_sessions:Q', title='# sessions'),
170
+ color=alt.Color('parameter_set_num:N', title='Param set',
171
+ scale=bar_color_scale),
172
+ opacity=alt.condition(bar_sel, alt.value(1.0), alt.value(0.4)),
173
+ tooltip=['subject_name:N', 'parameter_set_num:N', 'n_sessions:Q'],
174
+ ).add_params(bar_sel).properties(height=200)
175
+ )
176
+ bar_event = st.altair_chart(chart, width='stretch', on_select='rerun',
177
+ key='seg_sessions_bar')
178
+
179
+ pts = (bar_event.selection or {}).get('bar_sel', [])
180
+ if pts and pts[0].get('subject_name'):
181
+ clicked_subject = pts[0]['subject_name']
182
+ clicked_param = str(pts[0].get('parameter_set_num', ''))
183
+ if clicked_subject in subjects:
184
+ st.session_state['seg_subject'] = clicked_subject
185
+ st.session_state['seg_bar_param_filter'] = clicked_param
186
+ bar_param_filter = clicked_param
187
+
188
+ if bar_param_filter:
189
+ st.write(f'**Filtered by param set:** {bar_param_filter}')
190
+ if st.button('Clear filter', key='seg_bar_clear'):
191
+ st.session_state['seg_bar_clear_pending'] = True
192
+ st.rerun()
193
+
134
194
  subject = st.selectbox('Subject', subjects, index=None, key='seg_subject')
135
195
  if not subject:
136
196
  return
@@ -140,15 +200,79 @@ def segmentation_tab(schema=None):
140
200
  st.write('No segmentation sessions found for this subject.')
141
201
  return
142
202
 
203
+ # Apply bar param filter when active
204
+ if bar_param_filter is not None:
205
+ filtered = sessions[
206
+ sessions['parameter_set_num'].astype(str) == str(bar_param_filter)]
207
+ if not filtered.empty:
208
+ sessions = filtered.reset_index(drop=True)
209
+
210
+ # Clear scatter sticky selection when subject changes
211
+ if st.session_state.get('seg_scatter_subject') != subject:
212
+ st.session_state['seg_scatter_subject'] = subject
213
+ st.session_state.pop('seg_scatter_sel_key', None)
214
+
215
+ _colors = ['#000000', '#d62728', '#1f77b4', '#ff7f0e', '#2ca02c',
216
+ '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22']
217
+ scatter_df = sessions.copy()
218
+ scatter_df['parameter_set_num'] = scatter_df['parameter_set_num'].astype(str)
219
+ param_sets_s = sorted(scatter_df['parameter_set_num'].unique().tolist())
220
+ sess_color_scale = alt.Scale(
221
+ domain=param_sets_s,
222
+ range=[_colors[i % len(_colors)] for i in range(len(param_sets_s))])
223
+
224
+ scatter_click = alt.selection_point(
225
+ fields=['session_name', 'dataset_name'], name='sess_click')
226
+ scatter_chart = (
227
+ alt.Chart(scatter_df).mark_circle(size=120).encode(
228
+ x=alt.X('session_datetime:T', title='Date'),
229
+ y=alt.Y('parameter_set_num:N', title='Param set'),
230
+ color=alt.Color('parameter_set_num:N', scale=sess_color_scale,
231
+ title='Param set'),
232
+ opacity=alt.condition(scatter_click, alt.value(1.0), alt.value(0.4)),
233
+ tooltip=['session_name:N', 'dataset_name:N', 'parameter_set_num:N',
234
+ alt.Tooltip('session_datetime:T', format='%Y-%m-%d'),
235
+ 'n_rois:Q'],
236
+ ).add_params(scatter_click).properties(height=150)
237
+ )
238
+ scatter_event = st.altair_chart(scatter_chart, width='stretch', on_select='rerun',
239
+ key='seg_scatter_timeline')
240
+
241
+ scatter_pts = (scatter_event.selection or {}).get('sess_click', [])
242
+ scatter_sel_idx = None
243
+ if scatter_pts and scatter_pts[0].get('session_name'):
244
+ sname = scatter_pts[0]['session_name']
245
+ dname = scatter_pts[0].get('dataset_name', '')
246
+ matches = sessions[(sessions['session_name'] == sname) &
247
+ (sessions['dataset_name'] == dname)]
248
+ if not matches.empty:
249
+ scatter_sel_idx = int(matches.index[0])
250
+ st.session_state['seg_scatter_sel_key'] = f"{sname}|{dname}"
251
+
143
252
  event_sess = st.dataframe(
144
253
  sessions, hide_index=True, width='stretch',
145
254
  on_select='rerun', selection_mode='single-row', key='seg_sessions_table',
146
255
  )
147
- sel_idx = (event_sess.selection or {}).get('rows', [])
148
- if not sel_idx:
256
+ table_rows = (event_sess.selection or {}).get('rows', [])
257
+
258
+ if table_rows:
259
+ sel_idx_final = table_rows[0]
260
+ elif scatter_sel_idx is not None:
261
+ sel_idx_final = scatter_sel_idx
262
+ else:
263
+ stored = st.session_state.get('seg_scatter_sel_key', '')
264
+ if stored:
265
+ parts = stored.split('|', 1)
266
+ matches = sessions[(sessions['session_name'] == parts[0]) &
267
+ (sessions['dataset_name'] == parts[1])]
268
+ sel_idx_final = int(matches.index[0]) if not matches.empty else None
269
+ else:
270
+ sel_idx_final = None
271
+
272
+ if sel_idx_final is None:
149
273
  return
150
274
 
151
- row = sessions.iloc[sel_idx[0]]
275
+ row = sessions.iloc[sel_idx_final]
152
276
  param_set = int(row['parameter_set_num'])
153
277
  seg_key = dict(subject_name=subject, session_name=row['session_name'],
154
278
  dataset_name=row['dataset_name'], parameter_set_num=param_set)
@@ -175,6 +299,9 @@ def segmentation_tab(schema=None):
175
299
  methods = get_selection_methods(
176
300
  subject, row['session_name'], row['dataset_name'], param_set)
177
301
 
302
+ if 'seg_sel_method_pending' in st.session_state:
303
+ st.session_state['seg_sel_method'] = st.session_state.pop('seg_sel_method_pending')
304
+
178
305
  cA, cB, cC = st.columns([1, 1, 2])
179
306
  show_rois = cA.toggle('Show ROIs', value=True, key='seg_show_rois')
180
307
  show_mode = cB.radio('ROIs to show', ['All', 'Selected'], horizontal=True,
@@ -184,10 +311,10 @@ def segmentation_tab(schema=None):
184
311
  label_visibility='collapsed',
185
312
  disabled=not show_rois)
186
313
 
187
- # Duplicate selection to create custom labelings
314
+ # Duplicate / delete selection methods
188
315
  _, _, dC = st.columns([1, 1, 2])
189
316
  with dC:
190
- dname, dbtn = st.columns([3, 1])
317
+ dname, dbtn, del_btn = st.columns([3, 1, 1])
191
318
  new_name = dname.text_input('New method name', key='seg_new_method_name',
192
319
  placeholder=f'Duplicate "{sel_method}" as…',
193
320
  label_visibility='collapsed')
@@ -208,11 +335,34 @@ def segmentation_tab(schema=None):
208
335
  [{**r, 'selection_method': nm} for r in src_rows])
209
336
  get_selection_methods.clear()
210
337
  get_roi_data.clear()
211
- st.session_state['seg_sel_method'] = nm
338
+ st.session_state['seg_sel_method_pending'] = nm
212
339
  st.rerun()
213
340
  except Exception as exc:
214
341
  st.error(f'Insert failed: {exc}')
215
342
 
343
+ if del_btn.button('Delete', key='seg_del_btn', type='secondary'):
344
+ st.session_state['seg_del_confirm'] = sel_method
345
+
346
+ pending_del = st.session_state.get('seg_del_confirm')
347
+ if pending_del and pending_del == sel_method:
348
+ st.warning(f'Delete selection method **"{pending_del}"**? This cannot be undone.')
349
+ yes_col, no_col, _ = st.columns([1, 1, 6])
350
+ if yes_col.button('Yes, delete', key='seg_del_yes', type='primary'):
351
+ try:
352
+ schema.dj.config['safemode'] = False # disable temporarly
353
+ (schema.CellSegmentation.Selection
354
+ & dict(seg_key, selection_method=pending_del)).delete(force=True)
355
+ schema.dj.config['safemode'] = True # re-enable
356
+ get_selection_methods.clear()
357
+ get_roi_data.clear()
358
+ st.session_state.pop('seg_del_confirm', None)
359
+ st.rerun()
360
+ except Exception as exc:
361
+ st.error(f'Delete failed: {exc}')
362
+ if no_col.button('Cancel', key='seg_del_no'):
363
+ st.session_state.pop('seg_del_confirm', None)
364
+ st.rerun()
365
+
216
366
  with st.spinner('Loading ROI positions…'):
217
367
  centers = get_roi_data(
218
368
  subject, row['session_name'], row['dataset_name'], param_set, sel_method)
@@ -313,6 +313,12 @@ def load_project_schema(project_name):
313
313
  # delete so it can be reloaded
314
314
  for k in [k for k in sys.modules if 'labdata.schema' in k]:
315
315
  del sys.modules[k]
316
+ # update plugins in-place so existing references stay valid
317
+ import labdata
318
+ labdata.plugins.clear()
319
+ labdata.plugins.update(load_plugins(project=project_name))
320
+ for _name, _mod in labdata.plugins.items():
321
+ setattr(labdata, _name, _mod)
316
322
  return newschema
317
323
 
318
324
  def _get_table(schema,table_name):
@@ -412,12 +418,14 @@ def find_local_filepath(path,allowed_extensions = [],local_paths = None):
412
418
  return p # found allowed extensions (use this to find ProcessedFiles)
413
419
  return None # file not found
414
420
 
415
- def plugin_lazy_import(name):
421
+ def plugin_lazy_import(name, modpath):
416
422
  '''
417
- Lazy import function to load the plugins.
423
+ Lazy import function to load a plugin from modpath.
418
424
  '''
425
+ if 'LABDATA_VERBOSE' in os.environ.keys():
426
+ print(f'[labdata] Loading plugin: {name}')
419
427
  import importlib.util
420
- spec = importlib.util.spec_from_file_location(name, str(Path(prefs['plugins'][name])/"__init__.py"))
428
+ spec = importlib.util.spec_from_file_location(name, str(Path(modpath)/"__init__.py"))
421
429
  loader = importlib.util.LazyLoader(spec.loader)
422
430
  spec.loader = loader
423
431
  module = importlib.util.module_from_spec(spec)
@@ -425,6 +433,37 @@ def plugin_lazy_import(name):
425
433
  loader.exec_module(module)
426
434
  return module
427
435
 
436
+
437
+ def load_plugins(project=None):
438
+ '''Load plugins from preferences, filtered by project.
439
+
440
+ Plugin keys in prefs['plugins'] can be:
441
+ - "plugin_name" load for any project
442
+ - "plugin_name@proj1,proj2" load only for proj1 or proj2
443
+
444
+ When project is None only always-active plugins (no "@") are loaded.
445
+ '''
446
+ result = {}
447
+ if 'plugins' not in prefs:
448
+ return result
449
+ for modkey, modpath in prefs['plugins'].items():
450
+ if '@' in modkey:
451
+ name, projects_str = modkey.split('@', 1)
452
+ allowed = [p.strip() for p in projects_str.split(',')]
453
+ else:
454
+ name = modkey
455
+ allowed = None # no filter — load for any project
456
+
457
+ if allowed is not None and project not in allowed:
458
+ continue
459
+
460
+ try:
461
+ result[name] = plugin_lazy_import(name, modpath)
462
+ except Exception as err:
463
+ print(err)
464
+ print(f'[labdata] Failed to load plugin: {name}')
465
+ return result
466
+
428
467
  def extrapolate_time_from_clock(master_clock,master_events, slave_events):
429
468
  '''
430
469
  Extrapolates the time for synchronizing events on different streams
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labdata
3
- Version: 0.0.25
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
@@ -26,6 +26,7 @@ labdata/compute/suite2p.py
26
26
  labdata/compute/utils.py
27
27
  labdata/dashboard/__init__.py
28
28
  labdata/dashboard/compute.py
29
+ labdata/dashboard/console.py
29
30
  labdata/dashboard/decisiontask.py
30
31
  labdata/dashboard/index.py
31
32
  labdata/dashboard/notes.py
@@ -1,16 +0,0 @@
1
- VERSION = "0.0.25"
2
-
3
- from .utils import *
4
- from .copy import copy_to_upload_server
5
- from .s3 import copy_to_s3
6
-
7
- plugins = {} # to have all plugins in the same place
8
- if 'plugins' in prefs.keys():
9
- for modkey in prefs['plugins'].keys():
10
- try:
11
- modpath = str(Path(prefs['plugins'][modkey])/"__init__.py")
12
- exec(f"{modkey} = plugin_lazy_import('{modkey}')")
13
- exec(f'plugins["{modkey}"] = {modkey}')
14
- except Exception as err:
15
- print(err)
16
- print(f'Failed to load plugin: {modkey}')
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