Skip to content

Commit eefa5e0

Browse files
committed
refactor: change how tags and default_grouping work
1 parent c3c3f3f commit eefa5e0

File tree

2 files changed

+75
-26
lines changed

2 files changed

+75
-26
lines changed

examples/quality_control.py

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@
4545
value=drift_value_with_options,
4646
reference="ecephys-drift-map",
4747
status_history=[sp],
48-
tags=["Drift map", "Probe A"],
48+
tags={
49+
"probe": "Probe A",
50+
"type": "drift map",
51+
}
4952
),
5053
QCMetric(
5154
name="Probe B drift",
@@ -55,7 +58,10 @@
5558
value=drift_value_with_flags,
5659
reference="ecephys-drift-map",
5760
status_history=[sp],
58-
tags=["Drift map", "Probe B"],
61+
tags={
62+
"probe": "Probe B",
63+
"type": "drift map",
64+
}
5965
),
6066
QCMetric(
6167
name="Probe C drift",
@@ -65,16 +71,10 @@
6571
value="Low",
6672
reference="ecephys-drift-map",
6773
status_history=[s],
68-
tags=["Drift map", "Probe C"],
69-
),
70-
QCMetric(
71-
name="Expected frame count",
72-
modality=Modality.BEHAVIOR_VIDEOS,
73-
stage=Stage.RAW,
74-
description="Expected frame count from experiment length, always pass",
75-
value=662,
76-
status_history=[s],
77-
tags=["Frame count checks"],
74+
tags={
75+
"probe": "Probe C",
76+
"type": "drift map",
77+
}
7878
),
7979
QCMetric(
8080
name="Video 1 frame count",
@@ -83,7 +83,10 @@
8383
description="Pass when frame count matches expected",
8484
value=662,
8585
status_history=[s],
86-
tags=["Frame count checks", "Video 1"],
86+
tags={
87+
"video": "Video 1",
88+
"type": "Frame count checks",
89+
},
8790
),
8891
QCMetric(
8992
name="Video 2 num frames",
@@ -92,7 +95,10 @@
9295
description="Pass when frame count matches expected",
9396
value=662,
9497
status_history=[s],
95-
tags=["Frame count checks", "Video 2"],
98+
tags={
99+
"video": "Video 2",
100+
"type": "Frame count checks",
101+
},
96102
),
97103
QCMetric(
98104
name="ProbeA",
@@ -101,7 +107,10 @@
101107
description="Pass when probe is present in the recording",
102108
value=True,
103109
status_history=[s],
104-
tags=["Probes present"],
110+
tags={
111+
"probe": "Probe A",
112+
"type": "Probes present",
113+
},
105114
),
106115
QCMetric(
107116
name="ProbeB",
@@ -110,7 +119,10 @@
110119
description="Pass when probe is present in the recording",
111120
value=True,
112121
status_history=[s],
113-
tags=["Probes present"],
122+
tags={
123+
"probe": "Probe B",
124+
"type": "Probes present",
125+
},
114126
),
115127
QCMetric(
116128
name="ProbeC",
@@ -119,14 +131,17 @@
119131
description="Pass when probe is present in the recording",
120132
value=True,
121133
status_history=[s],
122-
tags=["Probes present"],
134+
tags={
135+
"probe": "Probe C",
136+
"type": "Probes present",
137+
},
123138
),
124139
]
125140

126141
q = QualityControl(
127142
metrics=metrics,
128-
default_grouping=["Drift map", "Frame count checks", "Probes present"],
129-
allow_tag_failures=["Video 2"], # this will allow the Video 2 metric to fail without failing the entire QC
143+
default_grouping=[["probe", "video"], ["type"]], # in visualizations group probes together and videos together, then group metrics by type
144+
allow_tag_failures=["Video 2"], # allow any metrics with tag video: Video 2 to fail without failing overall QC
130145
)
131146

132147
if __name__ == "__main__":

src/aind_data_schema/core/quality_control.py

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from typing import Any, List, Literal, Optional, Union
66

77
from aind_data_schema_models.modalities import Modality
8-
from pydantic import Field, SkipValidation, model_validator
8+
from pydantic import Field, SkipValidation, field_validator, model_validator
99

1010
from aind_data_schema.base import AwareDatetimeWithDefault, DataCoreModel, DataModel, DiscriminatedList
1111
from aind_data_schema.utils.merge import merge_notes, merge_optional_list, remove_duplicates
@@ -49,8 +49,9 @@ class QCMetric(DataModel):
4949
status_history: List[QCStatus] = Field(default=[], title="Metric status history", min_length=1)
5050
description: Optional[str] = Field(default=None, title="Metric description")
5151
reference: Optional[str] = Field(default=None, title="Metric reference image URL or plot type")
52-
tags: List[str] = Field(
53-
default=[], title="Tags", description="Tags group QCMetric objects to allow for grouping and filtering"
52+
tags: dict[str, str] = Field(
53+
default={}, title="Tags",
54+
description="Tags group QCMetric objects. Unique keys define groups of tags, for example {'probe': 'probeA'}."
5455
)
5556
evaluated_assets: Optional[List[str]] = Field(
5657
default=None,
@@ -80,6 +81,28 @@ def validate_multi_asset(self):
8081
raise ValueError(f"Metric '{self.name}' is a single-asset metric and should not have evaluated_assets")
8182
return self
8283

84+
@model_validator(mode="before")
85+
@classmethod
86+
def fix_tag_lists(cls, self):
87+
"""Convert tags from list to dict if necessary
88+
89+
This function is for backwards compatibility with v2.2.X where tags were stored as lists of strings.
90+
91+
Remove this function in aind-data-schema v3.X
92+
"""
93+
tags = self["tags"]
94+
if isinstance(tags, list):
95+
# Convert list of strings to dict with string keys
96+
if len(tags) == 1:
97+
self["tags"] = {
98+
"tag": tags[0],
99+
"name": self["name"],
100+
}
101+
else:
102+
# Unfortunately there is no reasonable way to handle multiple tags, these assets should be re-generated
103+
self["tags"] = {f"tag_{i+1}": tag for i, tag in enumerate(tags)}
104+
return self
105+
83106

84107
class CurationHistory(DataModel):
85108
"""Schema to track curator name and timestamp for curation events"""
@@ -110,15 +133,15 @@ class QualityControl(DataCoreModel):
110133
)
111134
notes: Optional[str] = Field(default=None, title="Notes")
112135

113-
default_grouping: List[str] = Field(
136+
default_grouping: List[list[str]] = Field(
114137
...,
115138
title="Default grouping",
116-
description="Default tag grouping for this QualityControl object, used in visualizations",
139+
description="Tag *keys* that should be used to group metrics hierarchically for visualization",
117140
)
118-
allow_tag_failures: List[str | tuple] = Field(
141+
allow_tag_failures: List[str] = Field(
119142
default=[],
120143
title="Allow tag failures",
121-
description="List of tags that are allowed to fail without failing the overall QC",
144+
description="List of tag *values* that are allowed to fail without failing the overall QC",
122145
)
123146
status: Optional[dict] = Field(
124147
default=None,
@@ -257,6 +280,17 @@ def __add__(self, other: "QualityControl") -> "QualityControl":
257280
allow_tag_failures=combined_allow_tag_failures,
258281
)
259282

283+
@field_validator("default_grouping", mode="before")
284+
def fix_default_grouping_list(cls, value: dict) -> dict:
285+
"""Convert default grouping from list of strings to list of list of strings if necessary
286+
This function is for backwards compatibility with v2.2.X where default_grouping was stored as a list of strings.
287+
Remove this function in aind-data-schema v3.X
288+
"""
289+
if value and len(value) > 0 and isinstance(value[0], str):
290+
# Convert list of strings to list of list of strings
291+
value = [[tag] for tag in value]
292+
return value
293+
260294

261295
def _get_status_by_date(metric: QCMetric | CurationMetric, date: datetime) -> Status:
262296
"""Get the status of a metric at a specific date by looking through status_history.

0 commit comments

Comments
 (0)