rdf-isomorphic
Version:
Determines if two RDF graphs are isomorphic
353 lines • 14.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.isomorphic = isomorphic;
exports.getBijection = getBijection;
exports.getBijectionInner = getBijectionInner;
exports.hashValues = hashValues;
exports.hasValue = hasValue;
exports.getQuadsWithBlankNodes = getQuadsWithBlankNodes;
exports.getQuadsWithoutBlankNodes = getQuadsWithoutBlankNodes;
exports.indexGraph = indexGraph;
exports.deindexGraph = deindexGraph;
exports.uniqGraph = uniqGraph;
exports.getGraphBlankNodes = getGraphBlankNodes;
exports.hashTerms = hashTerms;
exports.hashTerm = hashTerm;
exports.hashNumber = hashNumber;
exports.quadToSignature = quadToSignature;
exports.termToSignature = termToSignature;
exports.isTermGrounded = isTermGrounded;
const rdf_string_1 = require("rdf-string");
const rdf_terms_1 = require("rdf-terms");
// tslint:disable-next-line:no-var-requires
const MurmurHash3 = require('imurmurhash');
/**
* Determines if the two given graphs are isomorphic.
*
* @param {Quad[]} graphA An array of quads, order is not important.
* @param {Quad[]} graphB An array of quads, order is not important.
* @return {boolean} If the two given graphs are isomorphic.
*/
function isomorphic(graphA, graphB) {
return !!getBijection(graphA, graphB);
}
/**
* Calculate a hash of graphA blank nodes to graphB blank nodes.
* This represents a bijection from graphA's blank nodes to graphB's blank nodes.
*
* @param {Quad[]} graphA An array of quads, order is not important.
* @param {Quad[]} graphB An array of quads, order is not important.
* @return {IBijection} A hash representing a bijection, or null if none could be found.
*/
function getBijection(graphA, graphB) {
// Check if all (non-blanknode-containing) quads in the two graphs are equal.
// We do this by creating a hash-based index for both graphs.
const nonBlankIndexA = indexGraph(getQuadsWithoutBlankNodes(graphA));
const nonBlankIndexB = indexGraph(getQuadsWithoutBlankNodes(graphB));
if (Object.keys(nonBlankIndexA).length !== Object.keys(nonBlankIndexB).length) {
return null;
}
for (const key in nonBlankIndexA) {
if (nonBlankIndexA[key] !== nonBlankIndexB[key]) {
return null;
}
}
// Pre-process data that needs to be present in each iteration of getBijectionInner.
const blankQuadsA = uniqGraph(getQuadsWithBlankNodes(graphA));
const blankQuadsB = uniqGraph(getQuadsWithBlankNodes(graphB));
const blankNodesA = getGraphBlankNodes(graphA);
const blankNodesB = getGraphBlankNodes(graphB);
return getBijectionInner(blankQuadsA, blankQuadsB, blankNodesA, blankNodesB);
}
function getBijectionInner(blankQuadsA, blankQuadsB, blankNodesA, blankNodesB, groundedHashesA, groundedHashesB) {
if (!groundedHashesA) {
groundedHashesA = {};
}
if (!groundedHashesB) {
groundedHashesB = {};
}
// Hash every term based on the signature of the quads if appears in.
const [hashesA, ungroundedHashesA] = hashTerms(blankQuadsA, blankNodesA, groundedHashesA);
const [hashesB, ungroundedHashesB] = hashTerms(blankQuadsB, blankNodesB, groundedHashesB);
// Break quickly if a graph contains a grounded node that is not contained in the other graph.
if (Object.keys(hashesA).length !== Object.keys(hashesB).length) {
return null;
}
for (const hashKeyA in hashesA) {
if (!hasValue(hashesB, hashesA[hashKeyA])) {
return null;
}
}
// Map the blank nodes from graph A to the blank nodes of graph B using the created hashes.
// Grounded hashes will also be equal, but not needed here, we will need them in the next recursion
// (as we only recurse on grounded nodes).
let bijection = {};
for (const blankNodeA of blankNodesA) {
const blankNodeAString = (0, rdf_string_1.termToString)(blankNodeA);
const blankNodeAHash = ungroundedHashesA[blankNodeAString];
for (const blankNodeBString in ungroundedHashesB) {
if (ungroundedHashesB[blankNodeBString] === blankNodeAHash) {
bijection[blankNodeAString] = blankNodeBString;
delete ungroundedHashesB[blankNodeBString];
break;
}
}
}
// Check if all nodes from graph A and B are present in the bijection,
// if not, speculatively mark pairs with matching ungrounded hashes as bijected, and recurse.
if (!arraysEqual(Object.keys(bijection).sort(), blankNodesA.map(rdf_string_1.termToString).sort())
|| !arraysEqual(hashValues(bijection).sort(), blankNodesB.map(rdf_string_1.termToString).sort())) {
// I have not yet been able to find any pathological cases where this code is reached.
// This may be removable, but let's wait until someone proves that.
bijection = null;
for (const blankNodeA of blankNodesA) {
// Only replace ungrounded node hashes
const blankNodeAString = (0, rdf_string_1.termToString)(blankNodeA);
if (!hashesA[blankNodeAString]) {
for (const blankNodeB of blankNodesB) {
// Only replace ungrounded node hashes
const blankNodeBString = (0, rdf_string_1.termToString)(blankNodeB);
if (!hashesB[blankNodeBString]) {
if (ungroundedHashesA[blankNodeAString] === ungroundedHashesB[blankNodeBString]) {
const hash = hashNumber(blankNodeAString);
bijection = getBijectionInner(blankQuadsA, blankQuadsB, blankNodesA, blankNodesB, Object.assign(Object.assign({}, hashesA), { [blankNodeAString]: hash }), Object.assign(Object.assign({}, hashesB), { [blankNodeBString]: hash }));
}
}
}
}
}
}
return bijection;
}
function arraysEqual(array1, array2) {
if (array1.length !== array2.length) {
return false;
}
for (let i = array1.length; i--;) {
if (array1[i] !== array2[i]) {
return false;
}
}
return true;
}
/**
* Get all values from the given hash
* @param hash A hash.
* @return {any[]} The array of values.
*/
function hashValues(hash) {
const arr = [];
for (const e in hash) {
arr.push(hash[e]);
}
return arr;
}
/**
* Check if the given hash contains the given value.
* @param hash A hash.
* @param {string} value A value.
* @return {boolean} If it contains the value.
*/
function hasValue(hash, value) {
for (const hashValue in hash) {
if (hash[hashValue] === value) {
return true;
}
}
return false;
}
/**
* Get all quads with blank nodes.
* @param {Quad[]} graph An array of quads.
* @return {Quad[]} An array of quads with blank nodes
*/
function getQuadsWithBlankNodes(graph) {
return graph.filter((quad) => (0, rdf_terms_1.someTerms)(quad, (value) => {
return value.termType === 'BlankNode'
|| (value.termType === 'Quad' && (0, rdf_terms_1.getTermsNested)(value).some(term => term.termType === 'BlankNode'));
}));
}
/**
* Get all quads without blank nodes.
* @param {Quad[]} graph An array of quads.
* @return {Quad[]} An array of quads without blank nodes
*/
function getQuadsWithoutBlankNodes(graph) {
return graph.filter((quad) => (0, rdf_terms_1.everyTerms)(quad, (value) => {
return value.termType !== 'BlankNode'
&& !(value.termType === 'Quad' && (0, rdf_terms_1.getTermsNested)(value).some(term => term.termType === 'BlankNode'));
}));
}
/**
* Create a hash-based index of the given graph.
* @param {Quad[]} graph An array of quads, the order does not matter.
* @return {{[p: string]: boolean}} A hash-based datastructure representing the graph.
*/
function indexGraph(graph) {
const index = {};
for (const quad of graph) {
index[JSON.stringify((0, rdf_string_1.quadToStringQuad)(quad))] = true;
}
return index;
}
/**
* Create a graph from the given hash-based index.
* @param {{[p: string]: boolean}} indexedGraph A hash-based datastructure representing the graph.
* @return {Quad[]} An array of quads, the order does not matter.
*/
function deindexGraph(indexedGraph) {
return Object.keys(indexedGraph).map((str) => (0, rdf_string_1.stringQuadToQuad)(JSON.parse(str)));
}
/**
* Unique-ify the given RDF graph based on strict equality.
* The output graph will consist of new quad and term instances.
* @param {Quad[]} graph An input graph.
* @return {Quad[]} The input graph without duplicates.
*/
function uniqGraph(graph) {
return deindexGraph(indexGraph(graph));
}
/**
* Find all blank nodes in the given graph.
* @param {Quad[]} graph An array of quads.
* @return {BlankNode[]} A list of (unique) blank nodes.
*/
function getGraphBlankNodes(graph) {
return (0, rdf_terms_1.uniqTerms)(graph.map((quad) => (0, rdf_terms_1.getBlankNodes)((0, rdf_terms_1.getTermsNested)(quad)))
.reduce((acc, val) => acc.concat(val), []));
}
/**
* Create term hashes for the given set of quads and blank node terms.
*
* @param {Quad[]} quads A set of quads.
* @param {Term[]} terms Blank node terms.
* @param {ITermHash} groundedHashes Grounded term hashes that are used to create more specific signatures
* of other terms, because they are based on non-blank nodes and grounded blank nodes.
* @return {[ITermHash]} A tuple of grounded and ungrounded hashes.
*/
function hashTerms(quads, terms, groundedHashes) {
const hashes = Object.assign({}, groundedHashes);
const ungroundedHashes = {};
let hashNeeded = true;
// Iteratively mark nodes as grounded.
// If a node is marked as grounded, then the next iteration can lead to new grounded states
while (hashNeeded) {
const initialGroundedNodesCount = Object.keys(hashes).length;
for (const term of terms) {
const termString = (0, rdf_string_1.termToString)(term);
if (!hashes[termString]) {
const [grounded, hash] = hashTerm(term, quads, hashes);
if (grounded) {
hashes[termString] = hash;
}
ungroundedHashes[termString] = hash;
}
}
// All terms that have a unique hash at this point can be marked as grounded
const uniques = new Map();
for (const termKey in ungroundedHashes) {
const hash = ungroundedHashes[termKey];
if (uniques.get(hash) === undefined) {
uniques.set(hash, termKey);
}
else {
uniques.set(hash, false);
}
}
for (const [hash, value] of uniques.entries()) {
if (value) {
hashes[value] = hash;
}
}
// Check if the loop needs to terminate
hashNeeded = initialGroundedNodesCount !== Object.keys(hashes).length;
}
return [hashes, ungroundedHashes];
}
/**
* Generate a hash for the given term based on the signature of the quads it appears in.
*
* Signatures are made up of grounded terms in quads that are associated with a term,
* i.e., everything except for ungrounded blank nodes.
* The hash is created by hashing a sorted list of each quad's signature,
* where each quad signature is a concatenation of the signature of all grounded terms.
*
* Terms are considered grounded if they are a member in the given hash AND if they are not the given term.
*
* @param {Term} term The term to get the hash around.
* @param {Quad[]} quads The quads to include in the hashing.
* @param {ITermHash} hashes A grounded term hash object.
* @return {[boolean , number]} A tuple indicating if the given term is grounded in all the given quads, and the hash.
*/
function hashTerm(term, quads, hashes) {
const quadSignatures = [];
let grounded = true;
for (const quad of quads) {
const terms = (0, rdf_terms_1.getTermsNested)(quad);
if (terms.some((quadTerm) => quadTerm.equals(term))) {
quadSignatures.push(quadToSignature(quad, hashes, term));
for (const quadTerm of terms) {
if (!isTermGrounded(quadTerm, hashes) && !quadTerm.equals(term)) {
grounded = false;
}
}
}
}
const hash = hashNumber(quadSignatures.sort().join(''));
return [grounded, hash];
}
/**
* Create a number hash.
* @param {string} data Something to hash.
* @return {string} A hash string.
*/
function hashNumber(data) {
return MurmurHash3().hash(data).result();
}
/**
* Convert the given quad to a string signature so that it can be used in the hash structure.
* @param {Quad} quad A quad.
* @param {ITermHash} hashes A grounded term hash object.
* @param {Term} term A target term to compare with.
* @return {string} A string signature.
*/
function quadToSignature(quad, hashes, term) {
return (0, rdf_terms_1.getTerms)(quad).map((quadTerm) => termToSignature(quadTerm, hashes, term)).join('|');
}
/**
* Convert the given term to a string signature so that it can be used in the hash structure.
* @param {Term} term A term.
* @param {ITermHash} hashes A grounded term hash object.
* @param {Term} target A target term to compare with.
* @return {string} A string signature.
*/
function termToSignature(term, hashes, target) {
var _a;
if (term.equals(target)) {
return '@self';
}
else if (term.termType === 'BlankNode') {
return ((_a = hashes[(0, rdf_string_1.termToString)(term)]) === null || _a === void 0 ? void 0 : _a.toString()) || '@blank';
}
else if (term.termType === 'Quad') {
return `<${quadToSignature(term, hashes, target)}>`;
}
else {
return (0, rdf_string_1.termToString)(term);
}
}
/**
* Check if a term is grounded.
*
* A term is grounded if it is not a blank node
* or if it included in the given hash of grounded nodes.
*
* @param {Term} term A term.
* @param {ITermHash} hashes A grounded term hash object.
* @return {boolean} If the given term is grounded.
*/
function isTermGrounded(term, hashes) {
return (term.termType !== 'BlankNode'
&& !(term.termType === 'Quad' && (0, rdf_terms_1.getTermsNested)(term).some(subTerm => !isTermGrounded(subTerm, hashes)))) || !!hashes[(0, rdf_string_1.termToString)(term)];
}
//# sourceMappingURL=RdfIsomorphic.js.map