From 97802233869ad85496505a078dcc4a985ee39721 Mon Sep 17 00:00:00 2001 From: rjawesome Date: Fri, 16 Aug 2024 14:56:24 -0700 Subject: [PATCH 1/3] fix subclassing, self graphs --- src/edge_manager.ts | 24 +++++-- src/index.ts | 163 +++++++++++++++++++++++++------------------- src/types.ts | 6 ++ 3 files changed, 120 insertions(+), 73 deletions(-) diff --git a/src/edge_manager.ts b/src/edge_manager.ts index dabacb86..f801a54d 100644 --- a/src/edge_manager.ts +++ b/src/edge_manager.ts @@ -10,7 +10,7 @@ import QEdge from './query_edge'; import MetaKG from '@biothings-explorer/smartapi-kg'; import { QueryHandlerOptions } from '@biothings-explorer/types'; import { Record } from '@biothings-explorer/api-response-transform'; -import { UnavailableAPITracker } from './types'; +import { SubclassEdges, UnavailableAPITracker } from './types'; import { RecordsByQEdgeID } from './results_assembly/query_results'; import path from 'path'; import { promises as fs } from 'fs'; @@ -22,7 +22,8 @@ export default class QueryEdgeManager { private _records: Record[]; options: QueryHandlerOptions; private _organizedRecords: RecordsByQEdgeID; - constructor(edges: QEdge[], metaKG: MetaKG, options: QueryHandlerOptions) { + private _subclassEdges: SubclassEdges; + constructor(edges: QEdge[], metaKG: MetaKG, subclassEdges: SubclassEdges, options: QueryHandlerOptions) { // flatten list of all edges available this._qEdges = _.flatten(edges); this._metaKG = metaKG; @@ -31,6 +32,7 @@ export default class QueryEdgeManager { //organized by edge with refs to connected edges this._organizedRecords = {}; this.options = options; + this._subclassEdges = subclassEdges; this.init(); } @@ -203,8 +205,22 @@ export default class QueryEdgeManager { records.forEach((record) => { // check against original, primaryID, and equivalent ids - const subjectIDs = [record.subject.original, record.subject.curie, ...record.subject.equivalentCuries]; - const objectIDs = [record.object.original, record.object.curie, ...record.object.equivalentCuries]; + let subjectIDs = [record.subject.original, record.subject.curie, ...record.subject.equivalentCuries]; + let objectIDs = [record.object.original, record.object.curie, ...record.object.equivalentCuries]; + + // check if IDs will be resolved to a parent + subjectIDs = [...subjectIDs, ...subjectIDs.reduce((set, subjectID) => { + Object.entries(this._subclassEdges[subjectID] ?? {}).forEach(([id, qNodes]) => { + if (qNodes.includes(qEdge.reverse ? qEdge.object.id : qEdge.subject.id)) set.add(id); + }); + return set; + }, new Set())]; + objectIDs = [...objectIDs, ...objectIDs.reduce((set, objectIDs) => { + Object.entries(this._subclassEdges[objectIDs] ?? {}).forEach(([id, qNodes]) => { + if (qNodes.includes(qEdge.reverse ? qEdge.subject.id : qEdge.object.id)) set.add(id); + }); + return set; + }, new Set())]; // there must be at least a minimal intersection const subjectMatch = subjectIDs.some((curie) => execSubjectCuries.includes(curie)); diff --git a/src/index.ts b/src/index.ts index 54d752c1..4b3f11f6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,6 +30,7 @@ import BTEGraph from './graph/graph'; import QEdge from './query_edge'; import { Telemetry } from '@biothings-explorer/utils'; import { enrichTrapiResultsWithPfocrFigures } from './results_assembly/pfocr'; +import { SubclassEdges } from './types'; // Exports for external availability export * from './types'; @@ -45,7 +46,7 @@ export default class TRAPIQueryHandler { includeReasoner: boolean; path: string; predicatePath: string; - subclassEdges: { [expandedID: string]: string }; + subclassEdges: SubclassEdges; originalQueryGraph: TrapiQueryGraph; bteGraph: BTEGraph; knowledgeGraph: KnowledgeGraph; @@ -168,14 +169,12 @@ export default class TRAPIQueryHandler { }); // Create subclass edges for nodes that were expanded - const nodesToRebind: { [nodeID: string]: { newNode: string; subclassEdgeID: string } } = {}; + const nodesToRebind: { [nodeID: string]: { [qEdgeID: string]: { newNode: string; subclassEdgeID: string } } } = {}; Object.keys(this.bteGraph.nodes).forEach((nodeID) => { - const subclassCuries = [...(expandedIDsbyPrimaryID[nodeID] ?? [])]?.map((expandedID) => [ - this.subclassEdges[expandedID], - expandedID, - ]); + const subclassCuries = []; + expandedIDsbyPrimaryID[nodeID]?.forEach((expandedID) => Object.keys(this.subclassEdges[expandedID]).forEach((parentID) => subclassCuries.push({ original: parentID, expanded: expandedID }))); if (!subclassCuries.length) return; // Nothing to rebind - subclassCuries.forEach(([original, expanded]) => { + subclassCuries.forEach(({original, expanded}) => { const subject = nodeID; const object = primaryIDsByOriginalID[original]; // Don't keep self-subclass @@ -202,77 +201,101 @@ export default class TRAPIQueryHandler { }, ]); this.bteGraph.edges[subclassEdgeID] = subclassEdge; - nodesToRebind[subject] = { newNode: object, subclassEdgeID }; + if (!nodesToRebind[subject]) nodesToRebind[subject] = {}; + this.subclassEdges[expanded][original].forEach((qNodeID) => nodesToRebind[subject][qNodeID] = { newNode: object, subclassEdgeID }); }); }); // Create new constructed edges and aux graphs for edges that used subclass edges let auxGraphs: { [supportGraphID: string]: TrapiAuxiliaryGraph } = {}; - const edgesToRebind = {}; + const edgesToRebind: { [edgeID: string]: { [originalSubject: string]: { [originalObject: string]: string /* re-bound edge ID */ } } } = {}; const edgesIDsByAuxGraphID = {}; Object.entries(this.bteGraph.edges).forEach(([edgeID, bteEdge]) => { if (edgeID.includes('expanded')) return; - const supportGraph = [edgeID]; - const [subject, object] = [bteEdge.subject, bteEdge.object].map((edgeNodeID) => { - if (!nodesToRebind[edgeNodeID]) { - return edgeNodeID; // nothing to rebind + const combos: {subject: string, object: string, supportGraph: string[]}[] = []; + const subjectToSupportGraphs: {[sbj: string]: Set} = { + [bteEdge.subject]: new Set(), + ...Object.values(nodesToRebind[bteEdge.subject] ?? {}).reduce((acc, x) => { + x.newNode in acc ? acc[x.newNode].add(x.subclassEdgeID) : acc[x.newNode] = new Set([x.subclassEdgeID]) + return acc; + }, {}) + }; + const objectToSupportGraphs: {[obj: string]: Set} = { + [bteEdge.object]: new Set(), + ...Object.values(nodesToRebind[bteEdge.object] ?? {}).reduce((acc, x) => { + x.newNode in acc ? acc[x.newNode].add(x.subclassEdgeID) : acc[x.newNode] = new Set([x.subclassEdgeID]); + return acc; + }, {}) + }; + for (const subject in subjectToSupportGraphs) { + for (const object in objectToSupportGraphs) { + if (subject == bteEdge.subject && object == bteEdge.object) continue; // no nodes are rebound + combos.push({ subject, object, supportGraph: [...subjectToSupportGraphs[subject], ...objectToSupportGraphs[object], edgeID] }); } - supportGraph.push(nodesToRebind[edgeNodeID].subclassEdgeID); - return nodesToRebind[edgeNodeID].newNode; - }); - - if (supportGraph.length === 1) return; // no subclasses - const boundEdgeID = `${subject}-${bteEdge.predicate.replace('biolink:', '')}-${object}-via_subclass`; - let suffix = 0; - while (Object.keys(auxGraphs).includes(`support${suffix}-${boundEdgeID}`)) { - suffix += 1; - } - const supportGraphID = `support${suffix}-${boundEdgeID}`; - auxGraphs[supportGraphID] = { edges: supportGraph, attributes: [] }; - if (!edgesIDsByAuxGraphID[supportGraphID]) { - edgesIDsByAuxGraphID[supportGraphID] = new Set(); } - edgesIDsByAuxGraphID[supportGraphID].add(boundEdgeID); - if (!this.bteGraph.edges[boundEdgeID]) { - const boundEdge = new KGEdge(boundEdgeID, { - predicate: bteEdge.predicate, - subject: subject, - object: object, - }); - boundEdge.addAdditionalAttributes('biolink:support_graphs', [supportGraphID]); - boundEdge.addAdditionalAttributes('biolink:knowledge_level', 'logical_entailment') - boundEdge.addAdditionalAttributes('biolink:agent_type', 'automated_agent') - boundEdge.addSource([ - { - resource_id: this.options.provenanceUsesServiceProvider - ? 'infores:service-provider-trapi' - : 'infores:biothings-explorer', - resource_role: 'primary_knowledge_source', - }, - ]); - this.bteGraph.edges[boundEdgeID] = boundEdge; - } else { - (this.bteGraph.edges[boundEdgeID].attributes['biolink:support_graphs'] as Set).add(supportGraphID); - } - edgesToRebind[edgeID] = boundEdgeID; + + combos.forEach(({subject, object, supportGraph}) => { + const boundEdgeID = `${subject}-${bteEdge.predicate.replace('biolink:', '')}-${object}-via_subclass`; + let suffix = 0; + while (Object.keys(auxGraphs).includes(`support${suffix}-${boundEdgeID}`)) { + suffix += 1; + } + const supportGraphID = `support${suffix}-${boundEdgeID}`; + auxGraphs[supportGraphID] = { edges: supportGraph, attributes: [] }; + if (!edgesIDsByAuxGraphID[supportGraphID]) { + edgesIDsByAuxGraphID[supportGraphID] = new Set(); + } + edgesIDsByAuxGraphID[supportGraphID].add(boundEdgeID); + if (!this.bteGraph.edges[boundEdgeID]) { + const boundEdge = new KGEdge(boundEdgeID, { + predicate: bteEdge.predicate, + subject: subject, + object: object, + }); + boundEdge.addAdditionalAttributes('biolink:support_graphs', [supportGraphID]); + boundEdge.addAdditionalAttributes('biolink:knowledge_level', 'logical_entailment') + boundEdge.addAdditionalAttributes('biolink:agent_type', 'automated_agent') + boundEdge.addSource([ + { + resource_id: this.options.provenanceUsesServiceProvider + ? 'infores:service-provider-trapi' + : 'infores:biothings-explorer', + resource_role: 'primary_knowledge_source', + }, + ]); + this.bteGraph.edges[boundEdgeID] = boundEdge; + } else { + (this.bteGraph.edges[boundEdgeID].attributes['biolink:support_graphs'] as Set).add(supportGraphID); + } + if (!edgesToRebind[edgeID]) edgesToRebind[edgeID] = {}; + if (!edgesToRebind[edgeID][subject]) edgesToRebind[edgeID][subject] = {}; + edgesToRebind[edgeID][subject][object] = boundEdgeID; + }) }); const resultBoundEdgesWithAuxGraphs = new Set(); const fixedResults = this.trapiResultsAssembler.getResults().map((result) => { - result.node_bindings = Object.fromEntries( - Object.entries(result.node_bindings).map(([qNodeID, bindings]) => { + result.analyses[0].edge_bindings = Object.fromEntries( + Object.entries(result.analyses[0].edge_bindings).map(([qEdgeID, bindings]) => { + const subQNode = this.queryGraph.edges[qEdgeID].subject; + const objQNode = this.queryGraph.edges[qEdgeID].object; return [ - qNodeID, + qEdgeID, bindings.reduce( ({ boundIDs, newBindings }, binding) => { - if (!nodesToRebind[binding.id]) { + const originalSub = this.bteGraph.edges[binding.id].subject; + const originalObj = this.bteGraph.edges[binding.id].object; + const subId = nodesToRebind[originalSub]?.[subQNode]?.newNode ?? originalSub; + const objId = nodesToRebind[originalObj]?.[objQNode]?.newNode ?? originalObj; + if (!edgesToRebind[binding.id]?.[subId]?.[objId]) { if (!boundIDs.has(binding.id)) { newBindings.push(binding); boundIDs.add(binding.id); } - } else if (!boundIDs.has(nodesToRebind[binding.id].newNode)) { - newBindings.push({ id: nodesToRebind[binding.id].newNode, attributes: [] }); - boundIDs.add(nodesToRebind[binding.id].newNode); + } else if (!boundIDs.has(edgesToRebind[binding.id]?.[subId]?.[objId])) { + newBindings.push({ id: edgesToRebind[binding.id]?.[subId]?.[objId], attributes: [] }); + boundIDs.add(edgesToRebind[binding.id]?.[subId]?.[objId]); + resultBoundEdgesWithAuxGraphs.add(edgesToRebind[binding.id]?.[subId]?.[objId]); } return { boundIDs, newBindings }; }, @@ -281,21 +304,21 @@ export default class TRAPIQueryHandler { ]; }), ); - result.analyses[0].edge_bindings = Object.fromEntries( - Object.entries(result.analyses[0].edge_bindings).map(([qEdgeID, bindings]) => { + + result.node_bindings = Object.fromEntries( + Object.entries(result.node_bindings).map(([qNodeID, bindings]) => { return [ - qEdgeID, + qNodeID, bindings.reduce( ({ boundIDs, newBindings }, binding) => { - if (!edgesToRebind[binding.id]) { + if (!nodesToRebind[binding.id]?.[qNodeID]) { if (!boundIDs.has(binding.id)) { newBindings.push(binding); boundIDs.add(binding.id); } - } else if (!boundIDs.has(edgesToRebind[binding.id])) { - newBindings.push({ id: edgesToRebind[binding.id], attributes: [] }); - boundIDs.add(edgesToRebind[binding.id]); - resultBoundEdgesWithAuxGraphs.add(edgesToRebind[binding.id]); + } else if (!boundIDs.has(nodesToRebind[binding.id][qNodeID].newNode)) { + newBindings.push({ id: nodesToRebind[binding.id][qNodeID].newNode, attributes: [] }); + boundIDs.add(nodesToRebind[binding.id][qNodeID].newNode); } return { boundIDs, newBindings }; }, @@ -304,6 +327,7 @@ export default class TRAPIQueryHandler { ]; }), ); + return result; }); @@ -403,7 +427,9 @@ export default class TRAPIQueryHandler { Object.entries(descendantsByCurie).forEach(([curie, descendants]) => { descendants.forEach((descendant) => { if (queryGraph.nodes[nodeId].ids.includes(descendant)) return; - this.subclassEdges[descendant] = curie; + if (!this.subclassEdges[descendant]) this.subclassEdges[descendant] = {}; + if (!this.subclassEdges[descendant][curie]) this.subclassEdges[descendant][curie] = []; + this.subclassEdges[descendant][curie].push(nodeId); }); }); } @@ -440,7 +466,6 @@ export default class TRAPIQueryHandler { if (err instanceof InvalidQueryGraphError || err instanceof SRINodeNormFailure) { throw err; } else { - console.log(err.stack); throw new InvalidQueryGraphError(); } } @@ -455,7 +480,7 @@ export default class TRAPIQueryHandler { // _.cloneDeep() is resource-intensive but only runs once per query qEdges = _.cloneDeep(qEdges); - const manager = new EdgeManager(qEdges, metaKG, this.options); + const manager = new EdgeManager(qEdges, metaKG, this.subclassEdges, this.options); const qEdgesMissingOps: { [qEdgeID: string]: boolean } = {}; while (manager.getEdgesNotExecuted()) { const currentQEdge = manager.getNext(); @@ -693,7 +718,7 @@ export default class TRAPIQueryHandler { if (!(await this._edgesSupported(queryEdges, metaKG))) { return; } - const manager = new EdgeManager(queryEdges, metaKG, this.options); + const manager = new EdgeManager(queryEdges, metaKG, this.subclassEdges, this.options); const executionSuccess = await manager.executeEdges(); this.logs = [...this.logs, ...manager.logs]; diff --git a/src/types.ts b/src/types.ts index 21388766..d00065a5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,3 +7,9 @@ export interface UnavailableAPITracker { export interface CompactQualifiers { [qualifier_type_id: string]: string; } + +export interface SubclassEdges { + [expandedID: string]: { + [parentID: string]: string[] /* QNode IDs */ + } +} \ No newline at end of file From 7d5d1db86983f78f78bf06342b851c1552d1da03 Mon Sep 17 00:00:00 2001 From: rjawesome Date: Fri, 16 Aug 2024 14:58:24 -0700 Subject: [PATCH 2/3] fix duplciate variable name --- src/edge_manager.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/edge_manager.ts b/src/edge_manager.ts index f801a54d..92a7a74b 100644 --- a/src/edge_manager.ts +++ b/src/edge_manager.ts @@ -215,8 +215,8 @@ export default class QueryEdgeManager { }); return set; }, new Set())]; - objectIDs = [...objectIDs, ...objectIDs.reduce((set, objectIDs) => { - Object.entries(this._subclassEdges[objectIDs] ?? {}).forEach(([id, qNodes]) => { + objectIDs = [...objectIDs, ...objectIDs.reduce((set, objectID) => { + Object.entries(this._subclassEdges[objectID] ?? {}).forEach(([id, qNodes]) => { if (qNodes.includes(qEdge.reverse ? qEdge.subject.id : qEdge.object.id)) set.add(id); }); return set; From 05d7b4635ea39e832d64192a21fde82a20580ed8 Mon Sep 17 00:00:00 2001 From: tokebe <43009413+tokebe@users.noreply.github.com> Date: Wed, 21 Aug 2024 14:59:37 -0400 Subject: [PATCH 3/3] fix: use sources passed from node expansion --- src/edge_manager.ts | 48 +++++++++++--------- src/index.ts | 107 ++++++++++++++++++++++++++------------------ src/types.ts | 11 +++-- 3 files changed, 98 insertions(+), 68 deletions(-) diff --git a/src/edge_manager.ts b/src/edge_manager.ts index 92a7a74b..79f00dcf 100644 --- a/src/edge_manager.ts +++ b/src/edge_manager.ts @@ -110,7 +110,7 @@ export default class QueryEdgeManager { } debug( `(5) Sending next edge '${nextQEdge.getID()}' ` + - `WITH entity count...(${nextQEdge.subject.entity_count || nextQEdge.object.entity_count})`, + `WITH entity count...(${nextQEdge.subject.entity_count || nextQEdge.object.entity_count})`, ); return this.preSendOffCheck(nextQEdge); } @@ -119,9 +119,9 @@ export default class QueryEdgeManager { this._qEdges.forEach((qEdge) => { debug( `'${qEdge.getID()}'` + - ` : (${qEdge.subject.entity_count || 0}) ` + - `${qEdge.reverse ? '<--' : '-->'}` + - ` (${qEdge.object.entity_count || 0})`, + ` : (${qEdge.subject.entity_count || 0}) ` + + `${qEdge.reverse ? '<--' : '-->'}` + + ` (${qEdge.object.entity_count || 0})`, ); }); } @@ -129,8 +129,9 @@ export default class QueryEdgeManager { _logSkippedQueries(unavailableAPIs: UnavailableAPITracker): void { Object.entries(unavailableAPIs).forEach(([api, { skippedQueries }]) => { if (skippedQueries > 0) { - const skipMessage = `${skippedQueries} additional quer${skippedQueries > 1 ? 'ies' : 'y'} to ${api} ${skippedQueries > 1 ? 'were' : 'was' - } skipped as the API was unavailable.`; + const skipMessage = `${skippedQueries} additional quer${skippedQueries > 1 ? 'ies' : 'y'} to ${api} ${ + skippedQueries > 1 ? 'were' : 'was' + } skipped as the API was unavailable.`; debug(skipMessage); this.logs.push(new LogEntry('WARNING', null, skipMessage).getLog()); } @@ -196,7 +197,7 @@ export default class QueryEdgeManager { const objectCuries = qEdge.object.curie; debug( `'${qEdge.getID()}' Reversed[${qEdge.reverse}] (${JSON.stringify(subjectCuries.length || 0)})` + - `--(${JSON.stringify(objectCuries.length || 0)}) entities / (${records.length}) records.`, + `--(${JSON.stringify(objectCuries.length || 0)}) entities / (${records.length}) records.`, ); // debug(`IDS SUB ${JSON.stringify(sub_count)}`) // debug(`IDS OBJ ${JSON.stringify(obj_count)}`) @@ -209,18 +210,24 @@ export default class QueryEdgeManager { let objectIDs = [record.object.original, record.object.curie, ...record.object.equivalentCuries]; // check if IDs will be resolved to a parent - subjectIDs = [...subjectIDs, ...subjectIDs.reduce((set, subjectID) => { - Object.entries(this._subclassEdges[subjectID] ?? {}).forEach(([id, qNodes]) => { - if (qNodes.includes(qEdge.reverse ? qEdge.object.id : qEdge.subject.id)) set.add(id); - }); - return set; - }, new Set())]; - objectIDs = [...objectIDs, ...objectIDs.reduce((set, objectID) => { - Object.entries(this._subclassEdges[objectID] ?? {}).forEach(([id, qNodes]) => { - if (qNodes.includes(qEdge.reverse ? qEdge.subject.id : qEdge.object.id)) set.add(id); - }); - return set; - }, new Set())]; + subjectIDs = [ + ...subjectIDs, + ...subjectIDs.reduce((set, subjectID) => { + Object.entries(this._subclassEdges[subjectID] ?? {}).forEach(([id, { qNodes }]) => { + if (qNodes.includes(qEdge.reverse ? qEdge.object.id : qEdge.subject.id)) set.add(id); + }); + return set; + }, new Set()), + ]; + objectIDs = [ + ...objectIDs, + ...objectIDs.reduce((set, objectID) => { + Object.entries(this._subclassEdges[objectID] ?? {}).forEach(([id, { qNodes }]) => { + if (qNodes.includes(qEdge.reverse ? qEdge.subject.id : qEdge.object.id)) set.add(id); + }); + return set; + }, new Set()), + ]; // there must be at least a minimal intersection const subjectMatch = subjectIDs.some((curie) => execSubjectCuries.includes(curie)); @@ -409,7 +416,8 @@ export default class QueryEdgeManager { new LogEntry( 'INFO', null, - `Executing ${currentQEdge.getID()}${currentQEdge.isReversed() ? ' (reversed)' : ''}: ${currentQEdge.subject.id + `Executing ${currentQEdge.getID()}${currentQEdge.isReversed() ? ' (reversed)' : ''}: ${ + currentQEdge.subject.id } ${currentQEdge.isReversed() ? '<--' : '-->'} ${currentQEdge.object.id}`, ).getLog(), ); diff --git a/src/index.ts b/src/index.ts index 4b3f11f6..71cf9e50 100644 --- a/src/index.ts +++ b/src/index.ts @@ -79,7 +79,7 @@ export default class TRAPIQueryHandler { if (this.options.smartapi) { smartapiRegistry = this.options.smartapi; } else { - const file = await fs.readFile(this.path, "utf-8"); + const file = await fs.readFile(this.path, 'utf-8'); smartapiRegistry = JSON.parse(file); } @@ -109,8 +109,8 @@ export default class TRAPIQueryHandler { `Query options are: ${JSON.stringify({ ...this.options, schema: this.options.schema ? this.options.schema.info.version : 'not included', - metakg: "", - smartapi: "" + metakg: '', + smartapi: '', })}`, ); @@ -172,9 +172,13 @@ export default class TRAPIQueryHandler { const nodesToRebind: { [nodeID: string]: { [qEdgeID: string]: { newNode: string; subclassEdgeID: string } } } = {}; Object.keys(this.bteGraph.nodes).forEach((nodeID) => { const subclassCuries = []; - expandedIDsbyPrimaryID[nodeID]?.forEach((expandedID) => Object.keys(this.subclassEdges[expandedID]).forEach((parentID) => subclassCuries.push({ original: parentID, expanded: expandedID }))); + expandedIDsbyPrimaryID[nodeID]?.forEach((expandedID) => + Object.keys(this.subclassEdges[expandedID]).forEach((parentID) => + subclassCuries.push({ original: parentID, expanded: expandedID }), + ), + ); if (!subclassCuries.length) return; // Nothing to rebind - subclassCuries.forEach(({original, expanded}) => { + subclassCuries.forEach(({ original, expanded }) => { const subject = nodeID; const object = primaryIDsByOriginalID[original]; // Don't keep self-subclass @@ -186,11 +190,9 @@ export default class TRAPIQueryHandler { subject, object, }); - const source = Object.entries(ontologyKnowledgeSourceMapping).find(([prefix]) => { - return expanded.includes(prefix); - })[1]; - subclassEdge.addAdditionalAttributes('biolink:knowledge_level', 'knowledge_assertion') - subclassEdge.addAdditionalAttributes('biolink:agent_type', 'manual_agent') + const source = ontologyKnowledgeSourceMapping[this.subclassEdges[expanded][original].source] ?? 'error-not-provided'; + subclassEdge.addAdditionalAttributes('biolink:knowledge_level', 'knowledge_assertion'); + subclassEdge.addAdditionalAttributes('biolink:agent_type', 'manual_agent'); subclassEdge.addSource([ { resource_id: source, resource_role: 'primary_knowledge_source' }, { @@ -202,39 +204,47 @@ export default class TRAPIQueryHandler { ]); this.bteGraph.edges[subclassEdgeID] = subclassEdge; if (!nodesToRebind[subject]) nodesToRebind[subject] = {}; - this.subclassEdges[expanded][original].forEach((qNodeID) => nodesToRebind[subject][qNodeID] = { newNode: object, subclassEdgeID }); + this.subclassEdges[expanded][original].qNodes.forEach( + (qNodeID) => (nodesToRebind[subject][qNodeID] = { newNode: object, subclassEdgeID }), + ); }); }); // Create new constructed edges and aux graphs for edges that used subclass edges let auxGraphs: { [supportGraphID: string]: TrapiAuxiliaryGraph } = {}; - const edgesToRebind: { [edgeID: string]: { [originalSubject: string]: { [originalObject: string]: string /* re-bound edge ID */ } } } = {}; + const edgesToRebind: { + [edgeID: string]: { [originalSubject: string]: { [originalObject: string]: string /* re-bound edge ID */ } }; + } = {}; const edgesIDsByAuxGraphID = {}; Object.entries(this.bteGraph.edges).forEach(([edgeID, bteEdge]) => { if (edgeID.includes('expanded')) return; - const combos: {subject: string, object: string, supportGraph: string[]}[] = []; - const subjectToSupportGraphs: {[sbj: string]: Set} = { - [bteEdge.subject]: new Set(), + const combos: { subject: string; object: string; supportGraph: string[] }[] = []; + const subjectToSupportGraphs: { [sbj: string]: Set } = { + [bteEdge.subject]: new Set(), ...Object.values(nodesToRebind[bteEdge.subject] ?? {}).reduce((acc, x) => { - x.newNode in acc ? acc[x.newNode].add(x.subclassEdgeID) : acc[x.newNode] = new Set([x.subclassEdgeID]) + x.newNode in acc ? acc[x.newNode].add(x.subclassEdgeID) : (acc[x.newNode] = new Set([x.subclassEdgeID])); return acc; - }, {}) + }, {}), }; - const objectToSupportGraphs: {[obj: string]: Set} = { - [bteEdge.object]: new Set(), + const objectToSupportGraphs: { [obj: string]: Set } = { + [bteEdge.object]: new Set(), ...Object.values(nodesToRebind[bteEdge.object] ?? {}).reduce((acc, x) => { - x.newNode in acc ? acc[x.newNode].add(x.subclassEdgeID) : acc[x.newNode] = new Set([x.subclassEdgeID]); + x.newNode in acc ? acc[x.newNode].add(x.subclassEdgeID) : (acc[x.newNode] = new Set([x.subclassEdgeID])); return acc; - }, {}) + }, {}), }; for (const subject in subjectToSupportGraphs) { for (const object in objectToSupportGraphs) { if (subject == bteEdge.subject && object == bteEdge.object) continue; // no nodes are rebound - combos.push({ subject, object, supportGraph: [...subjectToSupportGraphs[subject], ...objectToSupportGraphs[object], edgeID] }); + combos.push({ + subject, + object, + supportGraph: [...subjectToSupportGraphs[subject], ...objectToSupportGraphs[object], edgeID], + }); } } - combos.forEach(({subject, object, supportGraph}) => { + combos.forEach(({ subject, object, supportGraph }) => { const boundEdgeID = `${subject}-${bteEdge.predicate.replace('biolink:', '')}-${object}-via_subclass`; let suffix = 0; while (Object.keys(auxGraphs).includes(`support${suffix}-${boundEdgeID}`)) { @@ -253,8 +263,8 @@ export default class TRAPIQueryHandler { object: object, }); boundEdge.addAdditionalAttributes('biolink:support_graphs', [supportGraphID]); - boundEdge.addAdditionalAttributes('biolink:knowledge_level', 'logical_entailment') - boundEdge.addAdditionalAttributes('biolink:agent_type', 'automated_agent') + boundEdge.addAdditionalAttributes('biolink:knowledge_level', 'logical_entailment'); + boundEdge.addAdditionalAttributes('biolink:agent_type', 'automated_agent'); boundEdge.addSource([ { resource_id: this.options.provenanceUsesServiceProvider @@ -270,7 +280,7 @@ export default class TRAPIQueryHandler { if (!edgesToRebind[edgeID]) edgesToRebind[edgeID] = {}; if (!edgesToRebind[edgeID][subject]) edgesToRebind[edgeID][subject] = {}; edgesToRebind[edgeID][subject][object] = boundEdgeID; - }) + }); }); const resultBoundEdgesWithAuxGraphs = new Set(); @@ -343,15 +353,18 @@ export default class TRAPIQueryHandler { } appendOriginalCuriesToResults(results: TrapiResult[]): void { - results.forEach(result => { + results.forEach((result) => { Object.entries(result.node_bindings).forEach(([_, bindings]) => { - bindings.forEach(binding => { - if (this.bteGraph.nodes[binding.id].originalCurie && this.bteGraph.nodes[binding.id].originalCurie !== binding.id) { + bindings.forEach((binding) => { + if ( + this.bteGraph.nodes[binding.id].originalCurie && + this.bteGraph.nodes[binding.id].originalCurie !== binding.id + ) { binding.query_id = this.bteGraph.nodes[binding.id].originalCurie; } - }) - }) - }) + }); + }); + }); } async addQueryNodes(): Promise { @@ -412,8 +425,10 @@ export default class TRAPIQueryHandler { for (const nodeId in queryGraph.nodes) { // perform node expansion if (queryGraph.nodes[nodeId].ids && !this._queryUsesInferredMode()) { - const descendantsByCurie: { [curie: string]: string[] } = getDescendants(queryGraph.nodes[nodeId].ids); - let expanded = Object.values(descendantsByCurie).flat(); + const descendantsByCurie: { [curie: string]: { [descendants: string]: string } } = getDescendants( + queryGraph.nodes[nodeId].ids, + ); + let expanded = Object.values(descendantsByCurie).map(descendants => Object.keys(descendants)).flat() expanded = _.uniq([...queryGraph.nodes[nodeId].ids, ...expanded]); @@ -425,11 +440,11 @@ export default class TRAPIQueryHandler { if (foundExpandedIds) { Object.entries(descendantsByCurie).forEach(([curie, descendants]) => { - descendants.forEach((descendant) => { + Object.entries(descendants).forEach(([ descendant, source ]) => { if (queryGraph.nodes[nodeId].ids.includes(descendant)) return; if (!this.subclassEdges[descendant]) this.subclassEdges[descendant] = {}; - if (!this.subclassEdges[descendant][curie]) this.subclassEdges[descendant][curie] = []; - this.subclassEdges[descendant][curie].push(nodeId); + if (!this.subclassEdges[descendant][curie]) this.subclassEdges[descendant][curie] = { source, qNodes: [] }; + this.subclassEdges[descendant][curie].qNodes.push(nodeId); }); }); } @@ -492,11 +507,13 @@ export default class TRAPIQueryHandler { let log_msg: string; if (currentQEdge.reverse) { - log_msg = `qEdge ${currentQEdge.id} (reversed): ${currentQEdge.object.categories} > ${currentQEdge.predicate ? `${currentQEdge.predicate} > ` : '' - }${currentQEdge.subject.categories}`; + log_msg = `qEdge ${currentQEdge.id} (reversed): ${currentQEdge.object.categories} > ${ + currentQEdge.predicate ? `${currentQEdge.predicate} > ` : '' + }${currentQEdge.subject.categories}`; } else { - log_msg = `qEdge ${currentQEdge.id}: ${currentQEdge.subject.categories} > ${currentQEdge.predicate ? `${currentQEdge.predicate} > ` : '' - }${currentQEdge.object.categories}`; + log_msg = `qEdge ${currentQEdge.id}: ${currentQEdge.subject.categories} > ${ + currentQEdge.predicate ? `${currentQEdge.predicate} > ` : '' + }${currentQEdge.object.categories}`; } this.logs.push(new LogEntry('INFO', null, log_msg).getLog()); @@ -537,8 +554,9 @@ export default class TRAPIQueryHandler { }); const qEdgesLogStr = qEdgesToLog.length > 1 ? `[${qEdgesToLog.join(', ')}]` : `${qEdgesToLog.join(', ')}`; if (len > 0) { - const terminateLog = `Query Edge${len !== 1 ? 's' : ''} ${qEdgesLogStr} ${len !== 1 ? 'have' : 'has' - } no MetaKG edges. Your query terminates.`; + const terminateLog = `Query Edge${len !== 1 ? 's' : ''} ${qEdgesLogStr} ${ + len !== 1 ? 'have' : 'has' + } no MetaKG edges. Your query terminates.`; debug(terminateLog); this.logs.push(new LogEntry('WARNING', null, terminateLog).getLog()); return false; @@ -652,7 +670,8 @@ export default class TRAPIQueryHandler { new LogEntry( 'INFO', null, - `Execution Summary: (${KGNodes}) nodes / (${kgEdges}) edges / (${results}) results; (${resultQueries}/${queries}) queries${cached ? ` (${cached} cached qEdges)` : '' + `Execution Summary: (${KGNodes}) nodes / (${kgEdges}) edges / (${results}) results; (${resultQueries}/${queries}) queries${ + cached ? ` (${cached} cached qEdges)` : '' } returned results from(${sources.length}) unique API${sources.length === 1 ? 's' : ''}`, ).getLog(), new LogEntry('INFO', null, `APIs: ${sources.join(', ')} `).getLog(), diff --git a/src/types.ts b/src/types.ts index d00065a5..60d5fbf8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -9,7 +9,10 @@ export interface CompactQualifiers { } export interface SubclassEdges { - [expandedID: string]: { - [parentID: string]: string[] /* QNode IDs */ - } -} \ No newline at end of file + [expandedID: string]: { + [parentID: string]: { + source: string; + qNodes: string[]; + }; + }; +}