Skip to content

Commit

Permalink
Consolidate spatial metrics (#191)
Browse files Browse the repository at this point in the history
* Consolidated squidpy and proximity into spatial metrics endpoints.

* Version bump.
  • Loading branch information
jimmymathews authored Aug 21, 2023
1 parent 69ce946 commit c1592d9
Show file tree
Hide file tree
Showing 9 changed files with 59 additions and 84 deletions.
86 changes: 36 additions & 50 deletions spatialprofilingtoolbox/apiserver/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
Channel,
PhenotypeCriteria,
PhenotypeCounts,
ProximityMetricsComputationResult,
SquidpyMetricsComputationResult,
UnivariateMetricsComputationResult,
)
from spatialprofilingtoolbox.db.exchange_data_formats.metrics import UMAPChannel
from spatialprofilingtoolbox.db.querying import query
Expand All @@ -28,13 +27,11 @@
ValidStudy,
ValidPhenotypeSymbol,
ValidPhenotypeList,
ValidPhenotype1,
ValidPhenotype2,
ValidChannelListPositives,
ValidChannelListNegatives,
ValidChannelListPositives2,
ValidChannelListNegatives2,
ValidSquidpyFeatureClass,
ValidFeatureClass,
)
VERSION = '0.10.0'

Expand Down Expand Up @@ -163,41 +160,14 @@ async def get_anonymous_phenotype_counts_fast(
return counts


@app.get("/request-phenotype-proximity-computation/")
async def request_phenotype_proximity_computation(
study: ValidStudy,
phenotype1: ValidPhenotype1,
phenotype2: ValidPhenotype2,
radius: int = Query(default=100),
) -> ProximityMetricsComputationResult:
"""Spatial proximity statistics between pairs of cell populations defined by phenotype criteria.
The metric is the average number of cells of a second phenotype within a fixed distance to a
given cell of a primary phenotype.
"""
retrieve = query().retrieve_signature_of_phenotype
criteria1 = retrieve(phenotype1, study)
criteria2 = retrieve(phenotype2, study)
with OnDemandRequester() as requester:
metrics = requester.get_proximity_metrics(
query().get_study_components(study).measurement,
radius, (
criteria1.positive_markers,
criteria1.negative_markers,
criteria2.positive_markers,
criteria2.negative_markers,
),
)
return metrics


@app.get("/request-squidpy-computation/")
async def request_squidpy_computation(
@app.get("/request-spatial-metrics-computation/")
async def request_spatial_metrics_computation(
study: ValidStudy,
phenotype: ValidPhenotypeList,
feature_class: ValidSquidpyFeatureClass,
feature_class: ValidFeatureClass,
radius: float | None = None,
) -> SquidpyMetricsComputationResult:
"""Spatial proximity statistics between phenotype cell sets as calculated by Squidpy."""
) -> UnivariateMetricsComputationResult:
"""Spatial proximity statistics between phenotype cell sets, as calculated by Squidpy."""
phenotypes = phenotype
criteria: list[PhenotypeCriteria] = [
query().retrieve_signature_of_phenotype(p, study) for p in phenotypes
Expand All @@ -209,44 +179,60 @@ async def request_squidpy_computation(
return get_squidpy_metrics(study, markers, feature_class, radius=radius)


@app.get("/request-squidpy-computation-custom-phenotype/")
async def request_squidpy_computation_custom_phenotype(
@app.get("/request-spatial-metrics-computation-custom-phenotype/")
async def request_spatial_metrics_computation_custom_phenotype(
study: ValidStudy,
positive_marker: ValidChannelListPositives,
negative_marker: ValidChannelListNegatives,
feature_class: ValidSquidpyFeatureClass,
feature_class: ValidFeatureClass,
radius: float | None = None,
) -> SquidpyMetricsComputationResult:
"""Spatial proximity statistics for a single custom-defined phenotype (cell set) as calculated
by Squidpy.
) -> UnivariateMetricsComputationResult:
"""Spatial proximity statistics for a single custom-defined phenotype (cell set), as
calculated by Squidpy.
"""
markers = [positive_marker, negative_marker]
return get_squidpy_metrics(study, markers, feature_class, radius=radius)


@app.get("/request-squidpy-computation-custom-phenotypes/")
async def request_squidpy_computation_custom_phenotypes( # pylint: disable=too-many-arguments
@app.get("/request-spatial-metrics-computation-custom-phenotypes/")
async def request_spatial_metrics_computation_custom_phenotypes( # pylint: disable=too-many-arguments
study: ValidStudy,
positive_marker: ValidChannelListPositives,
negative_marker: ValidChannelListNegatives,
positive_marker2: ValidChannelListPositives2,
negative_marker2: ValidChannelListNegatives2,
feature_class: ValidSquidpyFeatureClass,
feature_class: ValidFeatureClass,
radius: float | None = None,
) -> SquidpyMetricsComputationResult:
"""Spatial proximity statistics for a pair of custom-defined phenotype (cell set) as calculated
by Squidpy.
) -> UnivariateMetricsComputationResult:
"""Spatial proximity statistics for a pair of custom-defined phenotypes (cell sets), most
calculated by Squidpy.
"""
markers = [positive_marker, negative_marker, positive_marker2, negative_marker2]
if feature_class == 'proximity':
return get_proximity_metrics(study, markers, radius=radius)
return get_squidpy_metrics(study, markers, feature_class, radius=radius)


def get_proximity_metrics(
study: str,
markers: list[list[str]],
radius: float | None = None,
) -> UnivariateMetricsComputationResult:
with OnDemandRequester() as requester:
metrics = requester.get_proximity_metrics(
query().get_study_components(study).measurement,
radius,
markers,
)
return metrics


def get_squidpy_metrics(
study: str,
markers: list[list[str]],
feature_class: str,
radius: float | None = None,
) -> SquidpyMetricsComputationResult:
) -> UnivariateMetricsComputationResult:
with OnDemandRequester() as requester:
metrics = requester.get_squidpy_metrics(
query().get_study_components(study).measurement,
Expand Down
8 changes: 3 additions & 5 deletions spatialprofilingtoolbox/apiserver/app/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,10 @@ async def valid_channel_list_negatives2(negative_marker2: ChannelList) -> list[s
return valid_channel_list(negative_marker2)


async def valid_squidpy_feature_classname(
async def valid_spatial_feature_classname(
feature_class: str = Query(min_length=1, max_length=100),
) -> str:
if feature_class not in squidpy_feature_classnames():
if feature_class not in (list(squidpy_feature_classnames()) + ['proximity']):
raise ValueError(f'Feature class "{feature_class}" does not exist.')
return feature_class

Expand All @@ -102,11 +102,9 @@ async def valid_squidpy_feature_classname(
ValidStudy = Annotated[str, Depends(valid_study_name)]
ValidPhenotypeSymbol = Annotated[str, Depends(valid_phenotype_symbol)]
ValidPhenotype = Annotated[str, Depends(valid_phenotype)]
ValidPhenotype1 = Annotated[str, Depends(valid_phenotype1)]
ValidPhenotype2 = Annotated[str, Depends(valid_phenotype2)]
ValidPhenotypeList = Annotated[list[str], Depends(valid_phenotype_list)]
ValidChannelListPositives = Annotated[list[str], Depends(valid_channel_list_positives)]
ValidChannelListNegatives = Annotated[list[str], Depends(valid_channel_list_negatives)]
ValidChannelListPositives2 = Annotated[list[str], Depends(valid_channel_list_positives2)]
ValidChannelListNegatives2 = Annotated[list[str], Depends(valid_channel_list_negatives2)]
ValidSquidpyFeatureClass = Annotated[str, Depends(valid_squidpy_feature_classname)]
ValidFeatureClass = Annotated[str, Depends(valid_spatial_feature_classname)]
14 changes: 3 additions & 11 deletions spatialprofilingtoolbox/db/exchange_data_formats/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,22 +71,14 @@ class PhenotypeCounts(BaseModel):
number_cells_in_study: int


class ProximityMetricsComputationResult(BaseModel):
"""The response to a request for retrieval of proximity metrics in some specific case. This
request may also be a request for computation of these metrics in the background (which may be
pending).
class UnivariateMetricsComputationResult(BaseModel):
"""The response to a request for retrieval of derived/computed metrics (typically a spatially-
enrich feature), or a request for such metrics to be computed as a background job.
"""
values: dict[str, float | None]
is_pending: bool


class SquidpyMetricsComputationResult(BaseModel):
"""Response to an on demand request for computation of Squidpy metrics."""

values: dict[str, float | None]
is_pending: bool


class UMAPChannel(BaseModel):
"""A UMAP dimensional reduction of a cell set, with one intensity channel's overlay.
The image is encoded in base 64.
Expand Down
2 changes: 1 addition & 1 deletion spatialprofilingtoolbox/ondemand/request_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ def _get_proximity_metrics(self, study, radius, signature):
def _get_phenotype_pair_specification(self, groups):
record_separator = chr(30)
study_name = groups[0]
radius = int(groups[1])
radius = float(groups[1])
channel_lists = [
self._trim_empty_entry(group.split(record_separator))
for group in groups[2:6]
Expand Down
11 changes: 5 additions & 6 deletions spatialprofilingtoolbox/ondemand/service_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
PhenotypeCount,
PhenotypeCounts,
CompositePhenotype,
ProximityMetricsComputationResult,
SquidpyMetricsComputationResult,
UnivariateMetricsComputationResult,
)


Expand Down Expand Up @@ -72,7 +71,7 @@ def get_proximity_metrics(
study: str,
radius: int,
signature: tuple[list[str], list[str], list[str], list[str]]
) -> ProximityMetricsComputationResult:
) -> UnivariateMetricsComputationResult:
positives1, negatives1, positives2, negatives2 = signature
separator = self._get_record_separator()
groups = [
Expand All @@ -87,7 +86,7 @@ def get_proximity_metrics(
query = self._get_group_separator().join(groups).encode('utf-8')
self.tcp_client.sendall(query)
response = self._parse_response()
return ProximityMetricsComputationResult(
return UnivariateMetricsComputationResult(
values=response['metrics'],
is_pending=response['pending'],
)
Expand Down Expand Up @@ -127,7 +126,7 @@ def get_squidpy_metrics(
signature: list[list[str]],
feature_class: str,
radius: float | None = None,
) -> SquidpyMetricsComputationResult:
) -> UnivariateMetricsComputationResult:
"""Get spatial proximity statistics between phenotype clusters as calculated by Squidpy."""
if not len(signature) in {2, 4}:
message = f'Expected 2 or 4 channel lists (1 or 2 phenotypes) but got {len(signature)}.'
Expand All @@ -142,7 +141,7 @@ def get_squidpy_metrics(
query = self._get_group_separator().join(groups).encode('utf-8')
self.tcp_client.sendall(query)
response = self._parse_response()
return SquidpyMetricsComputationResult(
return UnivariateMetricsComputationResult(
values=response['metrics'],
is_pending=response['pending'],
)
Expand Down
10 changes: 5 additions & 5 deletions test/apiserver/module_tests/test_proximity.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ function test_proximity() {
p2="$2"
r="$3"
filename="$4"
query="http://spt-apiserver-testing:8080/request-phenotype-proximity-computation/?study=Melanoma%20intralesional%20IL2&phenotype1=$p1&phenotype2=$p2&radius=$r"
query="http://spt-apiserver-testing:8080/request-spatial-metrics-computation-custom-phenotypes/?study=Melanoma%20intralesional%20IL2&feature_class=proximity&$p1&$p2&radius=$r"
start=$SECONDS
while (( SECONDS - start < 15 )); do
echo -en "Doing query $blue$query$reset_code ... "
Expand Down Expand Up @@ -53,7 +53,7 @@ function test_proximity() {
fi
}

test_proximity B2M CD4 60 module_tests/expected_proximity.json
test_proximity B7H3 2 60 module_tests/expected_proximity2.json
test_proximity CD8 MHCI 125 module_tests/expected_proximity3.json
test_proximity 3 PD1 125 module_tests/expected_proximity4.json
test_proximity "positive_marker=B2M&negative_marker=" "positive_marker2=CD4&negative_marker2=" 60 module_tests/expected_proximity.json
test_proximity "positive_marker=B7H3&negative_marker=" "positive_marker2=CD3&positive_marker2=CD4&negative_marker2=CD8&negative_marker2=FOXP3&negative_marker2=CD20&negative_marker2=CD56&negative_marker2=SOX10&" 60 module_tests/expected_proximity2.json
test_proximity "positive_marker=CD8&negative_marker=" "positive_marker2=MHCI&negative_marker2=" 125 module_tests/expected_proximity3.json
test_proximity "positive_marker=CD3&positive_marker=CD8&negative_marker=CD4&negative_marker=FOXP3&negative_marker=CD20&negative_marker=CD56&negative_marker=SOX10&" "positive_marker2=PD1&negative_marker2=" 125 module_tests/expected_proximity4.json
8 changes: 4 additions & 4 deletions test/apiserver/module_tests/test_squidpy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,27 @@ function test_squidpy() {
p1="$2"
p2="$3"
filename="$4"
query="http://spt-apiserver-testing:8080/request-squidpy-computation/?study=Melanoma%20intralesional%20IL2&phenotype=$p1&phenotype=$p2&feature_class=$feature_class"
query="http://spt-apiserver-testing:8080/request-spatial-metrics-computation/?study=Melanoma%20intralesional%20IL2&phenotype=$p1&phenotype=$p2&feature_class=$feature_class"
fi
if [[ "$feature_class" == "co-occurrence" ]];
then
p1="$2"
p2="$3"
r="$4"
filename="$5"
query="http://spt-apiserver-testing:8080/request-squidpy-computation/?study=Melanoma%20intralesional%20IL2&phenotype=$p1&phenotype=$p2&radius=$r&feature_class=$feature_class"
query="http://spt-apiserver-testing:8080/request-spatial-metrics-computation/?study=Melanoma%20intralesional%20IL2&phenotype=$p1&phenotype=$p2&radius=$r&feature_class=$feature_class"
fi
if [[ "$feature_class" == "ripley" ]];
then
p="$2"
filename="$3"
query="http://spt-apiserver-testing:8080/request-squidpy-computation/?study=Melanoma%20intralesional%20IL2&phenotype=$p&feature_class=$feature_class"
query="http://spt-apiserver-testing:8080/request-spatial-metrics-computation/?study=Melanoma%20intralesional%20IL2&phenotype=$p&feature_class=$feature_class"
fi
if [[ "$feature_class" == "spatial%20autocorrelation" ]];
then
p="$2"
filename="$3"
query="http://spt-apiserver-testing:8080/request-squidpy-computation/?study=Melanoma%20intralesional%20IL2&phenotype=$p&feature_class=$feature_class"
query="http://spt-apiserver-testing:8080/request-spatial-metrics-computation/?study=Melanoma%20intralesional%20IL2&phenotype=$p&feature_class=$feature_class"
fi
start=$SECONDS
while (( SECONDS - start < 30 )); do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ function test_squidpy_custom_phenotypes() {
phenotype_query="$part1"
fi

endpoint="request-squidpy-computation-custom-phenotype"
endpoint="request-spatial-metrics-computation-custom-phenotype"
if [[ "$feature_class" == "neighborhood%20enrichment" || "$feature_class" == "co-occurrence" ]];
then
endpoint="${endpoint}s"
Expand Down
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.15.1
0.15.2

0 comments on commit c1592d9

Please sign in to comment.