Skip to content

Commit e429137

Browse files
committed
Overhaul the alignment checking command.
Rename the `odk:validate` command to `odk:check-align`. "Validate" is a name that is too generic, a name that better reflects what the command actually does is preferrable. Make it possible to check the alignment against arbitrary root terms, instead of only the root terms of a upper ontology.
1 parent a0dfd48 commit e429137

File tree

9 files changed

+387
-224
lines changed

9 files changed

+387
-224
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ Available commands
1212
The following commands are provided by the plugin:
1313

1414
* `odk:subset`: to create ontology subsets;
15-
* `odk:validate`: to validate an ontology against an upper-level
16-
ontology;
15+
* `odk:check-align`: to check the alignment of an ontology against an
16+
upper-level ontology and/or arbitrary root terms;
1717
* `odk:normalize`: to “normalise” an ontology.
1818

1919
Building
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
/*
2+
* ODK ROBOT Plugin
3+
* Copyright © 2025 Damien Goutte-Gattat
4+
*
5+
* This program is free software; you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
package org.incenp.obofoundry.odk;
20+
21+
import java.io.BufferedWriter;
22+
import java.io.FileWriter;
23+
import java.io.IOException;
24+
import java.util.ArrayList;
25+
import java.util.HashSet;
26+
import java.util.List;
27+
import java.util.Set;
28+
29+
import org.apache.commons.cli.CommandLine;
30+
import org.obolibrary.robot.CommandLineHelper;
31+
import org.obolibrary.robot.CommandState;
32+
import org.obolibrary.robot.MergeOperation;
33+
import org.semanticweb.owlapi.model.IRI;
34+
import org.semanticweb.owlapi.model.OWLAnnotation;
35+
import org.semanticweb.owlapi.model.OWLAnnotationValue;
36+
import org.semanticweb.owlapi.model.OWLClass;
37+
import org.semanticweb.owlapi.model.OWLDataFactory;
38+
import org.semanticweb.owlapi.model.OWLOntology;
39+
import org.semanticweb.owlapi.model.parameters.Imports;
40+
import org.semanticweb.owlapi.reasoner.OWLReasoner;
41+
import org.slf4j.Logger;
42+
import org.slf4j.LoggerFactory;
43+
44+
/**
45+
* A command to check the alignment of an ontology against another, upper-level
46+
* ontology. The ontology is said to be “aligned” if all its classes are
47+
* subclasses of one of the upper ontology’s classes.
48+
* <p>
49+
* The command can also check “self-alignment” (option
50+
* <code>--use-self true</code>): the ontology is “self-aligned” if all its
51+
* classes are subclasses of one of the self-declared “preferred roots”
52+
* (declared within the ontology itself with the <code>IAO:0000700</code>
53+
* annotation).
54+
* <p>
55+
* Lastly, the command can check alignment against arbitrary root terms which
56+
* can be specified with the <code>--term</code> or <code>--term-file</code>
57+
* options.
58+
*/
59+
public class CheckAlignmentCommand extends BasePlugin {
60+
61+
private static final Logger logger = LoggerFactory.getLogger(CheckAlignmentCommand.class);
62+
63+
private Set<String> basePrefixes = new HashSet<>();
64+
private OWLDataFactory factory;
65+
66+
public CheckAlignmentCommand() {
67+
super("check-align", "validate alignment with an upper ontology or with explicit roots",
68+
"robot validate [--upper-ontology[-iri] ONT] [--report-output FILE]");
69+
70+
options.addOption("u", "upper-ontology", true, "load the upper ontology from the specified file");
71+
options.addOption("U", "upper-ontology-iri", true, "load the upper ontology from the specified IRI");
72+
options.addOption("C", "use-cob", true, "use COB as the upper ontology");
73+
74+
options.addOption("t", "term", true, "check alignment against specified term");
75+
options.addOption("T", "term-file", true, "check alignment against specified list of terms");
76+
77+
options.addOption("S", "use-self", true, "check self-alignment against declared roots");
78+
79+
options.addOption("b", "base-iri", true, "only check classes in the specified namespace(s)");
80+
options.addOption("d", "ignore-dangling", true, "if true, ignore dangling classes");
81+
82+
options.addOption("r", "reasoner", true, "the reasoner to use");
83+
options.addOption("O", "report-output", true, "write report to the specified file");
84+
options.addOption("x", "fail", true, "if true (default), fail if the ontology is misaligned");
85+
}
86+
87+
@Override
88+
public void performOperation(CommandState state, CommandLine line) throws Exception {
89+
boolean ignoreDangling = CommandLineHelper.getBooleanValue(line, "ignore-dangling", false);
90+
boolean failOnError = CommandLineHelper.getBooleanValue(line, "fail", true);
91+
if ( line.hasOption("base-iri") ) {
92+
for ( String iri : line.getOptionValues("base-iri") ) {
93+
basePrefixes.add(iri);
94+
}
95+
}
96+
factory = state.getOntology().getOWLOntologyManager().getOWLDataFactory();
97+
98+
// The classes we need to check alignment against
99+
Set<OWLClass> upperClasses = new HashSet<>();
100+
for ( IRI explicitRoot : CommandLineHelper.getTerms(ioHelper, line, true) ) {
101+
upperClasses.add(factory.getOWLClass(explicitRoot));
102+
}
103+
if ( CommandLineHelper.getBooleanValue(line, "use-self", false) ) {
104+
upperClasses.addAll(getPreferredRoots(state.getOntology()));
105+
}
106+
107+
OWLOntology ontology = getUpperOntology(line);
108+
if ( ontology != null ) {
109+
// All classes from the upper ontology are treated as valid roots
110+
upperClasses.addAll(ontology.getClassesInSignature(Imports.INCLUDED));
111+
upperClasses.remove(factory.getOWLThing());
112+
113+
// We merge the current ontology into the upper ontology rather than the other
114+
// way around, so that the current ontology remains unchanged and can be used
115+
// for further operations downstream in the ROBOT pipeline.
116+
MergeOperation.mergeInto(state.getOntology(), ontology, true, true);
117+
} else {
118+
// No upper ontology, validate against explicit and declared roots only
119+
ontology = state.getOntology();
120+
}
121+
122+
if ( upperClasses.isEmpty() ) {
123+
logger.error("No roots to validate against");
124+
return;
125+
}
126+
if ( logger.isDebugEnabled() ) {
127+
for ( OWLClass klass : upperClasses ) {
128+
logger.debug("Using root class {}", klass.getIRI().toQuotedString());
129+
}
130+
}
131+
132+
OWLReasoner reasoner = CommandLineHelper.getReasonerFactory(line).createReasoner(ontology);
133+
Set<OWLClass> unalignedClasses = getUnalignedClasses(ontology, reasoner, upperClasses, ignoreDangling);
134+
135+
if ( line.hasOption("report-output") ) {
136+
// If a report has been requested, we always produce it, even if no unaligned
137+
// classes were found
138+
BufferedWriter writer = new BufferedWriter(new FileWriter(line.getOptionValue("report-output")));
139+
List<String> unalignedIRIs = new ArrayList<>();
140+
for ( OWLClass unalignedClass : unalignedClasses ) {
141+
unalignedIRIs.add(unalignedClass.getIRI().toString());
142+
}
143+
unalignedIRIs.sort((a, b) -> a.compareTo(b));
144+
for ( String iri : unalignedIRIs ) {
145+
writer.write(iri);
146+
writer.write('\n');
147+
}
148+
writer.close();
149+
}
150+
151+
if ( !unalignedClasses.isEmpty() ) {
152+
logger.error("Ontology contains {} top-level unaligned class(es)", unalignedClasses.size());
153+
if ( failOnError ) {
154+
System.exit(1);
155+
}
156+
}
157+
}
158+
159+
/*
160+
* Optionally loads an upper ontology from command line options.
161+
*/
162+
private OWLOntology getUpperOntology(CommandLine line) throws IOException {
163+
OWLOntology ontology = null;
164+
if ( line.hasOption("upper-ontology") ) {
165+
ontology = ioHelper.loadOntology(line.getOptionValue("upper-ontology"), true);
166+
} else if ( line.hasOption("upper-ontology-iri") ) {
167+
ontology = ioHelper.loadOntology(getIRI(line.getOptionValue("upper-ontology-iri"), "upper-ontology-iri"));
168+
} else if ( CommandLineHelper.getBooleanValue(line, "use-cob", false) ) {
169+
ontology = ioHelper.loadOntology(Constants.COB_IRI);
170+
}
171+
172+
return ontology;
173+
}
174+
175+
/*
176+
* Extracts self-declared roots from the ontology.
177+
*/
178+
private Set<OWLClass> getPreferredRoots(OWLOntology ontology) {
179+
Set<OWLClass> roots = new HashSet<>();
180+
for ( OWLAnnotation annot : ontology.getAnnotations() ) {
181+
if ( annot.getProperty().getIRI().equals(Constants.PREFERRED_ROOT_PROPERTY) ) {
182+
OWLAnnotationValue value = annot.getValue();
183+
if ( value.isIRI() ) {
184+
roots.add(factory.getOWLClass(value.asIRI().get()));
185+
} else {
186+
logger.warn("Ignoring non-IRI IAO:0000700 value: {}", value);
187+
}
188+
}
189+
}
190+
191+
return roots;
192+
}
193+
194+
/*
195+
* Gets all top-level unaligned classes in the ontology.
196+
*/
197+
private Set<OWLClass> getUnalignedClasses(OWLOntology ontology, OWLReasoner reasoner, Set<OWLClass> upperClasses,
198+
boolean ignoreDangling) {
199+
Set<OWLClass> unalignedClasses = new HashSet<>();
200+
for ( OWLClass klass : ontology.getClassesInSignature(Imports.INCLUDED) ) {
201+
if ( !klass.isTopEntity() && !upperClasses.contains(klass) && isInBase(klass.getIRI().toString()) ) {
202+
if ( ignoreDangling && Util.isDangling(ontology, klass) ) {
203+
continue;
204+
}
205+
if ( Util.isObsolete(ontology, klass) ) {
206+
continue;
207+
}
208+
209+
Set<OWLClass> ancestors = reasoner.getSuperClasses(klass, false).getFlattened();
210+
boolean aligned = false;
211+
for ( OWLClass upperClass : upperClasses ) {
212+
if ( ancestors.contains(upperClass) ) {
213+
aligned = true;
214+
break;
215+
}
216+
}
217+
if ( !aligned ) {
218+
if ( ancestors.size() == 1 ) {
219+
// This is already a top-level class
220+
logger.debug("Unaligned class: {}", klass.getIRI().toQuotedString());
221+
unalignedClasses.add(klass);
222+
} else {
223+
// Find the top-level ancestor(s)
224+
for ( OWLClass ancestor : ancestors ) {
225+
if ( reasoner.getSuperClasses(ancestor, false).isTopSingleton() ) {
226+
logger.debug("Unaligned class: {} (from {})", ancestor.getIRI().toQuotedString(),
227+
klass.getIRI().toQuotedString());
228+
unalignedClasses.add(ancestor);
229+
}
230+
}
231+
}
232+
}
233+
}
234+
}
235+
236+
return unalignedClasses;
237+
}
238+
239+
private boolean isInBase(String iri) {
240+
for ( String base : basePrefixes ) {
241+
if ( iri.startsWith(base) ) {
242+
return true;
243+
}
244+
}
245+
return basePrefixes.isEmpty();
246+
}
247+
}

src/main/java/org/incenp/obofoundry/odk/Constants.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,6 @@ public class Constants {
3737
public static final IRI SYNONYM_TYPE_PROPERTY = IRI.create(OIO_PREFIX + "SynonymTypeProperty");
3838

3939
public static final IRI COB_IRI = IRI.create("http://purl.obolibrary.org/obo/cob.owl");
40+
41+
public static final IRI PREFERRED_ROOT_PROPERTY = IRI.create("http://purl.obolibrary.org/obo/IAO_0000700");
4042
}

src/main/java/org/incenp/obofoundry/odk/StandaloneRobot.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ public static void main(String[] args) {
8585
m.addCommand("verify", new VerifyCommand());
8686
m.addCommand("odk:normalize", new NormalizeCommand());
8787
m.addCommand("odk:subset", new SubsetCommand());
88-
m.addCommand("odk:validate", new ValidateCommand());
88+
m.addCommand("odk:validate", new CheckAlignmentCommand());
8989

9090
m.main(args);
9191
}

0 commit comments

Comments
 (0)