sclab 0.1.7__py3-none-any.whl
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.
- sclab/__init__.py +7 -0
- sclab/_io.py +32 -0
- sclab/_sclab.py +80 -0
- sclab/dataset/__init__.py +8 -0
- sclab/dataset/_dataset.py +398 -0
- sclab/dataset/_exceptions.py +2 -0
- sclab/dataset/plotter/__init__.py +7 -0
- sclab/dataset/plotter/_controls.py +594 -0
- sclab/dataset/plotter/_plotter.py +1017 -0
- sclab/dataset/plotter/_utils.py +437 -0
- sclab/dataset/processor/__init__.py +7 -0
- sclab/dataset/processor/_processor.py +1063 -0
- sclab/dataset/processor/step/__init__.py +7 -0
- sclab/dataset/processor/step/_basic_processor_step.py +109 -0
- sclab/dataset/processor/step/_processor_step_base.py +120 -0
- sclab/event/__init__.py +7 -0
- sclab/event/_broker.py +201 -0
- sclab/event/_client.py +81 -0
- sclab/event/_utils.py +14 -0
- sclab/examples/__init__.py +5 -0
- sclab/examples/processor_steps/__init__.py +15 -0
- sclab/examples/processor_steps/_cluster.py +37 -0
- sclab/examples/processor_steps/_neighbors.py +72 -0
- sclab/examples/processor_steps/_pca.py +124 -0
- sclab/examples/processor_steps/_preprocess.py +186 -0
- sclab/examples/processor_steps/_qc.py +93 -0
- sclab/examples/processor_steps/_umap.py +48 -0
- sclab-0.1.7.dist-info/METADATA +139 -0
- sclab-0.1.7.dist-info/RECORD +30 -0
- sclab-0.1.7.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import traceback
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from IPython.display import display
|
|
5
|
+
from ipywidgets.widgets import (
|
|
6
|
+
HTML,
|
|
7
|
+
Button,
|
|
8
|
+
Output,
|
|
9
|
+
VBox,
|
|
10
|
+
)
|
|
11
|
+
from ipywidgets.widgets.valuewidget import ValueWidget
|
|
12
|
+
from ipywidgets.widgets.widget_description import DescriptionWidget
|
|
13
|
+
|
|
14
|
+
from ....event import EventClient
|
|
15
|
+
from .._processor import Processor
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BasicProcessorStep(EventClient):
|
|
19
|
+
events: list[str] = None
|
|
20
|
+
parent: Processor
|
|
21
|
+
description: str
|
|
22
|
+
function_name: str
|
|
23
|
+
function: callable
|
|
24
|
+
fixed_params: dict[str, Any]
|
|
25
|
+
variable_controls: dict[str, DescriptionWidget | ValueWidget]
|
|
26
|
+
output: Output
|
|
27
|
+
run_button: Button
|
|
28
|
+
controls: VBox
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
parent: Processor,
|
|
33
|
+
description: str,
|
|
34
|
+
function: callable,
|
|
35
|
+
fixed_params: dict[str, Any] = {},
|
|
36
|
+
variable_controls: dict[str, DescriptionWidget | ValueWidget] = {},
|
|
37
|
+
use_run_button: bool = True,
|
|
38
|
+
):
|
|
39
|
+
self.parent = parent
|
|
40
|
+
self.description = description
|
|
41
|
+
self.function = function
|
|
42
|
+
self.function_name = function.__name__
|
|
43
|
+
|
|
44
|
+
self.events = [
|
|
45
|
+
f"step_{self.function_name}_started",
|
|
46
|
+
f"step_{self.function_name}_ended",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
self.fixed_params = fixed_params
|
|
50
|
+
self.variable_controls = variable_controls
|
|
51
|
+
self.output = Output()
|
|
52
|
+
|
|
53
|
+
controls = []
|
|
54
|
+
for control in self.variable_controls.values():
|
|
55
|
+
control.layout.width = "98%"
|
|
56
|
+
self.parent.all_controls_list.append(control)
|
|
57
|
+
controls.append(control)
|
|
58
|
+
|
|
59
|
+
self.use_run_button = use_run_button
|
|
60
|
+
if use_run_button:
|
|
61
|
+
self.run_button = Button(description="Run", button_style="primary")
|
|
62
|
+
self.run_button.on_click(self.callback)
|
|
63
|
+
controls.append(self.run_button)
|
|
64
|
+
|
|
65
|
+
controls.append(self.output)
|
|
66
|
+
self.controls = VBox(controls)
|
|
67
|
+
super().__init__(parent.broker)
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def variable_params(self):
|
|
71
|
+
return {key: control.value for key, control in self.variable_controls.items()}
|
|
72
|
+
|
|
73
|
+
def callback(self, _: Button | None = None):
|
|
74
|
+
self.run()
|
|
75
|
+
|
|
76
|
+
def run(self, **extra_params):
|
|
77
|
+
try:
|
|
78
|
+
# extra params will override fixed and variable params
|
|
79
|
+
params = {**self.fixed_params, **self.variable_params, **extra_params}
|
|
80
|
+
|
|
81
|
+
if self.use_run_button:
|
|
82
|
+
self.run_button.disabled = True
|
|
83
|
+
self.run_button.button_style = "warning"
|
|
84
|
+
self.run_button.description = "Running..."
|
|
85
|
+
self.broker.publish(f"step_{self.function_name}_started")
|
|
86
|
+
|
|
87
|
+
self.function(**params)
|
|
88
|
+
self.parent.append_to_step_history(self.description, params)
|
|
89
|
+
|
|
90
|
+
if self.use_run_button:
|
|
91
|
+
self.run_button.button_style = "success"
|
|
92
|
+
|
|
93
|
+
self.broker.publish(f"step_{self.function_name}_ended", status="success")
|
|
94
|
+
|
|
95
|
+
except Exception as e:
|
|
96
|
+
if self.use_run_button:
|
|
97
|
+
self.run_button.button_style = "danger"
|
|
98
|
+
|
|
99
|
+
info = dict(status="failed", error=e, traceback=traceback.format_exc())
|
|
100
|
+
self.broker.publish(f"step_{self.function_name}_ended", **info)
|
|
101
|
+
self.broker.exceptions_log.append(traceback.format_exc())
|
|
102
|
+
|
|
103
|
+
with self.broker.exceptions_output.output:
|
|
104
|
+
display(HTML(f"<pre>{traceback.format_exc()}</pre>"))
|
|
105
|
+
|
|
106
|
+
finally:
|
|
107
|
+
if self.use_run_button:
|
|
108
|
+
self.run_button.description = "Run"
|
|
109
|
+
self.run_button.disabled = False
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import traceback
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from IPython.display import HTML, Markdown, display
|
|
5
|
+
from ipywidgets import Button, Output, VBox
|
|
6
|
+
from ipywidgets.widgets.valuewidget import ValueWidget
|
|
7
|
+
from ipywidgets.widgets.widget_description import DescriptionWidget
|
|
8
|
+
|
|
9
|
+
from ....event import EventClient
|
|
10
|
+
from .._processor import Processor
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ProcessorStepBase(EventClient):
|
|
14
|
+
events: list[str] = None
|
|
15
|
+
parent: Processor
|
|
16
|
+
name: str
|
|
17
|
+
description: str
|
|
18
|
+
fixed_params: dict[str, Any]
|
|
19
|
+
variable_controls: dict[str, DescriptionWidget | ValueWidget]
|
|
20
|
+
output: Output
|
|
21
|
+
run_button: Button
|
|
22
|
+
controls_list: list[DescriptionWidget | ValueWidget | Button]
|
|
23
|
+
controls: VBox
|
|
24
|
+
|
|
25
|
+
run_button_description = "Run"
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
parent: Processor,
|
|
30
|
+
name: str,
|
|
31
|
+
description: str,
|
|
32
|
+
fixed_params: dict[str, Any],
|
|
33
|
+
variable_controls: dict[str, DescriptionWidget | ValueWidget],
|
|
34
|
+
):
|
|
35
|
+
self.parent = parent
|
|
36
|
+
self.name = name
|
|
37
|
+
self.description = description
|
|
38
|
+
self.fixed_params = fixed_params
|
|
39
|
+
self.variable_controls = variable_controls
|
|
40
|
+
|
|
41
|
+
self.events = [
|
|
42
|
+
f"step_{self.name}_started",
|
|
43
|
+
f"step_{self.name}_ended",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
self.output = Output()
|
|
47
|
+
self.run_button = Button(
|
|
48
|
+
description=self.run_button_description, button_style="primary"
|
|
49
|
+
)
|
|
50
|
+
self.run_button.on_click(self.button_callback)
|
|
51
|
+
|
|
52
|
+
self.controls_list = [
|
|
53
|
+
*self.variable_controls.values(),
|
|
54
|
+
self.run_button,
|
|
55
|
+
self.output,
|
|
56
|
+
]
|
|
57
|
+
self.make_controls()
|
|
58
|
+
|
|
59
|
+
super().__init__(parent.broker)
|
|
60
|
+
|
|
61
|
+
def make_controls(self):
|
|
62
|
+
for control in self.controls_list:
|
|
63
|
+
control.layout.width = "98%"
|
|
64
|
+
self.parent.all_controls_list.append(control)
|
|
65
|
+
|
|
66
|
+
self.controls = VBox(children=self.controls_list)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def variable_params(self):
|
|
70
|
+
return {key: control.value for key, control in self.variable_controls.items()}
|
|
71
|
+
|
|
72
|
+
def button_callback(self, _: Button | None = None):
|
|
73
|
+
self.run()
|
|
74
|
+
|
|
75
|
+
def function(self, *pargs, **kwargs):
|
|
76
|
+
raise NotImplementedError
|
|
77
|
+
|
|
78
|
+
def run(self, **extra_params):
|
|
79
|
+
self.output.clear_output(wait=False)
|
|
80
|
+
try:
|
|
81
|
+
# extra params will override fixed and variable params
|
|
82
|
+
params = {**self.fixed_params, **self.variable_params, **extra_params}
|
|
83
|
+
|
|
84
|
+
self.run_button.disabled = True
|
|
85
|
+
self.run_button.button_style = "warning"
|
|
86
|
+
self.run_button.description = "..."
|
|
87
|
+
|
|
88
|
+
self.broker.publish(f"step_{self.name}_started")
|
|
89
|
+
self.function(**params)
|
|
90
|
+
|
|
91
|
+
self.parent.append_to_step_history(self.description, params)
|
|
92
|
+
info = dict(status="success")
|
|
93
|
+
self.run_button.button_style = "success"
|
|
94
|
+
|
|
95
|
+
except Exception as e:
|
|
96
|
+
self.run_button.button_style = "danger"
|
|
97
|
+
|
|
98
|
+
info = dict(status="failed", error=e, traceback=traceback.format_exc())
|
|
99
|
+
self.broker.exceptions_log.append(traceback.format_exc())
|
|
100
|
+
with self.broker.exceptions_output.output:
|
|
101
|
+
display(HTML(f"<pre>{traceback.format_exc()}</pre>"))
|
|
102
|
+
self.update_output(f"{type(e)}: {e}")
|
|
103
|
+
|
|
104
|
+
finally:
|
|
105
|
+
self.run_button.description = self.run_button_description
|
|
106
|
+
self.run_button.disabled = False
|
|
107
|
+
self.broker.publish(f"step_{self.name}_ended", **info)
|
|
108
|
+
|
|
109
|
+
def update_output(self, message: str | Any | None, clear: bool = True):
|
|
110
|
+
if clear:
|
|
111
|
+
self.output.clear_output(wait=True)
|
|
112
|
+
|
|
113
|
+
if isinstance(message, str):
|
|
114
|
+
message = Markdown(message)
|
|
115
|
+
|
|
116
|
+
elif message is None:
|
|
117
|
+
message = Markdown("")
|
|
118
|
+
|
|
119
|
+
with self.output:
|
|
120
|
+
display(message)
|
sclab/event/__init__.py
ADDED
sclab/event/_broker.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import traceback
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
|
|
5
|
+
from IPython.display import HTML, display
|
|
6
|
+
from ipywidgets.widgets import Layout, Output, Tab
|
|
7
|
+
|
|
8
|
+
from ._client import EventClient
|
|
9
|
+
from ._utils import LogOutput
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EventBroker:
|
|
13
|
+
"""Simple event broker for publishing and subscribing to events.
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
```
|
|
17
|
+
broker = EventBroker()
|
|
18
|
+
|
|
19
|
+
def callback(*args, **kwargs):
|
|
20
|
+
print(args, kwargs)
|
|
21
|
+
|
|
22
|
+
broker.subscribe("event", callback)
|
|
23
|
+
broker.publish("event", 1, 2, 3, a=4, b=5)
|
|
24
|
+
```
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
clients: list[EventClient]
|
|
28
|
+
available_events: set[str]
|
|
29
|
+
preemptions: dict[str, list[str]]
|
|
30
|
+
_subscribers: dict[str, list[callable]]
|
|
31
|
+
_disabled_events: list[str]
|
|
32
|
+
|
|
33
|
+
def __init__(self):
|
|
34
|
+
self.clients = []
|
|
35
|
+
self.available_events = set()
|
|
36
|
+
self.preemptions = {}
|
|
37
|
+
self._subscribers = {}
|
|
38
|
+
self._disabled_events = []
|
|
39
|
+
self.depth = 0
|
|
40
|
+
self.std_output = Output(layout=Layout(width="auto", height="500px"))
|
|
41
|
+
self.execution_log = []
|
|
42
|
+
self.execution_output = LogOutput()
|
|
43
|
+
self.exceptions_log = []
|
|
44
|
+
self.exceptions_output = LogOutput()
|
|
45
|
+
self.id = uuid4()
|
|
46
|
+
self.event_log: Output = Output(
|
|
47
|
+
layout=Layout(height="200px"),
|
|
48
|
+
# style={"overflow-y": "scroll"}, TODO: it seems it the way to set this has changed
|
|
49
|
+
)
|
|
50
|
+
self.logs_tab = Tab(
|
|
51
|
+
[
|
|
52
|
+
self.std_output,
|
|
53
|
+
self.execution_output,
|
|
54
|
+
self.exceptions_output,
|
|
55
|
+
],
|
|
56
|
+
titles=[
|
|
57
|
+
"Standard Output",
|
|
58
|
+
"Events",
|
|
59
|
+
"Exceptions",
|
|
60
|
+
],
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def register_client(self, client: EventClient):
|
|
64
|
+
if not isinstance(client, EventClient):
|
|
65
|
+
raise TypeError("client must be an instance of EventClient")
|
|
66
|
+
|
|
67
|
+
if client in self.clients:
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
self.clients.append(client)
|
|
71
|
+
self.available_events.update(client.events)
|
|
72
|
+
self.preemptions.update(client.preemptions)
|
|
73
|
+
client.broker = self
|
|
74
|
+
|
|
75
|
+
def unregister_client(self, client: EventClient):
|
|
76
|
+
# TODO: ensure full cleanup of subscriptions
|
|
77
|
+
|
|
78
|
+
if client in self.clients:
|
|
79
|
+
self.clients.remove(client)
|
|
80
|
+
|
|
81
|
+
self.available_events.difference_update(client.events)
|
|
82
|
+
|
|
83
|
+
self._disabled_events = [
|
|
84
|
+
e for e in self._disabled_events if e not in client.events
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
for event in client.events:
|
|
88
|
+
self._subscribers.pop(event, None)
|
|
89
|
+
self.preemptions.pop(event, None)
|
|
90
|
+
|
|
91
|
+
def update_subscriptions(self):
|
|
92
|
+
for client in self.clients:
|
|
93
|
+
for event in self.available_events:
|
|
94
|
+
if callback := getattr(client, f"{event}_callback", None):
|
|
95
|
+
self.subscribe(event, callback)
|
|
96
|
+
client.subscriptions[event] = callback
|
|
97
|
+
|
|
98
|
+
def subscribe(self, event: str, callback: callable):
|
|
99
|
+
if event not in self._subscribers:
|
|
100
|
+
self._subscribers[event] = []
|
|
101
|
+
|
|
102
|
+
if callback not in self._subscribers[event]:
|
|
103
|
+
# Prevent duplicate subscriptions
|
|
104
|
+
self._subscribers[event].append(callback)
|
|
105
|
+
|
|
106
|
+
def unsubscribe(self, event: str, callback: callable):
|
|
107
|
+
if event in self._subscribers and callback in self._subscribers[event]:
|
|
108
|
+
self._subscribers[event].remove(callback)
|
|
109
|
+
|
|
110
|
+
def publish(self, event: str, *args, **kwargs):
|
|
111
|
+
if event not in self.available_events:
|
|
112
|
+
raise ValueError(f"Event '{event}' is not available.")
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
self.depth += 1
|
|
116
|
+
tab = " " * self.depth * 4
|
|
117
|
+
txt_args = []
|
|
118
|
+
for arg in args:
|
|
119
|
+
if isinstance(arg, str | float | int | bool | tuple | Exception | None):
|
|
120
|
+
txt_args.append(arg)
|
|
121
|
+
else:
|
|
122
|
+
txt_args.append(type(arg))
|
|
123
|
+
txt_kwargs = {}
|
|
124
|
+
for k, v in kwargs.items():
|
|
125
|
+
if isinstance(v, str | float | int | bool | tuple | Exception | None):
|
|
126
|
+
txt_kwargs[k] = v
|
|
127
|
+
else:
|
|
128
|
+
txt_kwargs[k] = type(v)
|
|
129
|
+
|
|
130
|
+
if event in self._disabled_events:
|
|
131
|
+
msg = f"{tab}Disabled Event: {event}."
|
|
132
|
+
self.execution_log.append(msg)
|
|
133
|
+
with self.execution_output.output:
|
|
134
|
+
display(HTML(f"<pre>{msg}</pre>"))
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
msg = f"{tab}{event}. Args: {tuple(txt_args)}. Kwargs: {txt_kwargs}"
|
|
138
|
+
self.execution_log.append(msg)
|
|
139
|
+
with self.execution_output.output:
|
|
140
|
+
display(HTML(f"<pre>{msg}</pre>"))
|
|
141
|
+
|
|
142
|
+
preempt = self.preemptions.get(event, [])
|
|
143
|
+
with self.disable(preempt):
|
|
144
|
+
if event in self._subscribers:
|
|
145
|
+
for callback in self._subscribers[event]:
|
|
146
|
+
if hasattr(callback, "__self__"):
|
|
147
|
+
parent_class = callback.__self__.__class__.__name__
|
|
148
|
+
else:
|
|
149
|
+
parent_class = "<local context>"
|
|
150
|
+
msg = f"{tab}{event} --> {parent_class}.{callback.__name__}()"
|
|
151
|
+
self.execution_log.append(msg)
|
|
152
|
+
with self.execution_output.output:
|
|
153
|
+
display(HTML(f"<pre>{msg}</pre>"))
|
|
154
|
+
|
|
155
|
+
callback(*args, **kwargs)
|
|
156
|
+
except Exception as e:
|
|
157
|
+
msg = f"{tab} Exception: {e}"
|
|
158
|
+
self.execution_log.append(msg)
|
|
159
|
+
with self.execution_output.output:
|
|
160
|
+
display(HTML(f"<pre>{msg}</pre>"))
|
|
161
|
+
|
|
162
|
+
msg = f"{msg}{tab} Exception: {e}"
|
|
163
|
+
msg += f"\n\n{traceback.format_exc()}"
|
|
164
|
+
self.exceptions_log.append(msg)
|
|
165
|
+
|
|
166
|
+
with self.exceptions_output.output:
|
|
167
|
+
display(HTML(f"<pre>{traceback.format_exc()}</pre>"))
|
|
168
|
+
|
|
169
|
+
finally:
|
|
170
|
+
self.depth -= 1
|
|
171
|
+
|
|
172
|
+
def disable_event(self, event: str):
|
|
173
|
+
self._disabled_events.append(event)
|
|
174
|
+
|
|
175
|
+
def enable_event(self, event: str):
|
|
176
|
+
if event in self._disabled_events:
|
|
177
|
+
self._disabled_events.remove(event)
|
|
178
|
+
|
|
179
|
+
@contextmanager
|
|
180
|
+
def disable(self, events: str | list[str]):
|
|
181
|
+
if isinstance(events, str):
|
|
182
|
+
events = [events]
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
for event in events:
|
|
186
|
+
self.disable_event(event)
|
|
187
|
+
yield
|
|
188
|
+
finally:
|
|
189
|
+
for event in events:
|
|
190
|
+
self.enable_event(event)
|
|
191
|
+
|
|
192
|
+
@contextmanager
|
|
193
|
+
def delay(self, events: str | list[str]):
|
|
194
|
+
if isinstance(events, str):
|
|
195
|
+
events = [events]
|
|
196
|
+
|
|
197
|
+
# TODO: implement delay context manager
|
|
198
|
+
# the idea is to catch these events and execute them after the current event is done
|
|
199
|
+
# if the delayed event is duplicated, it should be executed only once
|
|
200
|
+
# we could generate a execution tree and execute the events in the correct order
|
|
201
|
+
pass
|
sclab/event/_client.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from uuid import uuid4
|
|
2
|
+
|
|
3
|
+
from ipywidgets.widgets import Button, Output
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# forward declaration
|
|
7
|
+
class EventBroker:
|
|
8
|
+
clients: list["EventClient"]
|
|
9
|
+
available_events: set[str]
|
|
10
|
+
preemptions: dict[str, list[str]]
|
|
11
|
+
_subscribers: dict[str, list[callable]]
|
|
12
|
+
_disabled_events: list[str]
|
|
13
|
+
std_output: Output
|
|
14
|
+
|
|
15
|
+
def register_client(self, client: "EventClient"):
|
|
16
|
+
...
|
|
17
|
+
|
|
18
|
+
def unregister_client(self, client: "EventClient"):
|
|
19
|
+
...
|
|
20
|
+
|
|
21
|
+
def update_subscriptions(self):
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
def subscribe(self, event: str, callback: callable):
|
|
25
|
+
...
|
|
26
|
+
|
|
27
|
+
def unsubscribe(self, event: str, callback: callable):
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
def publish(self, event: str, *args, **kwargs):
|
|
31
|
+
...
|
|
32
|
+
|
|
33
|
+
def disable_event(self, event: str):
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
def enable_event(self, event: str):
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class EventClient:
|
|
41
|
+
subscriptions: dict[str, callable]
|
|
42
|
+
preemptions: dict[str, list[str]] = {} # override this in subclasses if needed
|
|
43
|
+
broker: "EventBroker"
|
|
44
|
+
events: list[str]
|
|
45
|
+
|
|
46
|
+
def __init__(self, broker: "EventBroker"):
|
|
47
|
+
self.uuid = uuid4().hex
|
|
48
|
+
self.subscriptions = {}
|
|
49
|
+
broker.register_client(self)
|
|
50
|
+
broker.update_subscriptions()
|
|
51
|
+
|
|
52
|
+
def button_click_event_publisher(self, control_prefix: str, control_name: str):
|
|
53
|
+
event_str = f"{control_prefix}_{control_name}_click"
|
|
54
|
+
assert (
|
|
55
|
+
event_str in self.events
|
|
56
|
+
), f"Event {event_str} not in {self.__class__.__name__}.events list."
|
|
57
|
+
|
|
58
|
+
def publisher(_: Button):
|
|
59
|
+
self.broker.publish(event_str)
|
|
60
|
+
|
|
61
|
+
return publisher
|
|
62
|
+
|
|
63
|
+
def control_value_change_event_publisher(
|
|
64
|
+
self, control_prefix: str, control_name: str
|
|
65
|
+
):
|
|
66
|
+
event_str = f"{control_prefix}_{control_name}_change"
|
|
67
|
+
assert (
|
|
68
|
+
event_str in self.events
|
|
69
|
+
), f"Event {event_str} not in {self.__class__.__name__}.events list."
|
|
70
|
+
|
|
71
|
+
def publisher(event: dict):
|
|
72
|
+
if event["type"] != "change":
|
|
73
|
+
raise ValueError("Event type must be 'change'")
|
|
74
|
+
new_value = event["new"]
|
|
75
|
+
self.broker.publish(event_str, new_value=new_value)
|
|
76
|
+
|
|
77
|
+
return publisher
|
|
78
|
+
|
|
79
|
+
def __del__(self):
|
|
80
|
+
for event, callback in self.subscriptions.items():
|
|
81
|
+
self.broker.unsubscribe(event, callback)
|
sclab/event/_utils.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from ipywidgets.widgets import Button, Layout, Output, VBox
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class LogOutput(VBox):
|
|
5
|
+
def __init__(self):
|
|
6
|
+
self.clear_button = Button(description="Clear", button_style="warning")
|
|
7
|
+
self.output = Output(layout=Layout(width="auto", height="500px"))
|
|
8
|
+
|
|
9
|
+
super().__init__(children=[self.clear_button, self.output])
|
|
10
|
+
|
|
11
|
+
self.clear_button.on_click(self._clear)
|
|
12
|
+
|
|
13
|
+
def _clear(self, _):
|
|
14
|
+
self.output.clear_output()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from ._cluster import Cluster
|
|
2
|
+
from ._neighbors import Neighbors
|
|
3
|
+
from ._pca import PCA
|
|
4
|
+
from ._preprocess import Preprocess
|
|
5
|
+
from ._qc import QC
|
|
6
|
+
from ._umap import UMAP
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"QC",
|
|
10
|
+
"Preprocess",
|
|
11
|
+
"PCA",
|
|
12
|
+
"Neighbors",
|
|
13
|
+
"UMAP",
|
|
14
|
+
"Cluster",
|
|
15
|
+
]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from ipywidgets import FloatSlider
|
|
2
|
+
|
|
3
|
+
from sclab.dataset.processor import Processor
|
|
4
|
+
from sclab.dataset.processor.step import ProcessorStepBase
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Cluster(ProcessorStepBase):
|
|
8
|
+
parent: Processor
|
|
9
|
+
|
|
10
|
+
def __init__(self, parent: Processor) -> None:
|
|
11
|
+
try:
|
|
12
|
+
import scanpy as sc # noqa: F401
|
|
13
|
+
except ImportError:
|
|
14
|
+
raise ImportError("Please install scanpy: `pip install scanpy`")
|
|
15
|
+
|
|
16
|
+
variable_controls = dict(
|
|
17
|
+
resolution=FloatSlider(
|
|
18
|
+
value=1.0, min=0.1, max=10.0, step=0.1, description="Resolution"
|
|
19
|
+
)
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
super().__init__(
|
|
23
|
+
parent=parent,
|
|
24
|
+
name="cluster",
|
|
25
|
+
description="Cluster",
|
|
26
|
+
fixed_params={},
|
|
27
|
+
variable_controls=variable_controls,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def function(self, resolution: float = 1.0):
|
|
31
|
+
import scanpy as sc
|
|
32
|
+
|
|
33
|
+
dataset = self.parent.dataset
|
|
34
|
+
adata = self.parent.dataset.adata
|
|
35
|
+
sc.tl.leiden(adata, resolution=resolution)
|
|
36
|
+
|
|
37
|
+
self.broker.publish("dset_metadata_change", dataset.metadata, "leiden")
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from ipywidgets import Dropdown, IntText
|
|
2
|
+
|
|
3
|
+
from sclab.dataset.processor import Processor
|
|
4
|
+
from sclab.dataset.processor.step import ProcessorStepBase
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Neighbors(ProcessorStepBase):
|
|
8
|
+
parent: Processor
|
|
9
|
+
|
|
10
|
+
def __init__(self, parent: Processor) -> None:
|
|
11
|
+
try:
|
|
12
|
+
import scanpy as sc # noqa: F401
|
|
13
|
+
except ImportError:
|
|
14
|
+
raise ImportError("Please install scanpy: `pip install scanpy`")
|
|
15
|
+
|
|
16
|
+
variable_controls = dict(
|
|
17
|
+
use_rep=Dropdown(
|
|
18
|
+
options=tuple(parent.dataset.adata.obsm.keys()),
|
|
19
|
+
value=None,
|
|
20
|
+
description="Use rep.",
|
|
21
|
+
),
|
|
22
|
+
n_neighbors=IntText(value=20, description="N neighbors"),
|
|
23
|
+
n_dims=IntText(value=10, description="N Dims"),
|
|
24
|
+
metric=Dropdown(
|
|
25
|
+
options=["euclidean", "cosine"],
|
|
26
|
+
value="euclidean",
|
|
27
|
+
description="Metric",
|
|
28
|
+
),
|
|
29
|
+
**parent.make_groupbybatch_checkbox(),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
super().__init__(
|
|
33
|
+
parent=parent,
|
|
34
|
+
name="neighbors",
|
|
35
|
+
description="Neighbors",
|
|
36
|
+
fixed_params={},
|
|
37
|
+
variable_controls=variable_controls,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def function(
|
|
41
|
+
self,
|
|
42
|
+
n_neighbors: int = 20,
|
|
43
|
+
use_rep: str = "X_pca",
|
|
44
|
+
n_dims: int = 10,
|
|
45
|
+
metric: str = "euclidean",
|
|
46
|
+
group_by_batch: bool = False,
|
|
47
|
+
):
|
|
48
|
+
import scanpy as sc
|
|
49
|
+
|
|
50
|
+
adata = self.parent.dataset.adata
|
|
51
|
+
|
|
52
|
+
if group_by_batch and self.parent.batch_key:
|
|
53
|
+
group_by = self.parent.batch_key
|
|
54
|
+
sc.external.pp.bbknn(
|
|
55
|
+
adata,
|
|
56
|
+
batch_key=group_by,
|
|
57
|
+
use_rep=use_rep,
|
|
58
|
+
n_pcs=n_dims,
|
|
59
|
+
use_annoy=False,
|
|
60
|
+
metric=metric,
|
|
61
|
+
pynndescent_n_neighbors=n_neighbors,
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
sc.pp.neighbors(
|
|
65
|
+
adata,
|
|
66
|
+
n_neighbors=n_neighbors,
|
|
67
|
+
use_rep=use_rep,
|
|
68
|
+
n_pcs=n_dims,
|
|
69
|
+
metric=metric,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
self.broker.publish("dset_anndata_neighbors_change")
|