openllet.modularity.AbstractModuleExtractor Maven / Gradle / Ivy
// Copyright (c) 2006 - 2008, Clark & Parsia, LLC.
// This source code is available under the terms of the Affero General Public License v3.
//
// Please see LICENSE.txt for full license terms, including the availability of proprietary exceptions.
// Questions, comments, or requests for clarification: [email protected]
package openllet.modularity;
import com.clarkparsia.owlapi.modularity.locality.LocalityClass;
import com.clarkparsia.owlapi.modularity.locality.LocalityEvaluator;
import com.clarkparsia.owlapi.modularity.locality.SyntacticLocalityEvaluator;
import java.io.IOException;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import openllet.core.KnowledgeBase.ChangeType;
import openllet.core.OpenlletOptions;
import openllet.core.expressivity.Expressivity;
import openllet.core.taxonomy.Taxonomy;
import openllet.core.taxonomy.TaxonomyNode;
import openllet.core.utils.MultiValueMap;
import openllet.core.utils.SetUtils;
import openllet.core.utils.Timer;
import openllet.core.utils.Timers;
import openllet.core.utils.progress.ConsoleProgressMonitor;
import openllet.core.utils.progress.ProgressMonitor;
import openllet.modularity.io.ModuleExtractorPersistence;
import openllet.modularity.io.UncloseableOutputStream;
import openllet.owlapi.OWL;
import openllet.shared.tools.Log;
import org.semanticweb.owlapi.model.OWLAxiom;
import org.semanticweb.owlapi.model.OWLClass;
import org.semanticweb.owlapi.model.OWLEntity;
import org.semanticweb.owlapi.model.OWLOntology;
/**
*
* Copyright: Copyright (c) 2007
*
*
* Company: Clark & Parsia, LLC.
*
*
* @author Evren Sirin
*/
public abstract class AbstractModuleExtractor implements ModuleExtractor
{
public static final Logger _logger = Log.getLogger(AbstractModuleExtractor.class);
private final Set _additions = SetUtils.create();
private final Set _newClasses = SetUtils.create();
/**
* Map to find entities referenced in an axiom
*/
private final Set _axioms = SetUtils.create();
/**
* Set of _axioms that will be deleted
*/
private final Set _deletions = SetUtils.create();
/**
* The types of changes that are pending in additions and deletions
*/
protected EnumSet _changes = EnumSet.noneOf(ChangeType.class);
/**
* Map to find _axioms that references an axiom
*/
protected MultiValueMap _entityAxioms = new MultiValueMap<>();
private LocalityEvaluator _localityEvaluator = null;
protected MultiValueMap _modules = null;
/**
* Flag to check if a non-local axiom has been updated
*/
private boolean _nonLocalAxioms = false;
private final Timers _timers = new Timers();
public AbstractModuleExtractor()
{
this(new SyntacticLocalityEvaluator(LocalityClass.BOTTOM_BOTTOM));
}
public AbstractModuleExtractor(final LocalityEvaluator localityEvaluator)
{
_localityEvaluator = localityEvaluator;
}
@Override
public void addAxiom(final OWLAxiom axiom)
{
checkNonLocalAxiom(axiom);
if (_axioms.contains(axiom))
return;
_logger.fine(() -> "Adding " + axiom);
_deletions.remove(axiom);
_additions.add(axiom);
categorizeAddedAxiom(axiom);
}
/**
* Returns if the extracted modules can be updated. We can update the modules if we have computed modules and no non-local axiom has been added or deleted.
*
* @return
*/
@Override
public boolean canUpdate()
{
return _modules != null && !_nonLocalAxioms;
}
/**
* Checks if the given axiom is non-local w.r.t. empty signature and updates the {@link #_nonLocalAxioms} field.
*
* @param axiom - Axiom to be checked
*/
private void checkNonLocalAxiom(final OWLAxiom axiom)
{
if (!axiom.isLogicalAxiom())
return;
// no need to check for non-locals if we already know that we cannot
// update modules
if (canUpdate())
if (!isLocal(axiom, Collections. emptySet()))
{
_logger.warning("*** Non-local axiom: " + axiom);
_nonLocalAxioms = true;
}
}
@Override
public void deleteAxiom(final OWLAxiom axiom)
{
checkNonLocalAxiom(axiom);
if (!_axioms.contains(axiom))
{
if (_additions.remove(axiom))
_logger.fine(() -> "Deleted axiom from add _queue before processing " + axiom);
return;
}
_logger.fine(() -> "Deleting " + axiom);
_additions.remove(axiom);
_deletions.add(axiom);
categorizeRemovedAxiom(axiom);
}
@Override
public MultiValueMap getModules()
{
return _modules;
}
/**
* Extract modules from scratch
*
* @return
*/
@Override
public MultiValueMap extractModules()
{
final Timer timer = _timers.startTimer("extractModules");
// _cache the axiom signatures
processAdditions();
_additions.clear();
// no need to consider _deletions for initial module extraction
_deletions.clear();
_changes.clear();
_nonLocalAxioms = false;
_modules = new MultiValueMap<>();
extractModuleSignatures(_entityAxioms.keySet());
timer.stop();
return _modules;
}
/**
* This is a main method to extract the signature for a set of classes Note that this method updates the modules for the classes which are maintained
* (either newly created or already existing) in a module partial _order
*
* @param entities - the set of entities whose modules should be extracted
*/
private void extractModuleSignatures(final Set entities)
{
_logger.fine(() -> "Extracting module for each of " + entities);
if (entities.isEmpty())
return;
final ProgressMonitor monitor = new ConsoleProgressMonitor();
monitor.setProgressTitle("Extracting");
monitor.setProgressLength(entities.size());
//monitor.setProgressLength(10);
monitor.taskStarted();
extractModuleSignatures(entities, monitor);
monitor.taskFinished();
_logger.finer(() -> "Modules: " + _modules);
}
protected abstract void extractModuleSignatures(Set entities, ProgressMonitor monitor);
/**
* Given an axiom, this function locates all root _nodes in the partial _order that are affected by the update
*
* @param axiom - the update
* @param add - Flag for _additions/_deletions
*/
private Set getAffectedRoots(final OWLAxiom axiom, final Taxonomy taxonomy, final boolean add)
{
final Set roots = new HashSet<>();
final Set> visited = new HashSet<>();
visited.add(taxonomy.getBottomNode());
getAffectedRoots(axiom, taxonomy.getTop(), roots, add, visited);
/*
* Special case when the only _node affected by a deletion is
* unsatisfiable
*/
if (!add && roots.isEmpty())
for (final OWLClass unsat : taxonomy.getEquivalents(OWL.Nothing))
{
final Set signature = _modules.get(unsat);
if ((signature != null) && signature.containsAll(getSignature(axiom)))
roots.add(unsat);
}
return roots;
}
/**
* Given an axiom, this function locates all root _nodes in the partial _order that are affected by the update
*
* @param axiom - the update
* @param _node - the next _node
* @param effects - the actual set of affected _nodes collected
* @param add - Flag for _additions/_deletions
* @param visited - _nodes visited so far
*/
private void getAffectedRoots(final OWLAxiom axiom, final TaxonomyNode node, final Set effects, final boolean add, final Set> visited)
{
// only proceed if not seen this _node
if (visited.contains(node))
return;
else
visited.add(node);
final OWLEntity entity = node.getName();
// get the sig for this module
final Set signature = _modules.get(entity);
boolean outdated = false;
// check if the entity has been removed due to a deletion
if (signature == null)
{
if (_logger.isLoggable(Level.FINE))
_logger.fine("Removed entity " + entity);
}
else
if (add)
// only affected if axiom is non-local w.r.t. the sig of the
// module
outdated = !isLocal(axiom, signature);
else
// only affected if sig of axiom is contained in sig of module
outdated = signature.containsAll(getSignature(axiom));
// if outdated add to effected set
if (outdated)
effects.addAll(node.getEquivalents());
else
// recursive call to children
for (final TaxonomyNode next : node.getSubs())
getAffectedRoots(axiom, next, effects, add, visited);
}
/**
* Return the _axioms which references this entity
*
* @param entity
* @return
*/
@Override
public Stream axioms(final OWLEntity entity)
{
final Set axioms = _entityAxioms.get(entity);
if (axioms == null)
return Stream.empty();
return axioms.stream();
}
@Deprecated
@Override
public Set getAxioms(final OWLEntity entity)
{
Set axioms = _entityAxioms.get(entity);
if (axioms == null)
axioms = Collections.emptySet();
return axioms;
}
@Override
public OWLOntology getModule(final OWLEntity entity)
{
return getModuleFromSignature(_modules.get(entity));
}
protected Set getModuleAxioms(final Set signature)
{
final Set referenced = new HashSet<>();
final Set augmentedSig = new HashSet<>(signature);
augmentedSig.add(OWL.Thing);
final Set axioms = new HashSet<>();
final Set candidates = new HashSet<>();
for (final OWLEntity e : signature)
candidates.addAll(getAxioms(e));
for (final OWLAxiom axiom : candidates)
{
final Set sigAxiom = axiom.signature().collect(Collectors.toSet());
/*
* An axiom is in the module of the signature if the augmented
* signature contains all the entities referenced in the axiom.
* However, there are cases this necessary _condition is not
* sufficient. For example, an axiom may be a tautology, e.g.
* PropertyDomain(p owl:Thing), and if this axiom is included the
* module would not be minimal anymore. The locality check we
* perform for the axiom w.r.t. the given signature filters these
* _axioms.
*/
if (augmentedSig.containsAll(sigAxiom) && !isLocal(axiom, signature))
{
axioms.add(axiom);
referenced.addAll(sigAxiom);
}
}
/*
* Special handling is required if an entity is in the input signature,
* and is referenced by some _axioms, but all those _axioms are local to
* the signature and contain entities not in the signature. A
* declaration axiom is used to keep the entity in the module.
*/
final Set notReferenced = new HashSet<>(signature);
notReferenced.removeAll(referenced);
for (final OWLEntity e : notReferenced)
if (_entityAxioms.get(e) != null)
axioms.add(OWL.declaration(e));
return axioms;
}
/**
* Returns a new ontology that contains the _axioms that are in the module for given set of entities
*
* @param signature
* @return
* @throws OWLException
*/
@Override
public OWLOntology getModuleFromSignature(final Set signature)
{
final Set moduleAxioms = getModuleAxioms(signature);
return OWL.Ontology(moduleAxioms);
}
/**
* Get the entities referenced in this axiom
*
* @param axiom
* @return
*/
@Deprecated
protected Set getSignature(final OWLAxiom axiom)
{
return axiom.getSignature();
}
protected Stream signature(final OWLAxiom axiom)
{
return axiom.signature();
}
/**
* Checks if _axioms have been added/removed and modules need to be updated
*
* @return true
if _axioms have been added/removed
*/
@Override
public boolean isChanged()
{
return !_additions.isEmpty() || !_deletions.isEmpty() || _nonLocalAxioms;
}
protected boolean isLocal(final OWLAxiom axiom, final Set signature)
{
return _localityEvaluator.isLocal(axiom, signature);
}
@Deprecated
@Override
public void addAxioms(final Iterable axioms)
{
for (final OWLAxiom axiom : axioms)
addAxiom(axiom);
}
@Override
public void addAxioms(final Stream axioms)
{
axioms.forEach(this::addAxiom);
}
private void processAdditions()
{
for (final OWLAxiom axiom : _additions)
{
_axioms.add(axiom);
axiom.signature().forEach(entity ->
{
_entityAxioms.add(entity, axiom);
if (entity instanceof OWLClass)
{
final OWLClass cls = (OWLClass) entity;
if (_modules != null && !_modules.containsKey(cls))
_newClasses.add(cls);
}
});
}
}
private void processDeletions()
{
for (final OWLAxiom axiom : _deletions)
{
_axioms.remove(axiom);
axiom.signature().forEach(entity ->
{
_entityAxioms.remove(entity, axiom);
if (!_entityAxioms.containsKey(entity))
{
_logger.fine(() -> "Remove " + entity + " which is not mentioned anymore");
_modules.remove(entity);
}
});
}
}
/**
* Method to 1) find affected modules and 2) update them - that is extract their new signatures
*
* @param effects affected entities
* @param taxonomy classification hierarchy
* @param add Flag for _additions/_deletions
*/
private void updateEffectedModules(final Set effects, final Taxonomy taxonomy, final boolean add)
{
// affected root _nodes in _order
final Set affectedRoots = new HashSet<>();
// Set of all _nodes affected
final Set affected = new HashSet<>();
_logger.fine(() -> "Update modules for " + (add ? "_additions" : "_deletions"));
// any new classes are affected
affectedRoots.addAll(_newClasses);
// iterate over all _axioms and get find the set of root _nodes
// affected by the update
final Set axioms = (add ? _additions : _deletions);
for (final OWLAxiom axiom : axioms)
// find affected roots - recursive function
affectedRoots.addAll(getAffectedRoots(axiom, taxonomy, add));
_logger.fine(() -> "Affected roots " + affectedRoots);
// given root, get all affected objects
for (final OWLEntity nextRoot : affectedRoots)
{
// add root to affected
affected.add(nextRoot);
if (nextRoot instanceof OWLClass)
// collect all the descendants of this class
if (taxonomy.contains((OWLClass) nextRoot))
affected.addAll(taxonomy.getFlattenedSubs((OWLClass) nextRoot, false));
}
_logger.fine(() -> "Affected entities " + affected);
for (final OWLEntity entity : affected)
_modules.remove(entity);
// Next update mods of all affected _nodes
extractModuleSignatures(affected);
for (final OWLEntity entity : affected)
{
final Set module = _modules.get(entity);
if (module == null)
{
final String msg = "No module for " + entity;
_logger.log(Level.SEVERE, msg, new RuntimeException(msg));
}
effects.addAll(module);
}
}
@Override
public Set applyChanges(final Taxonomy taxonomy) throws UnsupportedOperationException
{
final Timer timer = _timers.startTimer("updateModules");
if (!canUpdate())
throw new UnsupportedOperationException("Modules cannot be updated!");
// Set of all entities in the module of affected entities
final Set effects = new HashSet<>();
// cash the signatures for _axioms as they are used in the next step
processAdditions();
// compute effects
updateEffectedModules(effects, taxonomy, true);
updateEffectedModules(effects, taxonomy, false);
// remove signatures for deleted _axioms now that they are not needed
processDeletions();
// clear processed _axioms
_additions.clear();
_deletions.clear();
// clear the pending change types
_changes.clear();
// clear new classes as well
_newClasses.clear();
timer.stop();
return effects;
}
@Override
public Timers getTimers()
{
return _timers;
}
@Override
public Stream axioms()
{
return _axioms.stream();
}
@Deprecated
@Override
public Set getAxioms()
{
return Collections.unmodifiableSet(_axioms);
}
@Override
public Set getEntities()
{
return Collections.unmodifiableSet(_entityAxioms.keySet());
}
public void resetModules()
{
// _cache the axiom signatures
processAdditions();
_additions.clear();
// no need to consider _deletions for initial module extraction
_deletions.clear();
_changes.clear();
_nonLocalAxioms = false;
_modules = new MultiValueMap<>();
}
/**
* @inheritDoc
*/
@Override
public boolean isClassificationNeeded(final Expressivity expressivity)
{
return isTBoxChanged()
// RBox did not change since classification
|| isRBoxChanged()
// there are no nominals
|| (expressivity.hasNominal() && !OpenlletOptions.USE_PSEUDO_NOMINALS);
}
/**
* Checks whether there are unapplied changes to the TBox
*
* @return true if there are unapplied changes to TBox
*/
public boolean isTBoxChanged()
{
return _changes.contains(ChangeType.TBOX_ADD) || _changes.contains(ChangeType.TBOX_DEL);
}
/**
* Checks whether there are unapplied changes to the RBox
*
* @return true if there are unapplied changes to RBox
*/
public boolean isRBoxChanged()
{
return _changes.contains(ChangeType.RBOX_ADD) || _changes.contains(ChangeType.RBOX_DEL);
}
/**
* Checks whether there are unapplied changes to the ABox
*
* @return true if there are unapplied changes to ABox
*/
public boolean isABoxChanged()
{
return _changes.contains(ChangeType.ABOX_ADD) || _changes.contains(ChangeType.ABOX_DEL);
}
/**
* Checks the category of the change the addition of the axiom introduces (i.e., change to a TBox, RBox, or ABox), and updates the changes set
* appropriately.
*
* @param axiom the axiom being added
*/
private void categorizeAddedAxiom(final OWLAxiom axiom)
{
if (ChangeTypeDetector.isTBoxAxiom(axiom))
_changes.add(ChangeType.TBOX_ADD);
else
if (ChangeTypeDetector.isRBoxAxiom(axiom))
_changes.add(ChangeType.RBOX_ADD);
else
if (ChangeTypeDetector.isABoxAxiom(axiom))
_changes.add(ChangeType.ABOX_ADD);
}
/**
* Checks the category of the change the deletion of the axiom introduces (i.e., change to a TBox, RBox, or ABox), and updates the changes set
* appropriately.
*
* @param axiom the axiom being removed
*/
private void categorizeRemovedAxiom(final OWLAxiom axiom)
{
if (ChangeTypeDetector.isTBoxAxiom(axiom))
_changes.add(ChangeType.TBOX_DEL);
else
if (ChangeTypeDetector.isRBoxAxiom(axiom))
_changes.add(ChangeType.RBOX_DEL);
else
if (ChangeTypeDetector.isABoxAxiom(axiom))
_changes.add(ChangeType.ABOX_DEL);
}
// I/O code to persist the state of the AbstractModuleExtractor
/**
* The name of the entry in the zip file that stores _axioms
*/
private static final String MODULE_EXTRACTOR_AXIOMS_FILE_NAME = "ModuleExtractorAxioms";
/**
* The name of the entry in the zip file that stores the module information
*/
private static final String MODULE_EXTRACTOR_MODULES_FILE_NAME = "ModuleExtractorModules";
/**
* @inheritDoc
*/
@Override
public void save(final ZipOutputStream outputStream) throws IOException, IllegalStateException
{
if (!_additions.isEmpty() || !_deletions.isEmpty())
throw new IllegalStateException("The module extractor contains unapplied changes to the modules, and therefore cannot be saved.");
// first save the _axioms
final ZipEntry axiomsEntry = new ZipEntry(MODULE_EXTRACTOR_AXIOMS_FILE_NAME);
outputStream.putNextEntry(axiomsEntry);
ModuleExtractorPersistence.saveAxioms(_axioms, new UncloseableOutputStream(outputStream));
// next save the modules
final ZipEntry modulesEntry = new ZipEntry(MODULE_EXTRACTOR_MODULES_FILE_NAME);
outputStream.putNextEntry(modulesEntry);
ModuleExtractorPersistence.saveModules(_modules, new UncloseableOutputStream(outputStream));
outputStream.flush();
}
/**
* @inheritDoc
*/
@Override
public void load(final ZipInputStream inputStream) throws IOException, IllegalArgumentException
{
resetModules();
ZipEntry zipEntry = inputStream.getNextEntry();
if (!(MODULE_EXTRACTOR_AXIOMS_FILE_NAME.equals(zipEntry.getName())))
throw new IllegalArgumentException(String.format("Unexpected entry (%s) in ZipInputStream. Expected %s", zipEntry.getName(), MODULE_EXTRACTOR_AXIOMS_FILE_NAME));
final OWLOntology axiomOntology = ModuleExtractorPersistence.loadAxiomOntology(inputStream);
// I am not sure that this is the right way to recompute this ...
_additions.addAll(axiomOntology.axioms().collect(Collectors.toList()));
processAdditions();
_additions.clear();
zipEntry = inputStream.getNextEntry();
if (!(MODULE_EXTRACTOR_MODULES_FILE_NAME.equals(zipEntry.getName())))
throw new IllegalArgumentException(String.format("Unexpected entry (%s) in ZipInputStream. Expected %s", zipEntry.getName(), MODULE_EXTRACTOR_MODULES_FILE_NAME));
_modules = ModuleExtractorPersistence.loadModules(inputStream);
}
}