Initial commit.

equiv-set-merge
Damien Goutte-Gattat 2023-05-01 21:58:26 +01:00
commit 37fa00233d
Signed by: damien
GPG Key ID: 6F7F0F91D138FC7B
10 changed files with 632 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.classpath
.project
.settings
dependency-reduced-pom.xml
target

59
pom.xml Normal file
View File

@ -0,0 +1,59 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.incenp</groupId>
<artifactId>robot-plugins</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>ROBOT plugins</name>
<dependencies>
<dependency>
<groupId>org.obolibrary.robot</groupId>
<artifactId>robot-command</artifactId>
<version>1.10.0-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<release>8</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.MF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.SF</exclude>
</excludes>
</filter>
</filters>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.obolibrary.robot.CommandLineInterface</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,36 @@
package org.incenp.obofoundry.helpers;
import org.semanticweb.owlapi.model.IRI;
/**
* Helper class to deal with short IRIs ("CURIEs").
*/
public class CurieManager {
private static CurieManager instance = new CurieManager();
public static CurieManager getInstance() {
return instance;
}
public IRI expand(String curie) throws IllegalArgumentException {
if ( curie == null || curie.length() == 0 ) {
throw new IllegalArgumentException("Null or zero-length CURIE");
}
if ( curie.startsWith("http:") ) {
return IRI.create(curie);
}
String[] parts = curie.split(":", 2);
if ( parts.length == 1 ) {
return IRI.create(curie);
}
if ( parts[0].length() == 0 || parts[1].length() == 0 ) {
throw new IllegalArgumentException("Zero-length CURIE part");
}
return IRI.create(String.format("http://purl.obolibrary.org/obo/%s_%s", parts[0], parts[1]));
}
}

View File

@ -0,0 +1,10 @@
package org.incenp.obofoundry.helpers;
public class ReasoningException extends Exception {
private static final long serialVersionUID = -4867170036764099132L;
public ReasoningException(String msg) {
super(msg);
}
}

View File

@ -0,0 +1,311 @@
package org.incenp.obofoundry.helpers;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.semanticweb.owlapi.model.AxiomType;
import org.semanticweb.owlapi.model.IRI;
import org.semanticweb.owlapi.model.OWLAnnotationAssertionAxiom;
import org.semanticweb.owlapi.model.OWLAnnotationValue;
import org.semanticweb.owlapi.model.OWLAxiom;
import org.semanticweb.owlapi.model.OWLClass;
import org.semanticweb.owlapi.model.OWLClassExpression;
import org.semanticweb.owlapi.model.OWLDataFactory;
import org.semanticweb.owlapi.model.OWLEquivalentClassesAxiom;
import org.semanticweb.owlapi.model.OWLLiteral;
import org.semanticweb.owlapi.model.OWLObjectIntersectionOf;
import org.semanticweb.owlapi.model.OWLObjectProperty;
import org.semanticweb.owlapi.model.OWLObjectSomeValuesFrom;
import org.semanticweb.owlapi.model.OWLOntology;
import org.semanticweb.owlapi.model.OWLOntologyManager;
import org.semanticweb.owlapi.model.OWLSubClassOfAxiom;
import org.semanticweb.owlapi.model.parameters.Imports;
import org.semanticweb.owlapi.reasoner.OWLReasoner;
import org.semanticweb.owlapi.reasoner.OWLReasonerFactory;
/**
* This class may be used to create multi-species ontologies following the
* <a href=
* "https://github.com/obophenotype/uberon/wiki/Multi-species-composite-ontologies">"composite/merger"
* strategy</a> described in Uberon's wiki.
*
* It is heavily based on Chris Mungall's <a href=
* "https://github.com/owlcollab/owltools/blob/master/OWLTools-Core/src/main/java/owltools/mooncat/SpeciesMergeUtil.java">implementation</a>
* of that strategy in OWLTools.
*/
public class SpeciesMerger {
private OWLClass taxClass;
private OWLObjectProperty linkProperty;
private String suffix;
private OWLOntology ontology;
private OWLOntologyManager manager;
private OWLDataFactory factory;
private OWLReasonerFactory reasonerFactory;
private OWLReasoner reasoner;
private OWLClass txRootClass;
private Set<OWLClass> txClasses;
private Map<OWLClass, OWLClass> ecMap;
private Map<OWLClass, OWLClassExpression> exMap;
/**
* Creates a new instance with BFO:0000050 as the linking property.
*
* @param ontology The ontology to operate on.
* @param reasonerFactory The reasoner factory to use.
*/
public SpeciesMerger(OWLOntology ontology, OWLReasonerFactory reasonerFactory) {
this(ontology, reasonerFactory, IRI.create("http://purl.obolibrary.org/obo/BFO_0000050"));
}
/**
* Creates a new instance.
*
* @param ontology The ontology to operate on.
* @param reasonerFactory The reasoner factory to use.
* @param linkProperty The object property used to link taxon-specific
* classes to their taxon-neutral equivalent.
*/
public SpeciesMerger(OWLOntology ontology, OWLReasonerFactory reasonerFactory, IRI linkProperty) {
this.ontology = ontology;
this.reasonerFactory = reasonerFactory;
manager = ontology.getOWLOntologyManager();
factory = ontology.getOWLOntologyManager().getOWLDataFactory();
this.linkProperty = factory.getOWLObjectProperty(linkProperty);
}
/**
* Unfold classes for the specified taxon.
*
* @param taxon The taxon to unfold for.
* @param suffix The suffix to append to the label of unfolded subclasses.
*/
public void merge(IRI taxon, String suffix) throws ReasoningException {
reasoner = reasonerFactory.createReasoner(ontology);
this.suffix = suffix;
taxClass = factory.getOWLClass(taxon);
listTaxonSpecificClasses();
createMaps();
for ( OWLClass c : txClasses ) {
if ( c.isBottomEntity() ) {
continue;
}
if ( !reasoner.isSatisfiable(c) ) {
throw new ReasoningException("Unsatisfiable class: " + c);
}
Set<OWLAxiom> axioms = new HashSet<OWLAxiom>();
axioms.addAll(ontology.getAxioms(c, Imports.EXCLUDED));
axioms.addAll(ontology.getAnnotationAssertionAxioms(c.getIRI()));
for ( OWLClass p : reasoner.getSuperClasses(c, true).getFlattened() ) {
axioms.add(factory.getOWLSubClassOfAxiom(c, p));
}
Set<OWLAxiom> newAxioms = new HashSet<OWLAxiom>();
for ( OWLAxiom axiom : axioms ) {
OWLAxiom newAxiom;
if (axiom instanceof OWLSubClassOfAxiom) {
newAxiom = translate((OWLSubClassOfAxiom) axiom);
}
else if (axiom instanceof OWLEquivalentClassesAxiom) {
newAxiom = translate((OWLEquivalentClassesAxiom) axiom);
}
else if (axiom instanceof OWLAnnotationAssertionAxiom) {
newAxiom = translate(c, (OWLAnnotationAssertionAxiom) axiom);
}
else {
newAxiom = null;
}
if (newAxiom != null && ecMap.containsKey(c)) {
for (OWLClass sc : newAxiom.getClassesInSignature()) {
if (isSkippable(sc)) {
newAxiom = null;
break;
}
}
}
if ( newAxiom != null && !newAxiom.getClassesInSignature().contains(txRootClass) ) {
newAxioms.add(newAxiom);
}
}
manager.removeAxioms(ontology, axioms);
manager.addAxioms(ontology, newAxioms);
}
}
/*
* Prepare a flat list of all the taxon-specific classes (all inferred
* subclasses of "<property> some <taxon>").
*/
private void listTaxonSpecificClasses() {
txRootClass = factory.getOWLClass(IRI.create(taxClass.getIRI().toString() + "-part"));
OWLEquivalentClassesAxiom qax = factory.getOWLEquivalentClassesAxiom(txRootClass,
factory.getOWLObjectSomeValuesFrom(linkProperty, taxClass));
manager.addAxiom(ontology, qax);
reasoner.flush();
txClasses = reasoner.getSubClasses(txRootClass, false).getFlattened();
manager.removeAxiom(ontology, qax);
manager.removeAxiom(ontology, factory.getOWLDeclarationAxiom(txRootClass));
}
/*
* Iterate over all the EquivalentClasses axioms of the form
* "C equivalentTo N and (P some T)" and create two maps that associate the
* taxon-specific class C to:
*
* - the taxon-neutral class N (ecMap);
*
* - the entire class expression "N and (P some T)" (exMap).
*/
private void createMaps() {
ecMap = new HashMap<OWLClass, OWLClass>();
exMap = new HashMap<OWLClass, OWLClassExpression>();
for ( OWLEquivalentClassesAxiom eca : ontology.getAxioms(AxiomType.EQUIVALENT_CLASSES, Imports.INCLUDED) ) {
// Only get the axioms involving both the link property P and the taxon T.
if ( !eca.getClassesInSignature().contains(taxClass)
|| !eca.getObjectPropertiesInSignature().contains(linkProperty) ) {
continue;
}
for ( OWLClass c : eca.getClassesInSignature() ) {
if ( !txClasses.contains(c) ) {
continue;
}
// At this point c is the taxon-specific class. Now we get the expression it is
// equivalent to.
for ( OWLClassExpression x : eca.getClassExpressionsMinus(c) ) {
if ( x instanceof OWLObjectIntersectionOf ) {
OWLObjectIntersectionOf oio = (OWLObjectIntersectionOf) x;
for ( OWLClassExpression n : oio.getOperands() ) {
if ( n instanceof OWLClass ) {
ecMap.put(c, (OWLClass) n); /* C -> N */
exMap.put(c, x); /* C -> N and (P some T) */
}
}
}
}
}
}
}
private OWLAxiom translate(OWLEquivalentClassesAxiom axiom) {
// Equivalent classes axioms are translated by translating their component class
// expressions.
Set<OWLClassExpression> xs = new HashSet<OWLClassExpression>();
for ( OWLClassExpression x : axiom.getClassExpressions() ) {
OWLClassExpression tx = translateExpression(x, true);
if ( tx == null ) {
// If one class expression cannot be translated, the entire
// equivalent axiom cannot be translated.
return null;
}
xs.add(tx);
}
return factory.getOWLEquivalentClassesAxiom(xs);
}
private OWLAxiom translate(OWLSubClassOfAxiom axiom) {
OWLClassExpression trSub = translateExpression(axiom.getSubClass(), true);
OWLClassExpression trSuper = translateExpression(axiom.getSuperClass(), false);
// Both sides of the axiom need to be translatable.
if ( trSub == null || trSuper == null ) {
return null;
}
// Avoid circular references.
if ( trSub.getClassesInSignature().contains(trSuper) ) {
return null;
}
// No need for this SubClassOf axiom if the taxon-neutral class is already a
// subclass of the translated superclass.
if ( !trSub.equals(axiom.getSubClass()) ) {
if ( reasoner.getSuperClasses(ecMap.get(axiom.getSubClass()), false).getFlattened().contains(trSuper) ) {
return null;
}
Set<OWLClass> ancs = new HashSet<OWLClass>();
ancs.addAll(reasoner.getSuperClasses(ecMap.get(axiom.getSubClass()), false).getFlattened());
ancs.addAll(reasoner.getEquivalentClasses(ecMap.get(axiom.getSubClass())).getEntities());
for ( OWLClass p : ancs ) {
for ( OWLSubClassOfAxiom sca : ontology.getSubClassAxiomsForSubClass(p) ) {
if ( sca.getSuperClass().equals(trSuper) ) {
return null;
}
}
}
}
return factory.getOWLSubClassOfAxiom(trSub, trSuper);
}
private OWLClassExpression translateExpression(OWLClassExpression x, boolean mustBeEquiv) {
if ( !x.isAnonymous() ) {
if ( mustBeEquiv ) {
return exMap.getOrDefault(x, x);
} else {
return ecMap.getOrDefault(x, (OWLClass) x);
}
} else {
if ( x instanceof OWLObjectSomeValuesFrom ) {
OWLObjectSomeValuesFrom svf = (OWLObjectSomeValuesFrom) x;
return factory.getOWLObjectSomeValuesFrom(svf.getProperty(),
translateExpression(svf.getFiller(), mustBeEquiv));
}
}
return null;
}
private OWLAxiom translate(OWLClass c, OWLAnnotationAssertionAxiom axiom) {
if ( ecMap.containsKey(c) ) {
// No translation needed for the unfolded classes.
return null;
}
if ( axiom.getProperty().isLabel() ) {
// Translate the label by appending the taxon-specific suffix.
OWLLiteral lit = axiom.getValue().asLiteral().get();
String newLabel = lit.getLiteral() + " (" + suffix + ")";
return factory.getOWLAnnotationAssertionAxiom(axiom.getProperty(), axiom.getSubject(),
factory.getOWLLiteral(newLabel));
}
// Use other annotations as they are.
return axiom;
}
private boolean isSkippable(OWLClass c) {
for ( OWLAnnotationAssertionAxiom ax : ontology.getAnnotationAssertionAxioms(c.getIRI()) ) {
if ( !ax.getProperty().getIRI().toString().endsWith("inSubset") ) {
continue;
}
OWLAnnotationValue v = ax.getValue();
if ( v.isIRI() ) {
String val = v.asIRI().toString();
if ( val.contains("upper_level") || val.contains("non_informative")
|| val.contains("early_development") ) {
return true;
}
}
}
return false;
}
}

View File

@ -0,0 +1,115 @@
package org.incenp.obofoundry.robot;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.Options;
import org.obolibrary.robot.Command;
import org.obolibrary.robot.CommandLineHelper;
import org.obolibrary.robot.CommandState;
/**
* Helper base class for ROBOT commands.
*
* This class is intended to serve as a base class for ROBOT commands, to avoid
* duplicating boilerplate across several commands. Subclasses should call the
* constructor with the desired name, description, and help message, add any
* option they need, and implement the {@link performOperation} method.
*/
public abstract class BasePlugin implements Command {
private String name;
private String description;
private String usage;
private boolean with_io;
protected Options options;
/**
* Creates a new command.
*
* @param name The command name, as it should be invoked on the command
* line.
* @param description The description of the command that ROBOT will display.
* @param usage The help message for the command.
* @param with_io If true, input/output options will be handled
* automatically.
*/
protected BasePlugin(String name, String description, String usage, boolean with_io) {
this.name = name;
this.description = description;
this.usage = usage;
this.with_io = with_io;
options = CommandLineHelper.getCommonOptions();
if ( with_io ) {
options.addOption("i", "input", true, "load ontology from file");
options.addOption("I", "input-iri", true, "load ontology from IRI");
options.addOption("o", "output", true, "save ontology to file");
}
}
/**
* Creates a new command with default I/O options.
*
* @param name The command name, as it should be involed on the command
* line.
* @param description The description of the command that ROBOT will display.
* @param usage The help message for the command.
*/
protected BasePlugin(String name, String description, String usage) {
this(name, description, usage, true);
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
public String getUsage() {
return usage;
}
public Options getOptions() {
return options;
}
public void main(String[] args) {
try {
execute(null, args);
} catch ( Exception e ) {
CommandLineHelper.handleException(e);
}
}
public CommandState execute(CommandState state, String[] args) throws Exception {
CommandLine line = CommandLineHelper.getCommandLine(usage, options, args);
if ( line == null ) {
return null;
}
if ( state == null ) {
state = new CommandState();
}
if ( with_io ) {
state = CommandLineHelper.updateInputOntology(CommandLineHelper.getIOHelper(line), state, line);
}
performOperation(state, line);
if ( with_io ) {
CommandLineHelper.maybeSaveOutput(line, state.getOntology());
}
return state;
}
/**
* Perform whatever operation the command is supposed to do.
*
* @param state The internal state of ROBOT.
* @param line The command line used to invoke the command.
* @throws Exception If any error occurred when attempting to execute the
* operation.
*/
public abstract void performOperation(CommandState state, CommandLine line) throws Exception;
}

View File

@ -0,0 +1,35 @@
package org.incenp.obofoundry.robot;
import org.apache.commons.cli.CommandLine;
import org.obolibrary.robot.CommandState;
import org.semanticweb.owlapi.model.AddOntologyAnnotation;
import org.semanticweb.owlapi.model.OWLAnnotation;
import org.semanticweb.owlapi.model.OWLDataFactory;
import org.semanticweb.owlapi.model.OWLOntology;
import org.semanticweb.owlapi.vocab.OWLRDFVocabulary;
/**
* A "hello world"-type example of a ROBOT pluggable command.
*
* This command injects a "hello" message as an ontology annotation.
*/
public class HelloCommand extends BasePlugin {
public HelloCommand() {
super("hello", "insert a hello message into an ontology",
"robot hello --input <FILE> [--target <TARGET>] --output <FILE>");
options.addOption("r", "recipient", true, "recipient of the hello message");
}
@Override
public void performOperation(CommandState state, CommandLine line) {
OWLOntology ontology = state.getOntology();
OWLDataFactory fac = ontology.getOWLOntologyManager().getOWLDataFactory();
String recipient = line.getOptionValue('r', "world");
OWLAnnotation annot = fac.getOWLAnnotation(fac.getOWLAnnotationProperty(OWLRDFVocabulary.RDFS_COMMENT.getIRI()),
fac.getOWLLiteral(String.format("Hello, %s", recipient), ""));
ontology.getOWLOntologyManager().applyChange(new AddOntologyAnnotation(ontology, annot));
}
}

View File

@ -0,0 +1,37 @@
package org.incenp.obofoundry.robot;
import org.apache.commons.cli.CommandLine;
import org.incenp.obofoundry.helpers.CurieManager;
import org.incenp.obofoundry.helpers.SpeciesMerger;
import org.obolibrary.robot.CommandLineHelper;
import org.obolibrary.robot.CommandState;
import org.semanticweb.owlapi.model.IRI;
public class MergeSpeciesCommand extends BasePlugin {
public MergeSpeciesCommand() {
super("merge-species", "create a composite cross-species ontology",
"robot merge-species -i <FILE> -t TAXON [-s SUFFIX] -o <FILE>");
options.addOption("t", "taxon", true, "unfoled for specified taxon");
options.addOption("p", "property", true, "unfold on specified property");
options.addOption("s", "suffix", true, "suffix to append to class labels");
options.addOption("r", "reasoner", true, "reasoner to use");
}
@Override
public void performOperation(CommandState state, CommandLine line) throws Exception {
if ( !line.hasOption('t') ) {
throw new IllegalArgumentException("Missing --taxon argument");
}
IRI taxonIRI = CurieManager.getInstance().expand(line.getOptionValue('t'));
IRI propertyIRI = CurieManager.getInstance().expand(line.getOptionValue("p", "BFO:0000050"));
String suffix = line.getOptionValue("s", "species specific");
SpeciesMerger merger = new SpeciesMerger(state.getOntology(), CommandLineHelper.getReasonerFactory(line),
propertyIRI);
merger.merge(taxonIRI, suffix);
}
}

View File

@ -0,0 +1,23 @@
package org.incenp.obofoundry.robot;
import java.util.ArrayList;
import java.util.List;
import org.obolibrary.robot.Command;
import org.obolibrary.robot.ICommandProvider;
/**
* Provider for all the pluggable ROBOT commands available in this package.
*/
public class Provider implements ICommandProvider {
public List<Command> getCommands() {
ArrayList<Command> cmds = new ArrayList<Command>();
cmds.add(new HelloCommand());
cmds.add(new MergeSpeciesCommand());
return cmds;
}
}

View File

@ -0,0 +1 @@
org.incenp.obofoundry.robot.Provider