diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5fac628 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/data/ \ No newline at end of file diff --git a/edge.ts b/edge.ts index 6dca9f8..dd3990f 100644 --- a/edge.ts +++ b/edge.ts @@ -1,8 +1,8 @@ import { base_id, EdgeArgs } from "./enums.ts"; export class Edge { - private to: base_id; private from: base_id; + private to: base_id; weight: number; /** @@ -29,6 +29,23 @@ export class Edge { return { from: this.from, to: this.to, weight: this.weight }; } + isSameAs(edge: Edge, is_directed = false): boolean { + const { vertices } = this; + return ( + (edge.vertices.from === vertices.from && + edge.vertices.to === vertices.to) || + (!is_directed && + edge.vertices.from === vertices.to && + edge.vertices.to === vertices.from) + ); + } + + pairVertex(vertex_id: base_id): base_id | undefined { + if (vertex_id === this.to) return this.from; + else if (vertex_id === this.from) return this.to; + return undefined; + } + hasVertex(vertex_id: base_id): boolean { if (this.from === vertex_id || this.to === vertex_id) return true; return false; diff --git a/enums.ts b/enums.ts index 3590434..9f2c650 100644 --- a/enums.ts +++ b/enums.ts @@ -1,7 +1,5 @@ export type base_id = string | number; -export type Cycle = base_id[]; - export interface EdgeNeighborhood { from: { id: base_id; @@ -45,4 +43,5 @@ export const ERROR = { NOT_MULTIGRAPH: "Trying to add multiple edges between two vertices. Graph is not a multigraph!", UNDEFINED_ID: "Tried to use undefined id as input", + SELF_LOOP: "No self-loops", }; diff --git a/network.ts b/network.ts index f3c67a8..926aa98 100644 --- a/network.ts +++ b/network.ts @@ -6,7 +6,6 @@ import { EdgeArgs, NetworkArgs, ERROR, - Cycle, EdgeNeighborhood, } from "./enums.ts"; @@ -132,10 +131,22 @@ export class Network { return [...this.vertices.values()]; } + /** + * Get an array with the edges of the network + * @returns Edge[] + */ get edge_list(): Edge[] { return [...this.edges.values()]; } + /** + * Returns pairs of nodes that represent `from` and `to` (respectively) vertices in an edge + * @returns [base_id, base_id][] + */ + get simple_edge_list(): [base_id, base_id][] { + return this.edge_list.map(({ vertices }) => [vertices.from, vertices.to]); + } + /** * Number of edges in the [maximum clique possible](https://www.wikiwand.com/en/Clique_(graph_theory)) with the network's number of verices. * @returns number @@ -178,6 +189,8 @@ export class Network { if (this.edges.has(args.id)) throw { message: ERROR.EXISTING_EDGE }; + if (args.from === args.to) throw { message: ERROR.SELF_LOOP, args }; + if (this.edges.size >= this.edge_limit) throw { message: ERROR.EDGE_LIMIT }; if (!args.do_force) { @@ -283,10 +296,11 @@ export class Network { * @returns base_id[] */ edgeBetween( - from: base_id, - to: base_id, + from: base_id | undefined, + to: base_id | undefined, is_directed = this.is_directed ): Edge | undefined { + if (from === undefined || to === undefined) return undefined; return this.edge_list.find(({ vertices }) => this.checkEdgeIsSame(vertices, { from, to }, is_directed) ); @@ -347,6 +361,15 @@ export class Network { return this.vertices.has(id); } + /** + * Returns true if an edge with the given id exists + * @param {base_id} id + * @returns boolean + */ + hasVertices(ids: base_id[]): boolean { + return ids.every((id) => this.vertices.has(id)); + } + /** * Get in-neighbors of a given vertex. * @@ -647,8 +670,8 @@ export class Network { * Returns a list with all triplets in the network. * @returns Cycle[] */ - triplets(): Cycle[] { - const triplet_list: Cycle[] = []; + triplets(): base_id[][] { + const triplet_list: base_id[][] = []; const k2 = this.core(2); @@ -658,7 +681,7 @@ export class Network { const { from, to } = edge.vertices; k2.neighbors(from).forEach((id) => { if (edge.hasVertex(id)) return; - const triplet: Cycle = [id, from, to]; + const triplet: base_id[] = [id, from, to]; const unsorted = [...triplet]; if ( @@ -674,60 +697,73 @@ export class Network { return triplet_list; } + /** + * Algorithm to find all quadruplets in a network. + * @returns Cycle[] + */ quadruplets(): Cycle[] { const c4: Cycle[] = []; const k2 = this.core(2); - const { edges } = k2; - - edges.forEach((edge1) => { - const neighbors1 = this.edgeNeighbors(edge1); - const vertex_check: Map = new Map(); - - let small_neighborhood = neighbors1.from.neighbors; - vertex_check.set(neighbors1.from.id, 0); - vertex_check.set(neighbors1.to.id, 1); - - if (!this.is_directed) { - if (neighbors1.from.neighbors.length > neighbors1.to.neighbors.length) { - vertex_check.set(neighbors1.to.id, 0); - vertex_check.set(neighbors1.from.id, 1); - small_neighborhood = neighbors1.to.neighbors; + const edges1 = k2.edges; + + edges1.forEach((edge1) => { + const initial_edge = edge1.args; + let loop_vertex = edge1.vertices.from; + let pair_vertex = edge1.pairVertex(loop_vertex)!; + let pair_vertex_neighbors = k2.neighbors(pair_vertex); + + if (!k2.is_directed) { + const loop_vertex_neighbors = k2.neighbors(loop_vertex); + if (pair_vertex_neighbors.length > loop_vertex_neighbors.length) { + const temp = loop_vertex; + loop_vertex = pair_vertex; + pair_vertex = temp; + pair_vertex_neighbors = loop_vertex_neighbors; } } - small_neighborhood.forEach((vertex_id) => { - const edges_of = this.is_directed - ? this.edgesFrom(vertex_id) - : this.edgesWith(vertex_id); - if (!vertex_check.has(vertex_id)) { - vertex_check.set(vertex_id, 2); - edges_of.forEach((edge2) => { - const { vertices } = edge2; - vertex_check.set( - vertices.from === vertex_id ? vertices.to : vertices.from, - 3 - ); - - const check_list = [...vertex_check.keys()]; - if ( - this.hasEdge(check_list[3], check_list[0]) && - vertex_check.size === 4 && - !this.listHasCycle(c4, [...vertex_check.keys()]) - ) - c4.push([...vertex_check.keys()]); + pair_vertex_neighbors.forEach((vertex) => { + const parallel_edges = k2.is_directed + ? k2.edgesFrom(vertex) + : k2.edgesWith(vertex); + + parallel_edges.forEach((p_edge) => { + const cycle = new Cycle({ + is_directed: k2.is_directed, + initial_edge, + loop_vertex, }); - } + + if (cycle.addEdge(k2.edgeBetween(cycle.tip, vertex)?.args)) + if (cycle.addEdge(p_edge.args)) + if (cycle.close(k2.edgeBetween(cycle.tip, loop_vertex)?.args)) + if (!c4.some((c) => c.isSameAs(cycle))) c4.push(cycle); + }); }); }); return c4; } - edgesFrom(vertex_id: base_id): Edge[] { - return this.edge_list.filter((edge) => edge.vertices.from === vertex_id); + /** + * Edges that start at vertex_id. Excluding edges with a `to` vertex in the `except` array + * @param {base_id} vertex_id + * @param {base_id[]=[]} except + * @returns Edge + */ + edgesFrom(vertex_id: base_id, except: base_id[] = []): Edge[] { + return this.edge_list.filter( + (edge) => + edge.vertices.from === vertex_id && !except.includes(edge.vertices.to) + ); } + /** + * Returns all edges with the given vertex, regardless of the position of the vertex (`from` or `to`). + * @param {base_id} vertex_id + * @returns Edge + */ edgesWith(vertex_id: base_id): Edge[] { return this.edge_list.filter( (edge) => @@ -735,6 +771,11 @@ export class Network { ); } + /** + * Returns the given edge's neighborhood. That is, the neighborhood of both its vertices. + * @param {Edge} edge + * @returns EdgeNeighborhood + */ edgeNeighbors(edge: Edge): EdgeNeighborhood { const { from, to } = edge.vertices; const edge_neighbors: EdgeNeighborhood = { @@ -763,7 +804,7 @@ export class Network { * @param {Cycle} triplet * @returns boolean */ - private listHasCycle(cycle_arr: Cycle[], cycle: Cycle): boolean { + private listHasCycle(cycle_arr: base_id[][], cycle: base_id[]): boolean { return cycle_arr.some((trip) => this.isSameCycle(cycle, trip)); } @@ -773,9 +814,9 @@ export class Network { * @param {Cycle} arr2 * @returns boolean */ - private isSameCycle(arr1: Cycle, arr2: Cycle): boolean { + private isSameCycle(arr1: base_id[], arr2: base_id[]): boolean { if (arr1.length !== arr2.length) return false; - return arr1.every((element, index) => element === arr2[index]); + return arr1.every((element) => arr2.includes(element)); } /** @@ -812,3 +853,118 @@ export class Network { return false; } } + +export class Cycle extends Network { + readonly loop_vertex: base_id; + private tip_vertex: base_id; + private is_closed: boolean; + + constructor(args: { + is_directed: boolean; + initial_edge: EdgeArgs; + loop_vertex?: base_id; + }) { + super(args); + const { initial_edge, loop_vertex, is_directed } = args; + super.addEdge(initial_edge); + this.loop_vertex = initial_edge.from; + this.tip_vertex = initial_edge.to; + if (!is_directed && loop_vertex !== undefined) { + this.loop_vertex = loop_vertex; + this.tip_vertex = this.edge_list[0].pairVertex(this.loop_vertex)!; + } + this.is_closed = false; + } + + /** + * Getter for the tip of the cycle. + * @returns base_id + */ + get tip(): base_id { + return this.tip_vertex; + } + + /** + * Returns true if the cycle is closed, otherwise returns false. + * @returns boolean + */ + get is_complete(): boolean { + return this.is_closed; + } + + /** + * Adds an edge to the cycle if possible. + * Returns true if the addition is successful. + * @param {EdgeArgs|undefined} edge + * @returns boolean + */ + addEdge(edge: EdgeArgs | undefined): boolean { + if (edge !== undefined && this.canAdd(edge)) { + super.addEdge(edge); + if (!this.is_directed && this.tip_vertex === edge.to) + this.tip_vertex = edge.from; + else this.tip_vertex = edge.to; + + return true; + } + + return false; + } + + /** + * Tries to close the cycle. + * Returns true if the operation was sucessful. + * @param {EdgeArgs|undefined} edge + * @returns boolean + */ + close(edge: EdgeArgs | undefined): boolean { + if (edge !== undefined && this.canCloseWith(edge)) { + super.addEdge(edge); + this.is_closed = true; + this.tip_vertex = this.loop_vertex; + return true; + } + + return false; + } + + /** + * Compares the cycle with a given cycle. + * Returns whether they are the same or not. + * @param {Cycle} cycle + * @returns boolean + */ + isSameAs(cycle: Cycle): boolean { + if (this.is_directed !== cycle.is_directed) return false; + return this.edge_list.every(({ vertices }) => { + return cycle.edge_list.some(({ vertices: compare }) => { + return ( + (compare.from === vertices.from && compare.to === vertices.to) || + (!this.is_directed && + compare.from === vertices.to && + compare.to === vertices.from) + ); + }); + }); + } + + private canCloseWith(edge: EdgeArgs): boolean { + const edge_has_tip_and_loop_vertex = + (edge.from === this.tip_vertex && edge.to === this.loop_vertex) || + (!this.is_directed && + edge.to === this.tip_vertex && + edge.from === this.loop_vertex); + + return !this.is_closed && edge_has_tip_and_loop_vertex; + } + + private canAdd(edge: EdgeArgs) { + const edge_has_tip = + (edge.from === this.tip_vertex && !this.hasVertex(edge.to)) || + (!this.is_directed && + edge.to === this.tip_vertex && + !this.hasVertex(edge.from)); + + return !this.is_closed && edge_has_tip; + } +} diff --git a/tester.ts b/tester.ts index 1ee7a45..3ecb823 100644 --- a/tester.ts +++ b/tester.ts @@ -81,7 +81,7 @@ function getTestTime(): string { ); } -// const net_csv = await nex.loadAdjacencyMatrix("./data/networkMatrix.csv"); +const net_csv = await nex.loadAdjacencyMatrix("./data/networkMatrix.csv"); // let test_data = valuesTest(net_csv) + "\n" + algorithmTest(net_csv); @@ -95,22 +95,21 @@ function getTestTime(): string { // test_data // ); -// const randomNet = nex.randomNetworkGen({ -// number_vertices: 20, -// number_edges: 40, -// }); +const randomNet = nex.randomNetworkGen({ + number_vertices: 6, + number_edges: 10, +}); // nex.writeAdjacencyMatrix(randomNet); -const net = new nets.Network(); -net.addEdgeList([ - ["a", "b"], - ["b", "c"], - ["c", "d"], - ["d", "a"], - ["e", "d"], - ["e", "c"], - ["b", "d"], -]); - -console.log(net.quadruplets()); +const quad_time = Date.now(); + +const quad = randomNet.quadruplets(); + +nex.writeAdjacencyMatrix(randomNet, "test_random_quad.csv"); + +console.log( + quad.map((c) => [...c.edges.values()]), + quad.length, + randomNet.edges +);