Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Edge/Node constraints #194

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions __test__/integration/QueryEdge.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import QNode from '../../src/query_node';
import QEdge from '../../src/query_edge';
import KGNode from '../../src/graph/kg_node';
import KGEdge from '../../src/graph/kg_edge';
import KnowledgeGraph from '../../src/graph/knowledge_graph';

describe('Testing QueryEdge Module', () => {
const gene_node1 = new QNode({ id: 'n1', categories: ['Gene'], ids: ['NCBIGene:1017'] });
Expand Down Expand Up @@ -197,4 +200,20 @@ describe('Testing QueryEdge Module', () => {
// NOTE: recently changed from not.toEqual, because an unfrozen edge *should* equal its original?
expect(qEdge1.getHashedEdgeRepresentation()).toEqual(qEdge2.getHashedEdgeRepresentation());
});

test('meetsConstraints', () => {
const qEdge = new QEdge({ id: 'e01', subject: type_node, object: disease1_node, predicates: ['biolink:treats'], attribute_constraints: [{ name: 'publications', id: 'biolink:publications', operator: '==', value: 'PMID:9248614', not: false }] });
const kgNode1 = new KGNode("node1", { label: "node1", semanticType: [], names: [], curies: [], primaryCurie: "node1", qNodeID: "e01"});
const kgNode2 = new KGNode("node2", { label: "node2", semanticType: [], names: [], curies: [], primaryCurie: "node2", qNodeID: "e01"});
const kgEdge1 = new KGEdge("edge1", {object: "node1", subject: "node2", predicate: "biolink:treats"});
const kgEdge2 = new KGEdge("edge2", {object: "node1", subject: "node2", predicate: "biolink:treats"});
kgEdge1.addPublication("PMID:9248614");
kgEdge1.addPublication("PMID:1234567");
kgEdge2.addPublication("PMID:7614243");
kgEdge2.addPublication("PMID:1234567");
const graph = new KnowledgeGraph();
graph.update({ nodes: { node1: kgNode1, node2: kgNode2 }, edges: { edge1: kgEdge1, edge2: kgEdge2 } });
expect(qEdge.meetsConstraints(graph.edges["edge1"], graph.nodes["node1"], graph.nodes["node2"])).toBeTruthy();
expect(qEdge.meetsConstraints(graph.edges["edge2"], graph.nodes["node1"], graph.nodes["node2"])).toBeFalsy();
})
});
3 changes: 2 additions & 1 deletion __test__/unittest/TRAPIQueryHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ describe('test TRAPIQueryHandler methods', () => {
edges: {},
};
try {
await handler._processQueryGraph(invalidQueryGraph);
handler.setQueryGraph(invalidQueryGraph);
await handler._processQueryGraph();
} catch (err) {
expect(err).toBeInstanceOf(InvalidQueryGraphError);
}
Expand Down
34 changes: 32 additions & 2 deletions src/edge_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { SubclassEdges, UnavailableAPITracker } from './types';
import { RecordsByQEdgeID } from './results_assembly/query_results';
import path from 'path';
import { promises as fs } from 'fs';
import KnowledgeGraph from './graph/knowledge_graph';
import BTEGraph from './graph/graph';

export default class QueryEdgeManager {
private _qEdges: QEdge[];
Expand Down Expand Up @@ -250,6 +252,32 @@ export default class QueryEdgeManager {
return keep;
}

_constrainEdgeRecords(qEdge: QEdge, records: Record[]) {
const keep: Record[] = [];
const bte = new BTEGraph();
const kg = new KnowledgeGraph();
bte.update(records);
kg.update(bte);
records.forEach(record => {
const edge = kg.kg.edges[record.recordHash];
const sub = qEdge.reverse ? kg.kg.nodes[edge.object] : kg.kg.nodes[edge.subject];
const obj = qEdge.reverse ? kg.kg.nodes[edge.subject] : kg.kg.nodes[edge.object];
if (qEdge.meetsConstraints(edge, sub, obj)) {
keep.push(record);
}
});

debug(`'${qEdge.getID()}' dropped (${records.length - keep.length}) records based on edge/node constraints.`);
this.logs.push(
new LogEntry(
'DEBUG',
null,
`'${qEdge.getID()}' kept (${keep.length}) / dropped (${records.length - keep.length}) records (based on node/edge constraints).`,
).getLog(),
);
return keep;
}

collectRecords(): boolean {
//go through edges and collect records organized by edge
let recordsByQEdgeID: RecordsByQEdgeID = {};
Expand Down Expand Up @@ -316,8 +344,10 @@ export default class QueryEdgeManager {

updateEdgeRecords(currentQEdge: QEdge): void {
//1. filter edge records based on current status
const filteredRecords = this._filterEdgeRecords(currentQEdge);
//2.trigger node update / entity update based on new status
let filteredRecords = this._filterEdgeRecords(currentQEdge);
//2. make sure node/edge constraints are met
filteredRecords = this._constrainEdgeRecords(currentQEdge, filteredRecords);
//3. trigger node update / entity update based on new status
currentQEdge.storeRecords(filteredRecords);
}

Expand Down
3 changes: 3 additions & 0 deletions src/graph/knowledge_graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
TrapiKGNodes,
TrapiQualifier,
TrapiSource,
TrapiAttributeConstraint,
} from '@biothings-explorer/types';
import KGNode from './kg_node';
import KGEdge from './kg_edge';
Expand Down Expand Up @@ -199,6 +200,8 @@ export default class KnowledgeGraph {
}

update(bteGraph: BTEGraphUpdate): void {
this.nodes = {};
this.edges = {};
Object.keys(bteGraph.nodes).map((node) => {
this.nodes[bteGraph.nodes[node].primaryCurie] = this._createNode(bteGraph.nodes[node]);
});
Expand Down
77 changes: 64 additions & 13 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export default class TRAPIQueryHandler {
auxGraphs: TrapiAuxGraphCollection;
finalizedResults: TrapiResult[];
queryGraph: TrapiQueryGraph;
queryGraphHandler: QueryGraph;
constructor(
options: QueryHandlerOptions = {},
smartAPIPath: string = undefined,
Expand Down Expand Up @@ -219,6 +220,9 @@ export default class TRAPIQueryHandler {
[edgeID: string]: { [originalSubject: string]: { [originalObject: string]: string /* re-bound edge ID */ } };
} = {};
const edgesIDsByAuxGraphID = {};
const supportGraphsByEdgeID: {
[edgeID: string]: { [originalSubject: string]: { [originalObject: string]: string /*aux/support graph ID */ } };
} = {};
Object.entries(this.bteGraph.edges).forEach(([edgeID, bteEdge]) => {
if (edgeID.includes('expanded')) return;
const combos: { subject: string; object: string; supportGraph: string[] }[] = [];
Expand Down Expand Up @@ -254,6 +258,10 @@ export default class TRAPIQueryHandler {
suffix += 1;
}
const supportGraphID = `support${suffix}-${boundEdgeID}`;
if (!supportGraphsByEdgeID[edgeID]) supportGraphsByEdgeID[edgeID] = {};
if (!supportGraphsByEdgeID[edgeID][subject]) supportGraphsByEdgeID[edgeID][subject] = {};
supportGraphsByEdgeID[edgeID][subject][object] = supportGraphID;

auxGraphs[supportGraphID] = { edges: supportGraph, attributes: [] };
if (!edgesIDsByAuxGraphID[supportGraphID]) {
edgesIDsByAuxGraphID[supportGraphID] = new Set();
Expand Down Expand Up @@ -305,7 +313,40 @@ export default class TRAPIQueryHandler {
newBindings.push(binding);
boundIDs.add(binding.id);
}
} else if (!boundIDs.has(edgesToRebind[binding.id]?.[subId]?.[objId])) {
}
// we only want to include support graphs that meet constraints (in case there is another QEdge using this same KGEdge)
else if (
this.queryGraph.edges[qEdgeID].attribute_constraints ||
this.queryGraph.nodes[this.queryGraph.edges[qEdgeID].subject].constraints ||
this.queryGraph.nodes[this.queryGraph.edges[qEdgeID].object].constraints
) {
const oldBoundEdge = this.bteGraph.edges[edgesToRebind[binding.id]?.[subId]?.[objId]];
const newBoundEdge = `${edgesToRebind[binding.id][subId][objId]}-constrained_by_${qEdgeID}`;
if (!boundIDs.has(newBoundEdge)) {
this.bteGraph.edges[newBoundEdge] = new KGEdge(newBoundEdge, {
predicate: oldBoundEdge.predicate,
subject: oldBoundEdge.subject,
object: oldBoundEdge.object,
});
this.bteGraph.edges[newBoundEdge].addAdditionalAttributes('biolink:support_graphs', []);
this.bteGraph.edges[newBoundEdge].addAdditionalAttributes('biolink:knowledge_level', 'logical_entailment')
this.bteGraph.edges[newBoundEdge].addAdditionalAttributes('biolink:agent_type', 'automated_agent')
this.bteGraph.edges[newBoundEdge].addSource([
{
resource_id: this.options.provenanceUsesServiceProvider
? 'infores:service-provider-trapi'
: 'infores:biothings-explorer',
resource_role: 'primary_knowledge_source',
},
]);
boundIDs.add(newBoundEdge);
newBindings.push({...binding, id: newBoundEdge });
resultBoundEdgesWithAuxGraphs.add(newBoundEdge);
}
(this.bteGraph.edges[newBoundEdge].attributes['biolink:support_graphs'] as Set<string>).add(supportGraphsByEdgeID[binding.id][subId][objId]);
edgesIDsByAuxGraphID[supportGraphsByEdgeID[binding.id][subId][objId]].add(newBoundEdge);
}
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]);
Expand Down Expand Up @@ -468,6 +509,7 @@ export default class TRAPIQueryHandler {
}
}
}
this.queryGraphHandler = new QueryGraph(queryGraph, this.options.schema, this._queryIsPathfinder());
}

_initializeResponse(): void {
Expand All @@ -477,11 +519,19 @@ export default class TRAPIQueryHandler {
this.bteGraph.subscribe(this.knowledgeGraph);
}

async _processQueryGraph(queryGraph: TrapiQueryGraph): Promise<QEdge[]> {
const queryGraphHandler = new QueryGraph(queryGraph, this.options.schema, this._queryIsPathfinder());
const queryEdges = await queryGraphHandler.calculateEdges();
this.logs = [...this.logs, ...queryGraphHandler.logs];
return queryEdges;
async _processQueryGraph(): Promise<QEdge[]> {
try {
const queryEdges = await this.queryGraphHandler.calculateEdges();
this.logs = [...this.logs, ...this.queryGraphHandler.logs];
return queryEdges;
} catch (err) {
if (err instanceof InvalidQueryGraphError || err instanceof SRINodeNormFailure) {
throw err;
} else {
console.log(err.stack);
throw new InvalidQueryGraphError();
}
}
}

async _edgesSupported(qEdges: QEdge[], metaKG: MetaKG): Promise<boolean> {
Expand Down Expand Up @@ -608,6 +658,9 @@ export default class TRAPIQueryHandler {
this.logs.push(new LogEntry('WARNING', null, message).getLog());
return;
}
if (await this._checkContraints()) {
return;
}
const inferredQueryHandler = new InferredQueryHandler(
this,
this.queryGraph,
Expand Down Expand Up @@ -644,7 +697,7 @@ export default class TRAPIQueryHandler {
new LogEntry(
'ERROR',
null,
`BTE does not currently support any type of constraint. Your query Terminates.`,
`BTE does not currently support constraints with creative mode. Your query Terminates.`,
).getLog(),
);
return true;
Expand Down Expand Up @@ -737,11 +790,8 @@ export default class TRAPIQueryHandler {
);
}

const queryEdges = await this._processQueryGraph(this.queryGraph);
// TODO remove this when constraints implemented
if (await this._checkContraints()) {
return;
}
const queryEdges = await this._processQueryGraph();

if ((this.options.smartAPIID || this.options.teamName) && Object.values(this.queryGraph.edges).length > 1) {
const message = 'smartAPI/team-specific endpoints only support single-edge queries. Your query terminates.';
this.logs.push(new LogEntry('WARNING', null, message).getLog());
Expand Down Expand Up @@ -779,10 +829,11 @@ export default class TRAPIQueryHandler {

// update query graph
this.bteGraph.update(manager.getRecords());
this.bteGraph.notify();
//update query results
await this.trapiResultsAssembler.update(
manager.getOrganizedRecords(),
!(this.options.smartAPIID || this.options.teamName),
!(this.options.smartAPIID || this.options.teamName)
);
this.logs = [...this.logs, ...this.trapiResultsAssembler.logs];
// fix subclassing
Expand Down
Loading
Loading