/******************************************************************************* * This file is part of ecco. * * ecco is distributed under the terms of the GNU Lesser General Public License (LGPL), Version 3.0. * * Copyright 2011-2014, The University of Manchester * * ecco is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser * General Public License as published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * ecco is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser * General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License along with ecco. * If not, see http://www.gnu.org/licenses/. ******************************************************************************/ package uk.ac.manchester.cs.diff.test; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import org.semanticweb.owlapi.apibinding.OWLManager; import org.semanticweb.owlapi.model.AddAxiom; import org.semanticweb.owlapi.model.IRI; 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.OWLEntity; import org.semanticweb.owlapi.model.OWLLogicalAxiom; import org.semanticweb.owlapi.model.OWLOntology; import org.semanticweb.owlapi.model.OWLOntologyCreationException; import org.semanticweb.owlapi.model.OWLOntologyManager; import org.semanticweb.owlapi.reasoner.InferenceType; import org.semanticweb.owlapi.reasoner.OWLReasoner; import uk.ac.manchester.cs.diff.concept.witnesses.WitnessConcepts; import uk.ac.manchester.cs.diff.concept.witnesses.WitnessGroup; import uk.ac.manchester.cs.diff.utils.ReasonerLoader; /** * @author Rafael S. Goncalves <br> * Information Management Group (IMG) <br> * School of Computer Science <br> * University of Manchester <br> */ public class Diff implements Callable<DiffResult>{ private OWLOntology ont1, ont2; private OWLReasoner ont1reasoner, ont2reasoner; private OWLDataFactory df; private Map<OWLClass,Set<OWLClassExpression>> ont1diff, ont2diff; private Set<OWLAxiom> extraAxioms; private Set<OWLClass> sig, affected; private boolean verbose; private String diff; /** * Constructor for subconcept diff w.r.t. given signature * @param f1 Ontology 1 file * @param f2 Ontology 2 file * @param diff Diff type * @param verbose Verbose mode */ public Diff(File f1, File f2, String diff, boolean verbose) { this.ont1 = loadOntology(f1); this.ont2 = loadOntology(f2); this.diff = diff; this.verbose = verbose; sig = new HashSet<OWLClass>(ont1.getClassesInSignature()); sig.addAll(ont2.getClassesInSignature()); df = OWLManager.getOWLDataFactory(); initDataStructures(); equalizeSignatures(ont1, ont2); } /** * Constructor for subconcept diff w.r.t. given signature * @param f1 Ontology 1 file * @param f2 Ontology 2 file * @param sig Set of concept names * @param diff Diff type * @param verbose Verbose mode */ public Diff(File f1, File f2, Set<OWLClass> sig, String diff, boolean verbose) { this.ont1 = loadOntology(f1); this.ont2 = loadOntology(f2); this.sig = sig; this.diff = diff; this.verbose = verbose; df = OWLManager.getOWLDataFactory(); initDataStructures(); equalizeSignatures(ont1, ont2); } /** * Load given ontology file * @param f File * @return Load OWL ontology */ private OWLOntology loadOntology(File f) { OWLOntologyManager man = OWLManager.createOWLOntologyManager(); OWLOntology ont = null; try { ont = man.loadOntologyFromOntologyDocument(f); } catch (OWLOntologyCreationException e) { e.printStackTrace(); } man.removeAxioms(ont, ont.getABoxAxioms(true)); return ont; } /** * Instantiate diff data structures: maps of concept names to sets of witness concepts */ private void initDataStructures() { ont1diff = new HashMap<OWLClass,Set<OWLClassExpression>>(); ont2diff = new HashMap<OWLClass,Set<OWLClassExpression>>(); } /** * Get the concept-based change set between the given ontologies * @return Diff result */ public DiffResult call() { long start = System.currentTimeMillis(); boolean atdiff = false; Map<OWLClass,OWLClassExpression> map = null; if(!atdiff) map = getSubconceptsMapping(); // Precompute subsumptions classifyOntologies(); equalizeSignatures(ont1, ont2); // Get set of affected concept names affected = computeChangeWitnesses(map); if(!atdiff) { // Remove extra axioms ont1.getOWLOntologyManager().removeAxioms(ont1, extraAxioms); ont2.getOWLOntologyManager().removeAxioms(ont2, extraAxioms); } // Precompute atomic subsumptions of altered ontologies (incremental reasoning would be good here) classifyOntologies(); // Get the final, partitioned according to witness impact, change set DiffResult diffResult = splitDirectIndirectChanges(affected); long end = System.currentTimeMillis(); System.out.println("finished diff " + diff + " (total time: " + (end-start)/1000.0 + " secs)"); return diffResult; } /** * Classify both ontologies */ public void classifyOntologies() { if(ont1reasoner!=null) ont1reasoner.dispose(); if(ont2reasoner!=null) ont2reasoner.dispose(); long start = System.currentTimeMillis(); if(verbose) System.out.println("[diff" + diff + "] Classifying ontologies..."); // ExecutorService exec = Executors.newSingleThreadExecutor(); System.out.println("[diff " + diff + "] Ontology 1: " + ont1.getLogicalAxiomCount() + ", Ontology 2: " + ont2.getLogicalAxiomCount()); Classifier ont1worker = new Classifier(ont1); Thread t1 = new Thread(ont1worker); t1.start(); // exec.execute(ont1worker); ont2reasoner = classifyHere(ont2); try { t1.join(); } catch (InterruptedException e) { e.printStackTrace(); } // exec.shutdown(); // try { exec.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); } // catch (InterruptedException e) { e.printStackTrace(); } ont1reasoner = ont1worker.getReasoner(); if(verbose) System.out.println("[diff" + diff + "] done (" + (System.currentTimeMillis()-start)/1000.0 + " secs)"); } private OWLReasoner classifyHere(OWLOntology ont) { System.out.println("[diff" + diff + "] [internal] Starting classification on given ontology"); OWLReasoner reasoner = new ReasonerLoader(ont, verbose).createReasoner(false); reasoner.precomputeInferences(InferenceType.CLASS_HIERARCHY); System.out.println("[diff" + diff + "] [internal] done classifying"); return reasoner; } /** * Compute change witnesses between the given ontologies * @param map Map of fresh concept names to complex concepts * @return Set of affected concept names */ private Set<OWLClass> computeChangeWitnesses(Map<OWLClass,OWLClassExpression> map) { long start = System.currentTimeMillis(); if(verbose) System.out.print("[diff" + diff + "] Computing change witnesses... "); Set<OWLClass> affected = new HashSet<OWLClass>(); Set<OWLClass> toRemove1 = null, toRemove2 = null; if(diff.equals("L")) { toRemove1 = ont1reasoner.getSuperClasses(df.getOWLThing(), false).getFlattened(); toRemove2 = ont2reasoner.getSuperClasses(df.getOWLThing(), false).getFlattened(); } else if(diff.equals("R")) { toRemove1 = ont1reasoner.getUnsatisfiableClasses().getEntitiesMinusBottom(); toRemove2 = ont2reasoner.getUnsatisfiableClasses().getEntitiesMinusBottom(); } // Get change witnesses for each concept for(OWLClass subc : sig) { WitnessConcepts wit = getWitnesses(subc, map, toRemove1, toRemove2, ont1reasoner, ont2reasoner); if(!wit.isEmpty()) affected.add(subc); addChangeToMap(subc, wit.getLHSWitnesses(), ont1diff); addChangeToMap(subc, wit.getRHSWitnesses(), ont2diff); } long end = System.currentTimeMillis(); if(verbose) System.out.println("[diff" + diff + "] done (" + (end-start)/1000.0 + " secs)"); return affected; } /** * Distinguish between directly and indirectly affected concepts * @param affected Set of affected concept names * @return Concept-based change set */ private DiffResult splitDirectIndirectChanges(Set<OWLClass> affected) { if(verbose) System.out.print("[diff" + diff + "] Splitting directly and indirectly affected concepts... "); long start3 = System.currentTimeMillis(); WitnessGroup lhs = getWitnesses(ont1diff, ont1reasoner); WitnessGroup rhs = getWitnesses(ont2diff, ont2reasoner); long end = System.currentTimeMillis(); if(verbose) System.out.println("[diff" + diff + "] done (" + (end-start3)/1000.0 + " secs)"); return new DiffResult(lhs, rhs); } /** * Add affected concept and given witnesses to the specified diff map * @param affected Affected concept * @param witnesses Set of witnesses * @param map Map of affected concepts to their witnesses */ private void addChangeToMap(OWLClass affected, Set<OWLClassExpression> witnesses, Map<OWLClass,Set<OWLClassExpression>> map) { if(!witnesses.isEmpty()) map.put(affected, witnesses); } /** * Get the sets of (LHS and RHS) specialisation witnesses for the given concept * @param subc Concept * @param map Map of fresh concept names to the concepts they represent * @param toRemove1 Set of classes * @param toRemove2 Set of classes * @param ont1reasoner Reasoner instance for ontology1 * @param ont2reasoner Reasoner instance for ontology2 * @return Concept witnesses for the given concept */ private WitnessConcepts getWitnesses(OWLClass subc, Map<OWLClass,OWLClassExpression> map, Set<OWLClass> toRemove1, Set<OWLClass> toRemove2, OWLReasoner ont1reasoner, OWLReasoner ont2reasoner) { Set<OWLClass> ind1 = null, ind2 = null; if(diff.equals("L")) { ind1 = ont1reasoner.getSuperClasses(subc, false).getFlattened(); ind2 = ont2reasoner.getSuperClasses(subc, false).getFlattened(); if(!subc.isOWLThing()) { ind1.removeAll(toRemove1); ind2.removeAll(toRemove2); } } else if(diff.equals("R")) { ind1 = ont1reasoner.getSubClasses(subc, false).getFlattened(); ind2 = ont2reasoner.getSubClasses(subc, false).getFlattened(); if(!subc.isOWLNothing()) { ind1.removeAll(toRemove1); ind2.removeAll(toRemove2); } } return getDifferentConcepts(ind1, ind2, map); } /** * Get the witnesses for the the difference in the given sub or superclass sets * @param set1 Set of classes * @param set2 Set of classes * @param map Map of fresh concept names to concepts * @return Witness concepts in the sub or superclass sets diff */ private WitnessConcepts getDifferentConcepts(Set<OWLClass> set1, Set<OWLClass> set2, Map<OWLClass,OWLClassExpression> map) { Set<OWLClassExpression> rhsWit = getWitnessDiff(set1, set2, map); Set<OWLClassExpression> lhsWit = getWitnessDiff(set2, set1, map); return new WitnessConcepts(lhsWit, rhsWit); } /** * Get the set of different concepts between the given sets * @param set1 Set of classes * @param set2 Set of classes * @param map Map of fresh concept names to concepts * @return Set of different concepts between sets */ private Set<OWLClassExpression> getWitnessDiff(Set<OWLClass> set1, Set<OWLClass> set2, Map<OWLClass,OWLClassExpression> map) { Set<OWLClassExpression> wit = new HashSet<OWLClassExpression>(); for(OWLClass c : set2) { if(!set1.contains(c)) { OWLClassExpression ce = c; if(map != null && map.containsKey(c)) ce = map.get(c); wit.add(ce); } } return wit; } /** * Extrapolate direct and indirect witnesses from the given map of affected concepts and witnesses * @param affectedConceptMap Map of concepts to their change witnesses * @param reasoner Reasoner instance * @return Pack of direct and indirect witnesses */ private WitnessGroup getWitnesses(Map<OWLClass,Set<OWLClassExpression>> affectedConceptMap, OWLReasoner reasoner) { Map<OWLClassExpression,Set<OWLClass>> witMap = getWitnessMap(affectedConceptMap); Map<OWLClass, Set<OWLAxiom>> directWits = new HashMap<OWLClass,Set<OWLAxiom>>(); Map<OWLClass, Set<OWLAxiom>> indirectWits = new HashMap<OWLClass,Set<OWLAxiom>>(); for(OWLClassExpression ce : witMap.keySet()) { // System.out.println("Checking concept: " + getManchesterRendering(ce)); Set<OWLClass> subs = null; if(diff.equals("L")) subs = reasoner.getSubClasses(ce, true).getFlattened(); else subs = reasoner.getSuperClasses(ce, true).getFlattened(); // for(OWLClass c : subs) { // System.out.println("\tDirect subclass: " + getManchesterRendering(c)); // } for(OWLClass c : witMap.get(ce)) { if(subs.contains(c)) { // direct witness // System.out.println("\tDirect witness for " + getManchesterRendering(c) ); if(directWits.containsKey(c)) { Set<OWLAxiom> wits = directWits.get(c); if(diff.equals("L")) wits.add(df.getOWLSubClassOfAxiom(c, ce)); else wits.add(df.getOWLSubClassOfAxiom(ce, c)); directWits.put(c, wits); } else { if(diff.equals("L")) directWits.put(c, new HashSet<OWLAxiom>(Collections.singleton(df.getOWLSubClassOfAxiom(c, ce)))); else directWits.put(c, new HashSet<OWLAxiom>(Collections.singleton(df.getOWLSubClassOfAxiom(ce, c)))); } } else { // indirect witness // System.out.println("\tIndirect witness for " + getManchesterRendering(c)); if(indirectWits.containsKey(c)) { Set<OWLAxiom> wits = indirectWits.get(c); if(diff.equals("L")) wits.add(df.getOWLSubClassOfAxiom(c, ce)); else wits.add(df.getOWLSubClassOfAxiom(ce, c)); indirectWits.put(c, wits); } else { if(diff.equals("L")) indirectWits.put(c, new HashSet<OWLAxiom>(Collections.singleton(df.getOWLSubClassOfAxiom(c, ce)))); else indirectWits.put(c, new HashSet<OWLAxiom>(Collections.singleton(df.getOWLSubClassOfAxiom(ce, c)))); } } } } return new WitnessGroup(directWits, indirectWits); } /** * Given a map of concepts to witnesses, get a reversed map of witnesses to concepts whose change they witness * @param map Map of concepts to witnesses * @return Map of witnesses to concepts whose change they witness */ private Map<OWLClassExpression,Set<OWLClass>> getWitnessMap(Map<OWLClass,Set<OWLClassExpression>> map) { Map<OWLClassExpression,Set<OWLClass>> output = new HashMap<OWLClassExpression,Set<OWLClass>>(); for(OWLClass c : map.keySet()) { Set<OWLClassExpression> wits = map.get(c); for(OWLClassExpression wit : wits) { if(output.containsKey(wit)) { Set<OWLClass> classes = output.get(wit); classes.add(c); output.put(wit, classes); } else output.put(wit, new HashSet<OWLClass>(Collections.singleton(c))); } } return output; } /** * Class hierarchy pre-computation */ class Classifier implements Runnable { private OWLOntology ont; private OWLReasoner reasoner; public Classifier(OWLOntology ont) { this.ont = ont; } @Override public void run() { System.out.println("[diff" + diff + "] [Classifier] Starting classification on given ontology"); reasoner = new ReasonerLoader(ont, verbose).createReasoner(false); reasoner.precomputeInferences(InferenceType.CLASS_HIERARCHY); System.out.println("[diff" + diff + "] [Classifier] done classifying "); } public OWLReasoner getReasoner() { return reasoner; } } /** * Collect sub-concepts in both ontologies * @return Set of subconcepts */ private Set<OWLClassExpression> collectSCs() { if(verbose) System.out.print("[diff" + diff + "] Extracting subconcepts from given ontologies... "); Set<OWLClassExpression> scs = new HashSet<OWLClassExpression>(); getSubConcepts(ont1, scs); getSubConcepts(ont2, scs); if(verbose) System.out.println("[diff" + diff + "] done. Nr. of subconcepts: " + scs.size()); return scs; } /** * Get sub-concepts of an ontology * @param ont Ontology * @param sc Set of subconcepts * @return Updated set of subconcepts */ private Set<OWLClassExpression> getSubConcepts(OWLOntology ont, Set<OWLClassExpression> sc) { Set<OWLLogicalAxiom> axs = ont.getLogicalAxioms(); for(OWLAxiom ax : axs) { Set<OWLClassExpression> ax_sc = ax.getNestedClassExpressions(); for(OWLClassExpression ce : ax_sc) { if(!sc.contains(ce) && !ce.isOWLThing() && !ce.isOWLNothing()) { if(ce.isAnonymous()) { sc.add(ce); getSubConcepts(ce, sc); } } } } return sc; } /** * Recursively get subconcepts of subconcept * @param ce Subconcept * @param sc Set of subconcepts */ private void getSubConcepts(OWLClassExpression ce, Set<OWLClassExpression> sc) { if(ce.getNestedClassExpressions().size() > 0) { for(OWLClassExpression c : ce.getNestedClassExpressions()) { if(!sc.contains(c) && !c.isOWLThing() && !c.isOWLNothing()) { if(c.isAnonymous()) { sc.add(c); getSubConcepts(c, sc); } } } } } /** * Create a mapping between a new term "TempX" and each sub-concept, and add the appropriate * equivalence axioms to each ontology * @return Map of new terms to subconcepts */ private Map<OWLClass,OWLClassExpression> getSubconceptsMapping() { Set<OWLClassExpression> sc = collectSCs(); Map<OWLClass,OWLClassExpression> map = new HashMap<OWLClass,OWLClassExpression>(); int counter = 1; extraAxioms = new HashSet<OWLAxiom>(); System.out.println("[diff" + diff + "] Adding axioms..."); for(OWLClassExpression ce : sc) { OWLClass c = df.getOWLClass(IRI.create("diffSubc_" + counter)); map.put(c, ce); OWLAxiom ax = null; if(diff.equals("L")) ax = df.getOWLSubClassOfAxiom(c, ce); else if(diff.equals("R")) ax = df.getOWLEquivalentClassesAxiom(c, ce); extraAxioms.add(ax); counter++; } ont1.getOWLOntologyManager().addAxioms(ont1, extraAxioms); ont2.getOWLOntologyManager().addAxioms(ont2, extraAxioms); System.out.println("[diff" + diff + "] Added " + extraAxioms.size() + " axioms"); return map; } /** * Given two ontologies, inject entity declarations so that both ontologies * end up with the same signature * @param ont1 Ontology 1 * @param ont2 Ontology 2 */ private void equalizeSignatures(OWLOntology ont1, OWLOntology ont2) { Set<OWLEntity> ont1sig = ont1.getSignature(); Set<OWLEntity> ont2sig = ont2.getSignature(); ont1sig.removeAll(ont2sig); ont2sig.removeAll(ont1sig); List<AddAxiom> ont1axs = new ArrayList<AddAxiom>(); for(OWLEntity c : ont1sig) { ont1axs.add(new AddAxiom(ont2, df.getOWLDeclarationAxiom(c))); } ont2.getOWLOntologyManager().applyChanges(ont1axs); List<AddAxiom> ont2axs = new ArrayList<AddAxiom>(); for(OWLEntity c : ont2sig) { ont2axs.add(new AddAxiom(ont1, df.getOWLDeclarationAxiom(c))); } ont1.getOWLOntologyManager().applyChanges(ont2axs); } /** * Get the set of affected concept names * @return Set of affected concept names */ public Set<OWLClass> getAffectedConcepts() { return affected; } }