Skip to content

Commit

Permalink
Add nonblocking counts endpoint (#345)
Browse files Browse the repository at this point in the history
* Add nonblocking counts endpoint

* Update test data artifacts
  • Loading branch information
jimmymathews authored Aug 2, 2024
1 parent bfd569e commit 020bab6
Show file tree
Hide file tree
Showing 6 changed files with 61 additions and 19 deletions.
17 changes: 17 additions & 0 deletions spatialprofilingtoolbox/apiserver/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,19 @@ def _get_anonymous_phenotype_counts_fast(
return counts


@app.get("/phenotype-counts/")
async def get_phenotype_counts_nonblocking(
positive_marker: ValidChannelListPositives,
negative_marker: ValidChannelListNegatives,
study: ValidStudy,
) -> PhenotypeCounts:
"""Computes the number of cells satisfying the given positive and negative criteria, in the
context of a given study. Non-blocking, has a "pending" flag in the response.
"""
counts = get_phenotype_counts(positive_marker, negative_marker, study, 0, blocking=False)
return counts


@app.get("/request-spatial-metrics-computation/")
async def request_spatial_metrics_computation(
study: ValidStudy,
Expand Down Expand Up @@ -333,13 +346,15 @@ def get_phenotype_counts_cached(
study: str,
number_cells: int,
selected: tuple[int, ...],
blocking: bool = True,
) -> PhenotypeCounts:
counts = OnDemandRequester.get_counts_by_specimen(
positives,
negatives,
study,
number_cells,
set(selected) if selected is not None else None,
blocking = blocking,
)
return counts

Expand All @@ -350,6 +365,7 @@ def get_phenotype_counts(
study: ValidStudy,
number_cells: int,
cells_selected: set[int] | None = None,
blocking: bool = True,
) -> PhenotypeCounts:
"""For each specimen, return the fraction of selected/all cells expressing the phenotype."""
positive_markers = [m for m in positive_marker if m != '']
Expand All @@ -360,6 +376,7 @@ def get_phenotype_counts(
study,
number_cells,
tuple(sorted(list(cells_selected))) if cells_selected is not None else (),
blocking = blocking,
)
return counts

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class PhenotypeCounts(BaseModel):
counts: tuple[PhenotypeCount, ...]
phenotype: CompositePhenotype
number_cells_in_study: int
is_pending: bool


class UnivariateMetricsComputationResult(BaseModel):
Expand Down
20 changes: 14 additions & 6 deletions spatialprofilingtoolbox/ondemand/request_scheduling.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,14 @@ def get_counts_by_specimen(
study_name: str,
number_cells: int,
cells_selected: set[int],
blocking: bool = True,
) -> PhenotypeCounts:
phenotype = PhenotypeCriteria(
positive_markers=tuple(filter(_nonempty, positives)),
negative_markers=tuple(filter(_nonempty, negatives)),
)
selected = tuple(sorted(list(cells_selected))) if cells_selected is not None else ()
feature1, counts, counts_all = OnDemandRequester._counts(study_name, phenotype, selected)
feature1, counts, counts_all, pending = OnDemandRequester._counts(study_name, phenotype, selected, blocking)
combined_keys = sorted(list(set(counts.values.keys()).intersection(counts_all.values.keys())))
missing_numerator = set(counts.values.keys()).difference(combined_keys)
if len(missing_numerator) > 0:
Expand All @@ -127,12 +128,17 @@ def get_counts_by_specimen(
criteria=phenotype,
),
number_cells_in_study=number_cells,
is_pending=pending,
)

@classmethod
def _counts(
cls, study_name: str, phenotype: PhenotypeCriteria, selected: tuple[int, ...],
) -> tuple[str, Metrics1D, Metrics1D]:
cls,
study_name: str,
phenotype: PhenotypeCriteria,
selected: tuple[int, ...],
blocking: bool,
) -> tuple[str, Metrics1D, Metrics1D, bool]:
get = CountsProvider.get_metrics_or_schedule

def get_results1() -> tuple[Metrics1D, str]:
Expand All @@ -153,15 +159,17 @@ def get_results2() -> tuple[Metrics1D, str]:

with DBConnection() as connection:
connection._set_autocommit(True)
cls._wait_for_wrappedup(connection, get_results1, study_name)
if blocking:
cls._wait_for_wrappedup(connection, get_results1, study_name)
counts, feature1 = get_results1()

with DBConnection() as connection:
connection._set_autocommit(True)
cls._wait_for_wrappedup(connection, get_results2, study_name)
if blocking:
cls._wait_for_wrappedup(connection, get_results2, study_name)
counts_all, _ = get_results2()

return (feature1, counts, counts_all)
return (feature1, counts, counts_all, counts.is_pending or counts_all.is_pending)

@classmethod
def _wait_for_wrappedup(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,43 @@
STUDY_NAME = quote('Melanoma intralesional IL2')
POSITIVE_MARKERS = ['CD3', 'CD4', 'CD8']
NEGATIVE_MARKERS: list[str] = ['']
ENDPOINT = 'anonymous-phenotype-counts-fast'
ENDPOINTS = ('anonymous-phenotype-counts-fast', 'phenotype-counts')
HOST = 'spt-apiserver-testing'
PORT = 8080

def main():
cases = [
(HOST, PORT, ENDPOINT, STUDY_NAME, POSITIVE_MARKERS, NEGATIVE_MARKERS, 7),
(HOST, PORT, ENDPOINT, STUDY_NAME, NEGATIVE_MARKERS, POSITIVE_MARKERS, 352),
(HOST, PORT, ENDPOINTS[0], STUDY_NAME, POSITIVE_MARKERS, NEGATIVE_MARKERS, 7),
(HOST, PORT, ENDPOINTS[0], STUDY_NAME, NEGATIVE_MARKERS, POSITIVE_MARKERS, 352),
(HOST, PORT, ENDPOINTS[1], STUDY_NAME, POSITIVE_MARKERS, NEGATIVE_MARKERS, 7),
(HOST, PORT, ENDPOINTS[1], STUDY_NAME, NEGATIVE_MARKERS, POSITIVE_MARKERS, 352),
]

for host, port, endpoint, study_name, positive_markers, negative_markers, expected in cases:
clause1 = '&'.join([f'positive_marker={m}' for m in positive_markers])
clause2 = '&'.join([f'negative_marker={m}' for m in negative_markers])
url = f'http://{host}:{port}/{endpoint}/?study={study_name}&'\
f'{clause1}&'\
f'{clause2}'
result = subprocess.run(
['curl', '-s', url],
capture_output=True,
encoding='UTF-8',
check=True,
).stdout
response = json.loads(result)

if endpoint == ENDPOINTS[0]:
result = subprocess.run(
['curl', '-s', url],
capture_output=True,
encoding='UTF-8',
check=True,
).stdout
response = json.loads(result)
else:
while True:
result = subprocess.run(
['curl', '-s', url],
capture_output=True,
encoding='UTF-8',
check=True,
).stdout
response = json.loads(result)
if not response['is_pending']:
break
phenotype_total = sum(
phenotype_count['count'] for phenotype_count in response['counts']
)
Expand Down
3 changes: 2 additions & 1 deletion test/ondemand/module_tests/expected_counts_structured1.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,6 @@
]
}
},
"number_cells_in_study": 0
"number_cells_in_study": 0,
"is_pending": false
}
3 changes: 2 additions & 1 deletion test/ondemand/module_tests/expected_counts_structured2.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,6 @@
]
}
},
"number_cells_in_study": 0
"number_cells_in_study": 0,
"is_pending": false
}

0 comments on commit 020bab6

Please sign in to comment.