Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ Build the same RAG-based alignment yourself for full control over every stage:
```python
from ontoaligner.ontology import GenericOMDataset
from ontoaligner.utils import metrics, xmlify
from ontoaligner.utils.sssom import write_sssom
from ontoaligner.aligner import MistralLLMBERTRetrieverRAG
from ontoaligner.encoder import ConceptParentRAGEncoder
from ontoaligner.postprocess import rag_hybrid_postprocessor
Expand Down Expand Up @@ -129,6 +130,9 @@ print("Hybrid Matching Evaluation Report:", evaluation)
# Step 7: Convert matchings to XML format and save the XML representation
xml_str = xmlify.xml_alignment_generator(matchings=hybrid_matchings)
open("matchings.xml", "w", encoding="utf-8").write(xml_str)

# Step 8: implicitly convert to SSSOM and write to disk
write_sssom(hybrid_matchings, "matchings.sssom.tsv")
```

### Advanced `AlignerPipeline`
Expand Down
Empty file added ontoaligner/utils/__init__.py
Empty file.
94 changes: 94 additions & 0 deletions ontoaligner/utils/sssom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""Utilities for writing SSSOM."""

from __future__ import annotations

import datetime
from pathlib import Path
from typing import Any, TextIO

import bioregistry
import curies
import sssom_pydantic
from curies import vocabulary as v
from sssom_pydantic import MappingSet, SemanticMapping

__all__ = [
"to_semantic_mappings",
"write_sssom",
]


def write_sssom(
matchings: list[dict],
path: str | Path | TextIO,
*,
converter: curies.Converter | None = None,
metadata: MappingSet | None = None,
**kwargs: Any,
) -> None:
"""Convert matchings to SSSOM objects and write a TSV to disk.

:param matchings: OntoAligner matchings
:param path: The path or file to write to.
:param converter:
:param metadata:
:param kwargs:
:return:
"""
if converter is None:
converter = bioregistry.get_default_converter()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not clear if this package produces a coherent prefix map somewhere else, so Bioregistry is a good place to get a comprehensive one if you aren't sure what you'll get

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will take care of this after a couple of tests. Right now, using Bioregistry adds another package to the setup list, which can sometimes cause an issue if it is used only once.

mappings = to_semantic_mappings(matchings, converter)
sssom_pydantic.write(
mappings, path, converter=converter, metadata=metadata, **kwargs
)


LOOKUPS = {
"=": v.exact_match,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: what else can appear as a relation besides =?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, I would keep it as it then we can fix this later.

}


def to_semantic_mappings(
matchings: list[dict],
converter: curies.Converter | None = None,
) -> list[SemanticMapping]:
if converter is None:
converter = bioregistry.get_default_converter()
mappings = []
for matching in matchings:
try:
mapping = _to_semantic_mapping(matching, converter)
except Exception: # noqa: BLE001
continue
else:
if mapping is not None:
mappings.append(mapping)
return mappings


def _to_semantic_mapping(
matching: dict[str, Any], converter: curies.Converter
) -> SemanticMapping | None:
sub = converter.parse_uri(matching["source"])
obj = converter.parse_uri(matching["target"])
if sub is None or obj is None:
return None

if confidence := matching.get("score"):
confidence = float(confidence)
if confidence < 0 or confidence > 1:
confidence = None

return SemanticMapping(
subject=sub,
predicate=LOOKUPS[matching.get("relation", "=")],
object=obj,
# depending on the aligner, this might be one of several
# other things from the SEMAPV namespace
justification=v.unspecified_matching_process,
confidence=confidence,
mapping_date=datetime.date.today(), # noqa: DTZ011
# TODO in follow-up, add additional fields such as:
# mapping tool, license, etc. from
# https://mapping-commons.github.io/sssom/dev/
)
3 changes: 3 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
"scipy==1.15.3",
"multiprocess",
"cohere",
"sssom-pydantic",
"bioregistry",
"curies",
],
classifiers=[
"Development Status :: 5 - Production/Stable",
Expand Down
57 changes: 57 additions & 0 deletions tests/data/ensemble_alignments.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
[
{
"source": "https://kos.lifewatch.eu/thesauri/fishtraits/c_10",
"target": "https://kos.lifewatch.eu/thesauri/zooplanktontraits/c_13",
"score": 5.0
},
{
"source": "https://kos.lifewatch.eu/thesauri/fishtraits/c_4",
"target": "https://kos.lifewatch.eu/thesauri/zooplanktontraits/c_1",
"score": 5.0
},
{
"source": "https://kos.lifewatch.eu/thesauri/fishtraits/c_44",
"target": "https://kos.lifewatch.eu/thesauri/zooplanktontraits/c_15",
"score": 5.0
},
{
"source": "https://kos.lifewatch.eu/thesauri/fishtraits/c_53",
"target": "https://kos.lifewatch.eu/thesauri/zooplanktontraits/c_16",
"score": 5.0
},
{
"source": "https://kos.lifewatch.eu/thesauri/fishtraits/c_6",
"target": "https://kos.lifewatch.eu/thesauri/zooplanktontraits/c_3",
"score": 5.0
},
{
"source": "https://kos.lifewatch.eu/thesauri/fishtraits/c_7",
"target": "https://kos.lifewatch.eu/thesauri/zooplanktontraits/c_4",
"score": 5.0
},
{
"source": "https://kos.lifewatch.eu/thesauri/fishtraits/c_82",
"target": "https://kos.lifewatch.eu/thesauri/zooplanktontraits/c_86",
"score": 5.0
},
{
"source": "https://kos.lifewatch.eu/thesauri/fishtraits/c_9",
"target": "https://kos.lifewatch.eu/thesauri/zooplanktontraits/c_24",
"score": 5.0
},
{
"source": "https://kos.lifewatch.eu/thesauri/fishtraits/c_49",
"target": "https://kos.lifewatch.eu/thesauri/zooplanktontraits/c_20",
"score": 4.0
},
{
"source": "https://kos.lifewatch.eu/thesauri/fishtraits/c_51",
"target": "https://kos.lifewatch.eu/thesauri/zooplanktontraits/c_19",
"score": 3.0
},
{
"source": "https://kos.lifewatch.eu/thesauri/fishtraits/c_52",
"target": "https://kos.lifewatch.eu/thesauri/zooplanktontraits/c_18",
"score": 3.0
}
]
35 changes: 35 additions & 0 deletions tests/test_sssom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Tests for SSSOM export."""

import unittest
from pathlib import Path
import json
from ontoaligner.utils.sssom import to_semantic_mappings
from curies import Converter, NamableReference
from sssom_pydantic import SemanticMapping
from sssom_pydantic.testing import assert_semantic_mapping_equal
import datetime

HERE = Path(__file__).parent.resolve()
TEST_DATA = HERE.parent.joinpath("data", "ensemble_alignments.json")


class TestSSSOM(unittest.TestCase):
"""A test case for SSSOM export."""

def test_sssom(self) -> None:
today = datetime.date.today()
with TEST_DATA.open() as file:
matchings = json.load(file)
converter = Converter.from_prefix_map(
{
"fishtraits": "https://kos.lifewatch.eu/thesauri/fishtraits/",
"zooplanktontraits": "https://kos.lifewatch.eu/thesauri/zooplanktontraits/",
}
)
expected = SemanticMapping.exact(
"fishtraits:c_10",
"zooplanktontraits:c_13",
mapping_date=today,
)
actual = to_semantic_mappings(matchings, converter=converter)
assert_semantic_mapping_equal(self, expected, actual[0])