Coverage for src/backoffice/compatibility.py: 0%
101 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-11-12 10:26 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-11-12 10:26 +0000
1"""data models for compatibility reports"""
3import warnings
4from typing import Any, List, Literal, Mapping, Optional, Sequence, Union
6from annotated_types import Interval
7from packaging.version import Version
9try:
10 from pydantic import BaseModel, Field, HttpUrl, computed_field, model_validator
11except ImportError as e:
12 raise ImportError(
13 "pydantic is required for backoffice.compatibility. "
14 "Please install `backoffice[dev]` or use backoffice.compatibility_pure instead."
15 ) from e
17from typing_extensions import Annotated
19PartnerToolName = Literal[
20 "ilastik",
21 "deepimagej",
22 "icy",
23 "biapy",
24 "careamics",
25]
26ToolName = Literal["bioimageio.core", PartnerToolName]
28PARTNER_TOOL_NAMES = (
29 "ilastik",
30 "deepimagej",
31 "icy",
32 "biapy",
33 "careamics",
34)
35TOOL_NAMES = ("bioimageio.core", *PARTNER_TOOL_NAMES)
37ToolNameVersioned = str
40class Node(BaseModel):
41 """Base data model with common config"""
43 pass
46class Badge(Node):
47 icon: HttpUrl
48 label: str
49 url: HttpUrl
52class ToolReportDetails(Node, extra="allow"):
53 traceback: Optional[Sequence[str]] = None
54 warnings: Optional[Mapping[str, Any]] = None
55 metadata_completeness: Optional[float] = None
56 status: Union[Literal["passed", "valid-format", "failed"], Any] = None
59class ToolCompatibilityReport(Node, extra="allow"):
60 """Used to report on the compatibility of resource description
61 in the bioimageio collection for a version specific tool.
62 """
64 tool: Annotated[ToolName, Field(exclude=True, pattern=r"^[a-zA-Z0-9-\.]+$")]
65 """tool name"""
67 tool_version: Annotated[str, Field(exclude=True, pattern=r"^[a-z0-9\.-]+$")]
68 """tool version, ideally in SemVer 2.0 format"""
70 @property
71 def report_name(self) -> str:
72 return f"{self.tool}_{self.tool_version}"
74 status: Literal["passed", "failed", "not-applicable"]
75 """status of this tool for this resource"""
77 score: Annotated[float, Interval(ge=0, le=1.0)]
78 """score for the compatibility of this tool with the resource"""
80 @model_validator(mode="before")
81 @classmethod
82 def _set_default_score(cls, values: dict[str, Any]) -> dict[str, Any]:
83 if isinstance(values, dict) and "score" not in values:
84 values["score"] = 1.0 if values.get("status") == "passed" else 0.0
86 return values
88 error: Optional[str]
89 """error message if `status`=='failed'"""
91 details: Union[ToolReportDetails, str, List[str], None] = None
92 """details to explain the `status`"""
94 badge: Optional[Badge] = None
95 """status badge with a resource specific link to the tool"""
97 links: Sequence[str] = ()
98 """the checked resource should link these other bioimage.io resources"""
101class CompatibilityScores(Node):
102 tool_compatibility_version_specific: Mapping[
103 ToolNameVersioned, Annotated[float, Interval(ge=0, le=1.0)]
104 ]
106 metadata_completeness: Annotated[float, Interval(ge=0, le=1.0)] = 0.0
107 """Score for metadata completeness.
109 A measure of how many optional fields in the resource RDF are filled out.
110 """
112 metadata_format: Annotated[float, Interval(ge=0, le=1.0)] = 0.0
113 """Score for metadata formatting.
115 - 1.0: resource RDF conforms to the latest spec version
116 - 0.5: resource RDF conforms to an older spec version
117 - 0.0: resource RDF does not conform to any known spec version
118"""
120 @computed_field
121 @property
122 def core_compatibility(self) -> float:
123 return self.tool_compatibility.get("bioimageio.core", 0.0)
125 @computed_field
126 @property
127 def tool_compatibility(
128 self,
129 ) -> Mapping[ToolName, Annotated[float, Interval(ge=0, le=1.0)]]:
130 """Aggregated tool compatibility score"""
131 grouped: dict[ToolName, dict[Version, float]] = {}
132 for tool, value in self.tool_compatibility_version_specific.items():
133 assert value <= 1.0, f"Tool {tool} has a compatibility score > 1.0: {value}"
134 tool_name, tool_version = tool.split("_", 1)
135 if tool_name not in TOOL_NAMES:
136 warnings.warn(f"Tool {tool_name} is not a valid ToolName")
137 continue
139 malus = 0.0
140 try:
141 version = Version(tool_version)
142 except Exception:
143 version = Version("0.0.0")
144 malus += 0.1 # penalize non-semver versions
146 grouped.setdefault(tool_name, {})[version] = value - malus
148 for tool in list(grouped):
149 if not grouped[tool]:
150 del grouped[tool]
152 agglomerated: dict[ToolName, float] = {}
153 for tool, version_scores in grouped.items():
154 latest_version = max(version_scores.keys())
156 if version_scores[latest_version] >= 0.8:
157 # if the latest version is compatible use it as the score
158 score = version_scores[latest_version]
159 else:
160 # average the top 4 scores to score max 0.8
161 # as penalty if the last_version isn't fully compatible
162 top4 = sorted(version_scores.values(), reverse=True)[:4]
163 score = min(0.8, sum(top4) / len(top4))
165 agglomerated[tool] = score
167 return agglomerated
169 @computed_field
170 @property
171 def overall_partner_tool_compatibility(
172 self,
173 ) -> Annotated[float, Interval(ge=0, le=1.0)]:
174 """Overall partner tool compatibility score.
175 Note:
176 - Currently implemented as: Average of the top 3 partner tool compatibility scores.
177 - Implementation is subject to change in the future.
178 """
179 top3 = sorted(
180 [v for k, v in self.tool_compatibility.items() if k in PARTNER_TOOL_NAMES],
181 reverse=True,
182 )[:3]
183 if not top3:
184 return 0.0
185 else:
186 return sum(top3) / 3
188 @computed_field
189 @property
190 def overall_compatibility(self) -> Annotated[float, Interval(ge=0, le=1.0)]:
191 """Weighted, overall score between 0 and 1.
192 Note: The scoring scheme is subject to change in the future.
193 """
194 return (
195 0.25 * self.metadata_format
196 + 0.25 * self.metadata_completeness
197 + 0.25 * self.core_compatibility
198 + 0.25 * self.overall_partner_tool_compatibility
199 )
202class InitialSummary(Node):
203 rdf_content: dict[str, Any]
204 """The RDF content of the original rdf.yaml file."""
206 rdf_yaml_sha256: str
207 """SHA-256 of the original RDF YAML file."""
209 status: Literal["passed", "failed", "untested"]
210 """status of the bioimageio.core reproducibility tests."""
213class CompatibilitySummary(InitialSummary):
214 scores: CompatibilityScores
215 """Scores for compatibility with the bioimage.io community tools."""
217 tests: Mapping[ToolName, Mapping[str, ToolCompatibilityReport]]
218 """Compatibility reports for each tool for each version."""