Skip to content

Commit 33d97bd

Browse files
authored
feat: smartspim histology (#12)
* feat: smartspim histology endpt * adds examples * linters
1 parent 19685dd commit 33d97bd

File tree

18 files changed

+1218
-3
lines changed

18 files changed

+1218
-3
lines changed
Lines changed: 389 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,389 @@
1+
"""
2+
Module to handle fetching histology data from slims and parsing it to a model
3+
"""
4+
5+
from datetime import datetime, timezone
6+
from typing import List, Optional, Tuple
7+
8+
from networkx import DiGraph
9+
from slims.criteria import is_one_of
10+
from slims.internal import Record
11+
12+
from aind_slims_service_server.handlers.table_handler import (
13+
SlimsTableHandler,
14+
)
15+
from aind_slims_service_server.models import (
16+
HistologyReagentData,
17+
HistologyWashData,
18+
SlimsHistologyData,
19+
)
20+
21+
22+
class HistologySessionHandler(SlimsTableHandler):
23+
"""Class to handle getting SPIM Histology Procedures info from SLIMS."""
24+
25+
def _get_reagent_data(
26+
self, records: List[Record]
27+
) -> List[HistologyReagentData]:
28+
"""
29+
Get reagent data from records
30+
Parameters
31+
----------
32+
records : List[Record]
33+
34+
Returns
35+
-------
36+
List[HistologyReagentData]
37+
38+
"""
39+
40+
reagents = []
41+
42+
for record in records:
43+
if record.table_name() == "Content" and self.get_attr_or_none(
44+
record, "cntn_fk_category", "displayValue"
45+
) in [
46+
"Reagents, Externally Manufactured",
47+
"Reagents, Internally Produced",
48+
]:
49+
n_reagent_lot_number = self.get_attr_or_none(
50+
record, "cntn_cf_lotNumber"
51+
)
52+
n_reagent_name = self.get_attr_or_none(
53+
record, "cntn_cf_fk_reagentCatalogNumber", "displayValue"
54+
)
55+
n_reagent_source = self.get_attr_or_none(
56+
record, "cntn_fk_source", "displayValue"
57+
)
58+
reagent_data = HistologyReagentData(
59+
name=n_reagent_name,
60+
source=n_reagent_source,
61+
lot_number=n_reagent_lot_number,
62+
)
63+
reagents.append(reagent_data)
64+
return reagents
65+
66+
def _get_wash_data(
67+
self, g: DiGraph, exp_run_step: str, exp_run_step_row: Record
68+
) -> HistologyWashData:
69+
"""
70+
Get wash data from SLIMS records.
71+
Parameters
72+
----------
73+
g : DiGraph
74+
exp_run_step : str
75+
Name of the node for the experiment run step
76+
exp_run_step_row : Record
77+
The Record attached to the node.
78+
79+
Returns
80+
-------
81+
HistologyWashData
82+
83+
"""
84+
wash_data = HistologyWashData()
85+
wash_data.wash_name = self.get_attr_or_none(
86+
exp_run_step_row, "xprs_name"
87+
)
88+
wash_data.wash_type = self.get_attr_or_none(
89+
exp_run_step_row, "xprs_cf_spimWashType"
90+
)
91+
start_time_ts = self.get_attr_or_none(
92+
exp_run_step_row, "xprs_cf_startTime"
93+
)
94+
wash_data.start_time = (
95+
None
96+
if start_time_ts is None
97+
else datetime.fromtimestamp(start_time_ts / 1000, tz=timezone.utc)
98+
)
99+
end_time_ts = self.get_attr_or_none(
100+
exp_run_step_row, "xprs_cf_endTime"
101+
)
102+
wash_data.end_time = (
103+
None
104+
if end_time_ts is None
105+
else datetime.fromtimestamp(end_time_ts / 1000, tz=timezone.utc)
106+
)
107+
wash_data.modified_by = self.get_attr_or_none(
108+
exp_run_step_row, "xprs_modifiedBy"
109+
)
110+
wash_data.mass = self.get_attr_or_none(
111+
exp_run_step_row, "xprs_cf_mass"
112+
)
113+
wash_data_successors = g.successors(exp_run_step)
114+
records = [g.nodes[n]["row"] for n in wash_data_successors]
115+
reagents = self._get_reagent_data(records)
116+
wash_data.reagents = reagents
117+
return wash_data
118+
119+
def _get_specimen_data(
120+
self, g: DiGraph, exp_run_step_content: str
121+
) -> Tuple[Optional[str], Optional[str]]:
122+
"""
123+
Get subject_id and specimen_id from Content record.
124+
Parameters
125+
----------
126+
g : DiGraph
127+
exp_run_step_content : str
128+
Name of the node for the experiment run step content
129+
130+
Returns
131+
-------
132+
tuple
133+
(subject_id, specimen_id)
134+
135+
"""
136+
content_nodes = g.successors(exp_run_step_content)
137+
records = [g.nodes[c]["row"] for c in content_nodes]
138+
specimen_id = None
139+
subject_id = None
140+
for record in records:
141+
n_subject_id = self.get_attr_or_none(record, "cntn_id")
142+
if n_subject_id is not None:
143+
subject_id = n_subject_id
144+
n_specimen_id = self.get_attr_or_none(record, "cntn_barCode")
145+
if n_specimen_id is not None:
146+
specimen_id = n_specimen_id
147+
return subject_id, specimen_id
148+
149+
def _parse_graph(
150+
self, g: DiGraph, root_nodes: List[str], subject_id: Optional[str]
151+
) -> List[SlimsHistologyData]:
152+
"""
153+
Parses the graph object into a list of pydantic models.
154+
Parameters
155+
----------
156+
g : DiGraph
157+
Graph of the SLIMS records.
158+
root_nodes : List[str]
159+
List of root nodes to pull descendants from.
160+
subject_id : str | None
161+
Labtracks ID of mouse to filter records by.
162+
163+
Returns
164+
-------
165+
List[SlimsHistologyData]
166+
"""
167+
168+
histology_data_list = []
169+
for node in root_nodes:
170+
histology_data = SlimsHistologyData()
171+
washes = []
172+
experiment_run_created_on_ts = self.get_attr_or_none(
173+
g.nodes[node]["row"], "xprn_createdOn"
174+
)
175+
histology_data.experiment_run_created_on = (
176+
None
177+
if experiment_run_created_on_ts is None
178+
else datetime.fromtimestamp(
179+
experiment_run_created_on_ts / 1000, tz=timezone.utc
180+
)
181+
)
182+
exp_run_name = self.get_attr_or_none(
183+
g.nodes[node]["row"], "xptm_name"
184+
)
185+
histology_data.procedure_name = exp_run_name
186+
187+
exp_run_steps = g.successors(node)
188+
189+
for exp_run_step in exp_run_steps:
190+
exp_run_step_row = g.nodes[exp_run_step]["row"]
191+
exp_run_step_name = self.get_attr_or_none(
192+
exp_run_step_row, "xprs_name"
193+
)
194+
if exp_run_step_name in [
195+
"Wash 1",
196+
"Wash 2",
197+
"Wash 3",
198+
"Wash 4",
199+
"Refractive Index Matching Wash",
200+
"Primary Antibody Wash",
201+
"Secondary Antibody Wash",
202+
"MBS Wash",
203+
"Gelation PBS Wash",
204+
"Stock X + VA-044 Equilibration",
205+
"Gelation + ProK RT",
206+
"Gelation + Add'l ProK 37C",
207+
"Final PBS Wash",
208+
]:
209+
wash_data = self._get_wash_data(
210+
g,
211+
exp_run_step=exp_run_step,
212+
exp_run_step_row=exp_run_step_row,
213+
)
214+
washes.append(wash_data)
215+
216+
exp_run_step_children = g.successors(exp_run_step)
217+
for exp_run_step_child in exp_run_step_children:
218+
table_name = g.nodes[exp_run_step_child]["table_name"]
219+
row = g.nodes[exp_run_step_child]["row"]
220+
if table_name == "SOP":
221+
stop_link = self.get_attr_or_none(row, "stop_link")
222+
stop_name = self.get_attr_or_none(row, "stop_name")
223+
histology_data.protocol_id = stop_link
224+
histology_data.protocol_name = stop_name
225+
if table_name == "ExperimentRunStepContent":
226+
n_subject_id, n_specimen_id = self._get_specimen_data(
227+
g=g, exp_run_step_content=exp_run_step_child
228+
)
229+
if n_subject_id is not None:
230+
histology_data.subject_id = n_subject_id
231+
if n_specimen_id is not None:
232+
histology_data.specimen_id = n_specimen_id
233+
histology_data.washes = washes
234+
if subject_id is None or subject_id == histology_data.subject_id:
235+
histology_data_list.append(histology_data)
236+
return histology_data_list
237+
238+
def _get_graph(
239+
self,
240+
start_date_greater_than_or_equal: Optional[datetime] = None,
241+
end_date_less_than_or_equal: Optional[datetime] = None,
242+
) -> Tuple[DiGraph, List[str]]:
243+
"""
244+
Generate a Graph of the records from SLIMS for histology.
245+
246+
Parameters
247+
----------
248+
start_date_greater_than_or_equal : datetime | None
249+
Filter experiment runs that were created on or after this datetime.
250+
end_date_less_than_or_equal : datetime | None
251+
Filter experiment runs that were created on or before this datetime.
252+
253+
Returns
254+
-------
255+
Tuple[DiGraph, List[str]]
256+
A directed graph of the SLIMS records and a list of the root nodes.
257+
258+
"""
259+
experiment_template_rows = self.session.fetch(
260+
table="ExperimentTemplate",
261+
criteria=is_one_of(
262+
"xptm_name",
263+
[
264+
"SmartSPIM Labeling",
265+
"SmartSPIM Delipidation",
266+
"SmartSPIM Refractive Index Matching",
267+
],
268+
),
269+
)
270+
date_criteria = self._get_date_criteria(
271+
start_date=start_date_greater_than_or_equal,
272+
end_date=end_date_less_than_or_equal,
273+
field_name="xprn_createdOn",
274+
)
275+
exp_run_rows = self.get_rows_from_foreign_table(
276+
input_table="ExperimentTemplate",
277+
input_rows=experiment_template_rows,
278+
input_table_cols=["xptm_pk"],
279+
foreign_table="ExperimentRun",
280+
foreign_table_col="xprn_fk_experimentTemplate",
281+
extra_criteria=date_criteria,
282+
)
283+
G = DiGraph()
284+
root_nodes = []
285+
for row in exp_run_rows:
286+
G.add_node(
287+
f"{row.table_name()}.{row.pk()}",
288+
row=row,
289+
pk=row.pk(),
290+
table_name=row.table_name(),
291+
)
292+
root_nodes.append(f"{row.table_name()}.{row.pk()}")
293+
294+
exp_run_step_rows = self.get_rows_from_foreign_table(
295+
input_table="ExperimentRun",
296+
input_rows=exp_run_rows,
297+
input_table_cols=["xprn_pk"],
298+
foreign_table="ExperimentRunStep",
299+
foreign_table_col="xprs_fk_experimentRun",
300+
graph=G,
301+
)
302+
_ = self.get_rows_from_foreign_table(
303+
input_table="ExperimentRunStep",
304+
input_rows=exp_run_step_rows,
305+
input_table_cols=["xprs_cf_fk_protocol"],
306+
foreign_table="SOP",
307+
foreign_table_col="stop_pk",
308+
graph=G,
309+
)
310+
exp_run_step_content_rows = self.get_rows_from_foreign_table(
311+
input_table="ExperimentRunStep",
312+
input_rows=exp_run_step_rows,
313+
input_table_cols=["xprs_pk"],
314+
foreign_table="ExperimentRunStepContent",
315+
foreign_table_col="xrsc_fk_experimentRunStep",
316+
graph=G,
317+
)
318+
_ = self.get_rows_from_foreign_table(
319+
input_table="ExperimentRunStepContent",
320+
input_rows=exp_run_step_content_rows,
321+
input_table_cols=["xrsc_fk_content"],
322+
foreign_table="Content",
323+
foreign_table_col="cntn_pk",
324+
graph=G,
325+
)
326+
reagent_content_rows = self.get_rows_from_foreign_table(
327+
input_table="ExperimentRunStep",
328+
input_rows=exp_run_step_rows,
329+
input_table_cols=["xprs_cf_fk_reagent"],
330+
foreign_table="Content",
331+
foreign_table_col="cntn_pk",
332+
graph=G,
333+
)
334+
_ = self.get_rows_from_foreign_table(
335+
input_table="Content",
336+
input_rows=reagent_content_rows,
337+
input_table_cols=["cntn_cf_fk_reagentCatalogNumber"],
338+
foreign_table="ReferenceDataRecord",
339+
foreign_table_col="rdrc_pk",
340+
graph=G,
341+
)
342+
return G, root_nodes
343+
344+
def get_histology_data_from_slims(
345+
self,
346+
subject_id: Optional[str] = None,
347+
start_date_greater_than_or_equal: Optional[str] = None,
348+
end_date_less_than_or_equal: Optional[str] = None,
349+
) -> List[SlimsHistologyData]:
350+
"""
351+
Get Histology data from SLIMS.
352+
353+
Parameters
354+
----------
355+
subject_id : str | None
356+
Labtracks ID of mouse. If None, then no filter will be performed.
357+
start_date_greater_than_or_equal : str | None
358+
Filter experiment runs that were created on or after this datetime.
359+
end_date_less_than_or_equal : str | None
360+
Filter experiment runs that were created on or before this datetime.
361+
362+
363+
Returns
364+
-------
365+
List[SlimsHistologyData]
366+
367+
Raises
368+
------
369+
ValueError
370+
The subject_id cannot be an empty string.
371+
372+
"""
373+
374+
if subject_id is not None and len(subject_id) == 0:
375+
raise ValueError("subject_id must not be empty!")
376+
377+
G, root_nodes = self._get_graph(
378+
start_date_greater_than_or_equal=self.parse_date(
379+
start_date_greater_than_or_equal
380+
),
381+
end_date_less_than_or_equal=self.parse_date(
382+
end_date_less_than_or_equal
383+
),
384+
)
385+
hist_data = self._parse_graph(
386+
g=G, root_nodes=root_nodes, subject_id=subject_id
387+
)
388+
389+
return hist_data

0 commit comments

Comments
 (0)