Metadata-Version: 2.5
Name: ontometer
Version: 0.1.2
Summary: Ontology quality assessment
Project-URL: Changelog, https://github.com/growgraph/OntoMeter/blob/main/CHANGELOG.md
Project-URL: Homepage, https://github.com/growgraph/OntoMeter
Project-URL: Issues, https://github.com/growgraph/OntoMeter/issues
Project-URL: Repository, https://github.com/growgraph/OntoMeter
Author-email: Alexander Belikov <alexander@growgraph.dev>, David Bareham <d.bareham@liverpool.ac.uk>
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Keywords: ontology,owl,quality,rdf,semantic-web
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.11
Requires-Dist: click<9,>=8.1
Requires-Dist: langchain-core<2,>=0.3
Requires-Dist: langchain-ollama<2,>=0.2
Requires-Dist: langchain-openai<2,>=0.2
Requires-Dist: owlready2<1,>=0.47
Requires-Dist: pydantic-settings<3,>=2.5
Requires-Dist: pydantic<3,>=2.9
Requires-Dist: pyyaml<7,>=6.0
Requires-Dist: rdflib<9,>=7.0
Provides-Extra: corpus
Requires-Dist: beautifulsoup4<5,>=4.12; extra == 'corpus'
Requires-Dist: networkx<4,>=3.2; extra == 'corpus'
Requires-Dist: pandas<3,>=2.2; extra == 'corpus'
Requires-Dist: pyarrow>=17.0; extra == 'corpus'
Requires-Dist: pyoxigraph<0.6,>=0.5.11; extra == 'corpus'
Requires-Dist: requests<3,>=2.32; extra == 'corpus'
Requires-Dist: scipy<2,>=1.11; extra == 'corpus'
Requires-Dist: suthing<0.7,>=0.6.0; extra == 'corpus'
Provides-Extra: dev
Requires-Dist: pre-commit>=4.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.14; extra == 'dev'
Requires-Dist: ty>=0.0.14; extra == 'dev'
Provides-Extra: docs
Requires-Dist: mkdocs-gen-files<1,>=0.5; extra == 'docs'
Requires-Dist: mkdocs-glightbox<1,>=0.4; extra == 'docs'
Requires-Dist: mkdocs-literate-nav<1,>=0.6; extra == 'docs'
Requires-Dist: mkdocs-material<10,>=9.6; extra == 'docs'
Requires-Dist: mkdocstrings[python]<1,>=0.29; extra == 'docs'
Requires-Dist: properdocs<2,>=1.6.7; extra == 'docs'
Provides-Extra: research
Requires-Dist: beautifulsoup4<5,>=4.12; extra == 'research'
Requires-Dist: diptest<1,>=0.10; extra == 'research'
Requires-Dist: hdbscan<1,>=0.8.40; extra == 'research'
Requires-Dist: ipykernel>=6.29; extra == 'research'
Requires-Dist: matplotlib<4,>=3.9; extra == 'research'
Requires-Dist: nbclient>=0.10; extra == 'research'
Requires-Dist: nbformat>=5.10; extra == 'research'
Requires-Dist: networkx<4,>=3.2; extra == 'research'
Requires-Dist: pandas<3,>=2.2; extra == 'research'
Requires-Dist: powerlaw>=1.5; extra == 'research'
Requires-Dist: pyarrow>=17.0; extra == 'research'
Requires-Dist: pyoxigraph<0.6,>=0.5.11; extra == 'research'
Requires-Dist: requests<3,>=2.32; extra == 'research'
Requires-Dist: scikit-learn<2,>=1.9; extra == 'research'
Requires-Dist: scipy<2,>=1.11; extra == 'research'
Requires-Dist: seaborn<1,>=0.13; extra == 'research'
Requires-Dist: suthing<0.7,>=0.6.0; extra == 'research'
Requires-Dist: umap-learn<1,>=0.5.9; extra == 'research'
Provides-Extra: store
Requires-Dist: beautifulsoup4<5,>=4.12; extra == 'store'
Requires-Dist: huggingface-hub<3,>=2.1; extra == 'store'
Requires-Dist: networkx<4,>=3.2; extra == 'store'
Requires-Dist: pandas<3,>=2.2; extra == 'store'
Requires-Dist: pyarrow>=17.0; extra == 'store'
Requires-Dist: pyoxigraph<0.6,>=0.5.11; extra == 'store'
Requires-Dist: requests<3,>=2.32; extra == 'store'
Requires-Dist: scipy<2,>=1.11; extra == 'store'
Requires-Dist: suthing<0.7,>=0.6.0; extra == 'store'
Requires-Dist: zstandard<1,>=0.25; extra == 'store'
Description-Content-Type: text/markdown

# OntoMeter

![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)
[![PyPI version](https://badge.fury.io/py/ontometer.svg)](https://badge.fury.io/py/ontometer)
[![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-green)](https://github.com/growgraph/OntoMeter/blob/main/LICENSE)
[![pre-commit](https://github.com/growgraph/OntoMeter/actions/workflows/pre-commit.yml/badge.svg)](https://github.com/growgraph/OntoMeter/actions/workflows/pre-commit.yml)

Documentation: <https://growgraph.github.io/OntoMeter>

**Ontology quality assessment.** Point it at an OWL/RDF/Turtle file and it computes the
sixteen OQuaRE metrics, scales each to 1–5, rolls them up into quality characteristics,
and — where a language model is configured — adds a review of the things structural metrics
cannot see.

```bash
pip install ontometer
ontometer eval my-ontology.ttl
```

<a href="https://growgraph.github.io/OntoMeter/corpus/growth/"><img src="docs/assets/abstractness-race.gif" alt="The abstractness race: ontologies drawn in a Poincaré disk, moving toward the centre as newer ontologies build on them" width="540"></a>

*The abstractness race, from the [corpus half](#6-the-corpus-half): every newer ontology that
builds on another pulls it toward the centre.* [Explore it](https://growgraph.github.io/OntoMeter/corpus/growth/).

---

## 1. What you get

```
$ ontometer eval test/fixtures/small.ttl --no-review

# Ontology evaluation — `test/fixtures/small.ttl`

**Overall 3.98/5** across 7 characteristics, aggregated by `mean`.

## Characteristics

| Characteristic      | Score |         |
|---------------------|-------|---------|
| Functional adequacy | 2.15  | `██···` |
| Structural          | 4.00  | `████·` |
| Maintainability     | 4.78  | `█████` |

## Metrics

| Metric    | Name               | Value | Score | Formula                                       |
|-----------|--------------------|-------|-------|-----------------------------------------------|
| `AROnto`  | Attribute richness | 0.143 | 1/5   | datatype-property domain axioms / (\|C\| + 1) |
| `CROnto`  | Class population   | 0.143 | 1/5   | \|individuals\| / (\|C\| + 1)                 |
| `DITOnto` | Depth of hierarchy | 4.000 | 4/5   | classes on the longest leaf-to-owl:Thing path |

## Weakest metrics, and what drives them

**CROnto** (Class population) — 1/5
  - `http://example.org/small#Animal`
  - `http://example.org/small#Mammal`
  - `http://example.org/small#Orphan`
```

*Abridged.*

Every metric reports **the formula it actually computed**. That is not decoration: the
OQuaRE implementations in circulation disagree with each other on most metrics, and with the
published descriptions. OntoMeter follows the authors' reference implementation, and each
metric states its formula, the source it follows, and how it departs from it
(`ontometer model --what metrics`), so a diverging number can be traced to a definition.

## 2. The review

Structural metrics cannot tell you whether an ontology models its domain correctly, whether
its terms mean what their names suggest, or whether it is internally consistent. OQuaRE
knows this — a large minority of its sub-characteristics have **no supporting metric at
all**:

```bash
ontometer model --what unsupported
```

That gap is what the review is for. It receives the weakest metrics and the specific
entities driving them, and returns findings ranked by what they actually break:

- **`defect`** — changes what a reasoner concludes. A property declared
  `rdfs:subPropertyOf` a *datatype*; two `rdfs:domain` triples on one property, which OWL
  reads as conjunction so every subject is inferred into both classes at once; a cycle in
  the subsumption hierarchy.
- **`weakness`** — sound but will cause trouble. Undeclared domains, absent inverse
  properties on a graph built for navigation.
- **`suggestion`** — style and documentation.

The model never produces a number. Metrics are computed; the review explains and
recommends.

```bash
export LLM_PROVIDER=ollama
export LLM_MODEL_NAME=llama3.1
ontometer eval my-ontology.ttl
```

Configuration is read from the process environment:

| Variable | Meaning | Default |
|---|---|---|
| `LLM_PROVIDER` | `openai` or `ollama` | `openai` |
| `LLM_MODEL_NAME` | Model identifier | `gpt-4o-mini` |
| `LLM_BASE_URL` | Endpoint override; any OpenAI-compatible server works | provider default |
| `LLM_API_KEY` | Required for `openai`, unused for `ollama` | — |
| `LLM_TEMPERATURE` | Sampling temperature | `0.0` |
| `LLM_PARSE_RETRIES` | Retries when the reply does not validate | `2` |

Metrics compute with or without a provider. With none configured the report still renders
and says why the review is missing, because a quality checker that fails closed on an
absent API key is useless in the CI pipelines most of these will run in.

## 3. Using it in CI

```bash
ontometer eval ontology.ttl --no-review --format json -o report.json
ontometer eval ontology.ttl --no-review --fail-under 3.5
```

`--fail-under` exits non-zero when the overall score drops below a threshold. Treat the
overall number as a tripwire for regressions, not as a verdict — OQuaRE defines no overall
score, and this one is an unweighted mean of characteristic scores.

## 4. The quality model is configuration

Thresholds and the characteristic → sub-characteristic → metric map ship as YAML read at
runtime, not compiled in. OQuaRE's thresholds were never published in an archival form; the
ones shipped are taken from the authors' reference implementation, each cited in
`scales.yaml`, and are editable rather than authoritative.

```bash
ontometer model --what metrics          # every metric and its formula
ontometer model --what scales           # threshold families and cut points
ontometer model --what characteristics  # the full rollup map
ontometer model --what unsupported      # what no metric covers
```

Which metrics support which sub-characteristic, and how they combine, follow the authors'
reference implementation: an unweighted mean at both levels, with a few metrics entering
inverted (`-NOCOnto` in `quality_model.yaml` means `6 - score`). Both are declared in the YAML
rather than hidden in code.

## 5. As a library

```python
from ontometer import evaluate

result = evaluate("my-ontology.ttl", with_review=False)

result.assessment.metrics["DITOnto"].score  # 5
result.assessment.characteristics["structural"]  # 4.12
result.assessment.worst_metrics(3)  # the three weakest
result.assessment.seeds["ANOnto"]  # undocumented classes
result.view.external_namespaces  # vocabulary borrowed, not declared
```

## 6. The corpus half

OntoMeter is also a research project on ontologies as a **population**: how they depend on
each other, how abstract their terms are, and how the network of ontologies grows. That half
lives in `ontometer.corpus` and needs a heavier dependency set, so it sits behind an extra:

```bash
pip install "ontometer[corpus]"
```

It fetches from ten registries (six OntoPortal Alliance instances, OBO Foundry, Ontohub,
OKG, LOV), parses each file for its structural and metadata footprint, deduplicates across
sources and versions, and assembles a dependency network distinguishing **declared** reuse
(`owl:imports`) from **actual** reuse: terms used from a namespace that was never imported.
On top of that network it measures the abstractness of terms and ontologies, and rebuilds the
specialisation network of ontologies over time.

- `ontometer/corpus/README.md`: the pipeline end to end, and reproducing it from a clean checkout
- `ontometer/corpus/pipeline/README.md`: fetch, then seed → parse → dedup
- `ontometer/corpus/network/README.md`: the node and edge model
- [Abstractness](https://growgraph.github.io/OntoMeter/corpus/abstractness/) and the
  [abstractness race](https://growgraph.github.io/OntoMeter/corpus/growth/) (the G(t) explorer) in the documentation

```bash
export ONTOMETER_STORE="$HOME/ontometer-store"    # raw files and structural sidecars
uv run python run/fetch_ontologies.py --sources lov --ontoportal BioPortal
uv run python run/process.py --phases seed,parse,dedup
uv run python -m ontometer.corpus.network.construction
```

Corpus artifacts live in their own git repository,
[`growgraph/ontometer-corpus`](https://github.com/growgraph/ontometer-corpus); pass its `data/` directory as
`--corpus` (or `ONTOMETER_CORPUS`). They are versioned because they cannot be
re-fetched into existence: registries change, and a crawl run today produces a different
corpus rather than this one. They are kept compact by projecting
registry metadata to a field allowlist before storage. The raw object store of ontology files
is *not* tracked. See `ontometer/corpus/README.md` for what reproducing the corpus does and
does not mean.

## 7. Prior art

OntoMeter is not the first tool to compute OQuaRE. [OntoInsight][oi] pairs the metrics with
LLM recommendations over a Java OWL API engine; [`oquare-metrics`][om] wraps the same engine
as a CI action; NEOntometrics computes OQuaRE and OntoQA at scale with version tracking.

What is different here: pure Python, so there is no JVM to install; external-namespace
coupling, which OQuaRE does not model at all; and the population dimension — every tool in
that list is strictly single-ontology, so none of them can tell you whether a score is
*unusual*.

[oi]: https://github.com/kracr/onto-insight
[om]: https://github.com/tecnomod-um/oquare-metrics

## 8. Development

```bash
uv sync --extra dev --extra store  # name every extra you need in one sync
uv run pytest test
uv run ruff check && uv run ruff format
uv run pre-commit install
uv sync --extra docs --extra corpus && uv run properdocs serve  # the documentation site
```

CI on every pull request and every push to `main` runs pre-commit (ruff and
`ty`) and `pytest` with the `dev` and `corpus` extras. Publishing to PyPI is a
GitHub Release whose tag is `v` followed by `version` in `pyproject.toml`.

## License

Apache-2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE).
