Commit 4e29d85f by Onlynagesha

init

parents
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
# c-w-im-visualization-ts
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```
/// <reference types="vite/client" />
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
{
"name": "c-w-im-visualization-ts",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build --force"
},
"dependencies": {
"@antv/g2": "^5.1.20",
"d3": "latest",
"netv": "latest",
"vue": "^3.4.21"
},
"devDependencies": {
"@tsconfig/node20": "^20.1.4",
"@types/d3": "^7.4.3",
"@types/node": "^20.12.5",
"@vitejs/plugin-vue": "^5.0.4",
"@vue/tsconfig": "^0.5.1",
"npm-run-all2": "^6.1.2",
"typescript": "~5.4.0",
"vite": "^5.2.8",
"vue-tsc": "^2.0.11"
}
}
<template>
<div>
<h3>可视化:基于图收缩的影响最大化</h3>
<input type="checkbox" v-model="autoStarts" @change="autoStartsChanged"> 自动播放
<hr>
<div>
<input type="radio" id="showsData1" name="dataSelector" value="demoSmall" v-model="curDataCandidateName" checked/>
<label for="showsData1">小型图结构(百万级)</label>
<input type="radio" id="showsData2" name="dataSelector" value="demoMedium" v-model="curDataCandidateName"/>
<label for="showsData2">中型图结构(千万级)</label>
<input type="radio" id="showsData3" name="dataSelector" value="demoLarge" v-model="curDataCandidateName"/>
<label for="showsData3">大型图结构(亿级)</label>
</div>
<div class="canvasGroup" @click="proceed">
<!-- Main: Propagation with Seeds Selected by Coarsening-Based IM Algorithm -->
<div class="mainCanvasContainer">
<Canvas ref="mainCanvas" class="mainCanvas"
:data="curDataCandidate" :config="config" title="主视图:图收缩IM算法(main)" :title-style="{fontSize: '16px'}"
:proceeding-handle="addAnimationLoopFunction">
</Canvas>
<div class="mainCanvasStateDescription">
<p class="mainCanvasNoteItem">
图收缩算法:<b style="color: orange">{{ mainStateDescription }}</b>
</p>
<p class="mainCanvasNoteItem">
对照算法:<b style="color: orangered">{{ contrastStateDescription }}</b>
</p>
</div>
<div class="mainCanvasNotes">
<p class="mainCanvasNoteItem">
信息从算法选定的<b :style="{color: seedNodeColorString}">种子节点</b>出发
</p>
<p class="mainCanvasNoteItem">
期望<b :style="{color: visitedNodeColorString}">接收信息的节点</b>尽可能多
</p>
<p>
指标(<b style="color: red">红色</b>越低越好,<b style="color: darkgreen">绿色</b>越高越好):
</p>
<ul>
<li class="mainCanvasNoteItem">
节点总数:<b style="color: blue;">{{ (curDataCandidate.nNodesRaw).toLocaleString() }}</b>
</li>
<li class="mainCanvasNoteItem">
收缩后节点总数:<b style="color: blue">{{ (nCoarsenedNodesRaw).toLocaleString() }}</b>
</li>
<li class="mainCanvasNoteItem">
收缩比:<b style="color: red">{{ (coarseningRatio * 100).toFixed(2) }}%</b>
</li>
<li class="mainCanvasNoteItem">
算法时间加速比:<b style="color: darkgreen">{{ (1.0 / timeUsageRatio).toFixed(2) }}</b>
</li>
<li class="mainCanvasNoteItem">
算法近似比(精度):<b style="color: darkgreen">{{ (approximationRatio * 100).toFixed(2) }}%</b>
</li>
</ul>
</div>
</div>
<div class="subCanvasColumn">
<Charts ref="charts" class="subCanvas"></Charts>
<!-- Sub #1: Same as Main -->
<Canvas ref="subCanvas1" class="subCanvas"
:data="curDataCandidate" :config="config" title="图收缩IM算法(main)"
:proceeding-handle="addAnimationLoopFunction">
</Canvas>
<!-- Sub #2: (As Contrast) Propagation with Seeds Selected by Another IM Algorithm -->
<Canvas ref="subCanvas2" class="subCanvas"
:data="curDataCandidate" :config="config" :contrast="true" title="对照组:常规IM算法(contrast)"
:algorithm-key="'contrast'"
:proceeding-handle="addAnimationLoopFunction">
</Canvas>
</div>
</div>
</div>
</template>
<script lang="ts">
import demoSmall from "@/assets/demo-n-400.json";
import demoMedium from "@/assets/demo-n-1000.json";
import demoLarge from "@/assets/demo-n-2000.json";
import demoConfig from "@/assets/demo-config.json";
import Canvas from "@/components/Canvas.vue";
import Charts from "@/components/Charts.vue";
import { nTotalVisitedNodes, parseCoarseningData } from "./scripts/data.js";
import type { AnimationConfig, AnimationPhase, DisplayAnimationStates, DisplayData } from "./scripts/data_types.js";
const dataCandidates = new Map<string, DisplayData>();
dataCandidates.set('demoSmall', parseCoarseningData(demoSmall));
dataCandidates.set('demoMedium', parseCoarseningData(demoMedium));
dataCandidates.set('demoLarge', parseCoarseningData(demoLarge));
interface AppData {
curDataCandidateName: string,
curDataCandidate: DisplayData,
config: AnimationConfig,
autoStarts: boolean,
mainState: AnimationPhase,
contrastState: AnimationPhase,
intervalID: number | null,
animationLoopFunctions: Array<() => void>,
}
export default {
data(): AppData {
return {
curDataCandidateName: "demoSmall",
curDataCandidate: dataCandidates.get("demoSmall") as DisplayData,
config: demoConfig as AnimationConfig,
autoStarts: false,
mainState: 'INITIAL',
contrastState: 'INITIAL',
intervalID: null,
animationLoopFunctions: [],
}
},
components: {
Canvas,
Charts
},
computed: {
mainStateDescription() {
const phase = this.mainState;
if (phase === 'INITIAL') {
return "未开始";
} else if (phase === 'COARSENING') {
return "图收缩:获得比原图小得多的缩略图";
} else if (phase === 'SHOWING_SEEDS' || phase === 'WAITING') {
return "求初始解(快速):对缩略图求解种子节点";
} else if (phase === 'EXPANDING') {
return "图展开:将种子节点展开至原图";
} else if (phase === 'SIMULATING') {
return "模拟:展示种子节点的信息传播能力";
} else {
return `未知状态:${phase}`;
}
},
contrastStateDescription() {
const phase = this.contrastState;
if (phase === 'INITIAL') {
return "未开始";
} else if (phase === 'WAITING' || phase === 'SHOWING_SEEDS') {
return "求解(慢速):对原图求解种子节点";
} else if (phase === 'SIMULATING') {
return "模拟:展示种子节点的信息传播能力";
} else {
return `未知状态:${phase}`;
}
},
seedNodeColorString() {
const fill = this.config.styles.nodes.seed.fill;
return this.fillColorToString(fill);
},
visitedNodeColorString() {
const fill = this.config.styles.nodes.visited.fill;
return this.fillColorToString(fill);
},
nCWIMVisitedNodes() {
return nTotalVisitedNodes(this.curDataCandidate, 'cwim');
},
nContrastVisitedNodes() {
return nTotalVisitedNodes(this.curDataCandidate, 'contrast');
},
coarseningRatio() {
const cur = this.curDataCandidate;
return cur.graphs[cur.graphs.length - 1].nodes.size / cur.graphs[0].nodes.size;
},
timeUsageRatio() {
const cur = this.curDataCandidate;
return cur.timeUsed.cwim / cur.timeUsed.contrast;
},
nCoarsenedNodesRaw() {
return Math.round(this.curDataCandidate.nNodesRaw * this.coarseningRatio);
},
approximationRatio() {
return this.nCWIMVisitedNodes / this.nContrastVisitedNodes;
}
},
methods: {
proceed() {
const children = [
this.$refs.mainCanvas,
this.$refs.subCanvas1,
this.$refs.subCanvas2,
];
children.forEach((c) => (c as typeof Canvas).proceed());
},
autoStartsChanged() {
const children = [
this.$refs.mainCanvas,
this.$refs.subCanvas1,
this.$refs.subCanvas2,
];
children.forEach((c) => (c as typeof Canvas).autoProceed(this.autoStarts));
},
updateChart() {
const chart = this.$refs.charts as typeof Charts;
const cur = this.curDataCandidate;
Object.entries(cur.timeUsed).forEach(([key, timeUsed]) => {
chart.updateData('timeUsage', key, timeUsed);
});
chart.updateData('influence', 'cwim', this.nCWIMVisitedNodes);
chart.updateData('influence', 'contrast', this.nContrastVisitedNodes);
chart.reRender();
},
addAnimationLoopFunction(loopFn: () => void) {
this.animationLoopFunctions.push(loopFn);
},
fillColorToString(fill?: {r?: number, g?: number, b?: number}): string {
const toChannel = (key: 'r' | 'g' | 'b') => {
return fill ? ((fill[key] ?? 0.0) * 255).toFixed(0) : 0;
};
return fill ? `rgb(${toChannel('r')}, ${toChannel('g')}, ${toChannel('b')})` : 'black';
}
},
mounted() {
this.updateChart();
this.intervalID = setInterval(() => {
for (let loopFn of this.animationLoopFunctions) {
loopFn();
}
this.mainState = ((this.$refs.mainCanvas as typeof Canvas).states as DisplayAnimationStates).phase;
this.contrastState = ((this.$refs.subCanvas2 as typeof Canvas).states as DisplayAnimationStates).phase;
}, 1000.0 / this.config.fps);
},
unmounted() {
if (this.intervalID) {
clearInterval(this.intervalID);
}
},
watch: {
curDataCandidateName(newValue) {
this.curDataCandidate = dataCandidates.get(newValue) as DisplayData;
this.updateChart();
}
}
}
</script>
<style scoped>
.canvasGroup {
width: fit-content;
float: left;
}
.mainCanvasContainer {
width: 960px;
height: 720px;
float: inherit;
position: relative;
box-sizing: border-box;
}
.mainCanvas {
width: inherit;
height: inherit;
float: inherit;
border: solid 1px black;
}
.mainCanvasStateDescription {
position: absolute;
left: 10px;
top: 32px;
}
.mainCanvasNotes {
position: absolute;
right: 10px;
top: 0px;
}
.mainCanvasNoteItem {
margin: 3px;
}
.subCanvasColumn {
width: 320px;
height: 720px;
float: inherit;
}
.subCanvas {
width: 320px;
height: 240px;
float: inherit;
box-sizing: border-box;
border: solid 1px black;
}
</style>
{
"fps": 60,
"nAnimatingNodesPerLink": 10,
"animatingNodeMaxSpeed": 1.25,
"nMovingFrames": 45,
"nWaitingFrames": 15,
"styles": {
"nodes": {
"seed": {
"shape": "circle",
"r": 7,
"fill": {
"r": 0.88,
"g": 0.222,
"b": 0.165,
"a": 0.9
},
"strokeWidth": 0
},
"visited": {
"shape": "circle",
"r": 6,
"fill": {
"r": 0.898,
"g": 0.607,
"b": 0.322,
"a": 0.9
},
"strokeWidth": 0
},
"animating": {
"shape": "circle",
"r": 3,
"fill": {
"r": 0.898,
"g": 0.749,
"b": 0.369,
"a": 0.75
},
"strokeWidth": 0
},
"default": {
"shape": "circle",
"r": 5,
"fill": {
"r": 0,
"g": 0.422,
"b": 0.622,
"a": 0.5
},
"strokeWidth": 0
}
},
"links": {
"animating": {
"strokeColor": {
"r": 0.75,
"g": 0.5,
"b": 0.25,
"a": 0.5
},
"strokeWidth": 2
},
"default": {
"strokeColor": {
"r": 0,
"g": 0,
"b": 0,
"a": 0.1
},
"strokeWidth": 2
}
}
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
<template>
<!-- 3 canvas objects overlapped -->
<div ref="canvas">
<text class="title" :style="titleStyle">{{ title }}</text>
<div ref="graphCanvas" class="graph-canvas"></div>
<div ref="animatingCanvas" class="animating-canvas"></div>
</div>
</template>
<script lang="ts">
import NetV from 'netv/src';
import { defineComponent } from 'vue';
import {
createEmptyCanvas,
drawInitialGraphCanvas,
getProceedAnimationFrameFn,
initialCoarseningAnimationStates,
nodeLinkLimitOfCanvases,
} from '@/scripts/draw_canvas';
import type {
AnimationConfig,
DisplayAnimationStates,
DisplayData
} from '@/scripts/data_types';
interface CanvasData {
graphCanvas: NetV | null,
animatingCanvas: NetV | null,
intervalID: number | null,
states: DisplayAnimationStates | null,
}
export default defineComponent({
data(): CanvasData {
return {
graphCanvas: null,
animatingCanvas: null,
intervalID: null,
states: null
};
},
props: {
data: {
type: Object,
required: true
},
config: {
type: Object,
required: true
},
title: {
type: String,
default: "<No Name>"
},
titleStyle: {
type: Object,
default: { fontSize: '12px' }
},
algorithmKey: {
type: String,
default: 'cwim'
},
proceedingHandle: {
type: Function,
default: null
}
},
methods: {
proceed() {
if (this.states) {
this.states.manualStarts = true;
}
},
autoProceed(flag = true) {
if (this.states) {
this.states.autoStarts = flag;
}
},
initCanvas() {
const config = (this.config as AnimationConfig);
const limit = nodeLinkLimitOfCanvases(this.data as DisplayData, this.config as AnimationConfig);
// (1) Graph canvas: bottommost
const graphCanvas = createEmptyCanvas((this.$refs.graphCanvas as HTMLDivElement), limit.graphCanvas);
drawInitialGraphCanvas(graphCanvas, this.data as DisplayData, config);
this.graphCanvas = graphCanvas;
// (2) Animating canvas: in the middle, animation effects to display the simulation process
this.animatingCanvas = createEmptyCanvas((this.$refs.animatingCanvas as HTMLDivElement), limit.animatingCanvas);
// Registers loop
this.states = initialCoarseningAnimationStates(config);
const proceedingFn = getProceedAnimationFrameFn(this.algorithmKey);
const proceedingLoopFn = () => proceedingFn(
this.graphCanvas,
this.animatingCanvas,
this.data as DisplayData,
this.states as DisplayAnimationStates
);
if (this.proceedingHandle) {
this.proceedingHandle(proceedingLoopFn);
} else {
this.intervalID = setInterval(proceedingLoopFn, 1000.0 / (config).fps);
}
},
deinitCanvas() {
if (this.intervalID) {
clearInterval(this.intervalID);
}
for (let canvas of [this.graphCanvas, this.animatingCanvas]) {
if (canvas) {
(canvas as NetV).dispose();
}
}
},
},
mounted() {
this.initCanvas();
},
unmounted() {
this.deinitCanvas();
},
watch: {
data() {
this.deinitCanvas();
this.states = initialCoarseningAnimationStates(this.config as AnimationConfig);
this.initCanvas();
}
}
});
</script>
<style scoped>
.title {
position: absolute;
margin: 5px;
z-index: 0;
}
.graph-canvas {
width: inherit;
height: inherit;
position: absolute;
z-index: 1;
}
.animating-canvas {
width: inherit;
height: inherit;
position: absolute;
z-index: 2;
}
</style>
<template>
<div>
<div ref="timeUsageChart" class="chart"></div>
<div ref="influenceChart" class="chart"></div>
</div>
</template>
<script lang="ts">
import { Chart } from '@antv/g2';
import { defineComponent } from 'vue';
import type { ChartsData, ChartsDataItemKey, ChartsDataKey } from '@/scripts/data_types';
interface ChartsComponentData {
data: ChartsData,
timeUsageChart: Chart | null,
influenceChart: Chart | null
}
export default defineComponent({
data(): ChartsComponentData {
return {
data: {
nNodes: { cwim: 0, contrast: 0 },
timeUsage: { cwim: 0, contrast: 0 },
influence: { cwim: 0, contrast: 0 }
},
timeUsageChart: null,
influenceChart: null
};
},
methods: {
toChartData(key: ChartsDataKey): {x: string, y: number}[] {
const res = [
{ x: 'main', y: this.data[key].cwim },
{ x: 'contrast', y: this.data[key].contrast }
];
return res;
},
updateData(key: ChartsDataKey, group: ChartsDataItemKey, value: number) {
this.data[key][group] = value;
},
reRender(key?: ChartsDataKey) {
if (key === 'timeUsage' || key === undefined || key === null) {
this.timeUsageChart?.changeData(this.toChartData('timeUsage'));
}
if (key === 'influence' || key === undefined || key === null) {
this.influenceChart?.changeData(this.toChartData('influence'));
}
}
},
mounted() {
const timeUsageChart = new Chart({
container: this.$refs.timeUsageChart as HTMLDivElement,
autoFit: true,
axis: {
y: { title: "影响最大化算法耗时(越短越好)", tickCount: 5 },
x: { title: '' }
}
});
timeUsageChart.interval()
.coordinate({ transform: [{ type: 'transpose' }] })
.data(this.toChartData('timeUsage'))
.encode('x', 'x')
.encode('y', 'y')
.style('fill', '#ff4444')
this.timeUsageChart = timeUsageChart;
const influenceChart = new Chart({
container: this.$refs.influenceChart as HTMLDivElement,
autoFit: true,
axis: {
y: { title: "信息传播节点个数(越多越好)", tickCount: 5 },
x: { title: '' }
}
});
influenceChart.interval()
.coordinate({ transform: [{ type: 'transpose' }] })
.data(this.toChartData('influence'))
.encode('x', 'x')
.encode('y', 'y');
this.influenceChart = influenceChart;
for (let chart of [timeUsageChart, influenceChart]) {
chart.render();
}
}
});
</script>
<style scoped>
.chart {
float: left;
width: inherit;
height: 50%;
}
</style>
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
import type { DisplayData, DisplayJSONData, DisplayNodeData, IMAlgorithmKey } from "./data_types";
import * as d3 from 'd3';
export function parseCoarseningData(obj: DisplayJSONData): DisplayData {
// With preprocessing
const res: DisplayData = {
...obj,
graphs: obj.graphs.map((G) => {
const nodeMap = new Map<string, DisplayNodeData>()
G.nodes.forEach((node) => {
nodeMap.set(node.id, node);
});
return { nodes: nodeMap, links: G.links };
}),
seeds: {
cwim: obj.seeds.cwim.map((seeds) => new Set(seeds)),
contrast: new Set(obj.seeds.contrast),
},
};
return res;
}
function nSeeds(seeds: Array<string> | Set<string>) {
if (seeds instanceof Array) {
return seeds.length;
} else if (seeds instanceof Set) {
return seeds.size;
} else {
throw TypeError("Invalid seeds data type.");
}
}
export function nTotalVisitedNodes(data: DisplayJSONData | DisplayData, key: IMAlgorithmKey) {
const simData = data.simulation[key];
const nVisited = d3.sum(simData.map((level) => level.length));
if (key === 'cwim') {
const seeds = data.seeds.cwim[0];
return nVisited + nSeeds(seeds);
} else if (key === 'contrast') {
const seeds = data.seeds.contrast;
return nVisited + nSeeds(seeds);
} else {
throw RangeError(`Invalid algorithm key '${key}'.`);
}
}
import type NetV from "netv/src"
import type { LinkData, LinkStyle, NodeData, NodeStyle } from "netv/src/interfaces"
export interface DisplayNodeData extends NodeData {
/** (Required) x-coordinate in range [0, 1]. */
x: number,
/** (Required) y-coordinate in range [0, 1]. */
y: number,
/**
* Node ID in the next level to which current node is coarsened.
* Not present in the final level.
*/
to?: string,
}
export interface DisplayLevelJSONData {
/** All the nodes in the current level. */
nodes: Array<DisplayNodeData>,
/** All the links in the current level. */
links: Array<LinkData>
}
export interface DisplayLevelData {
/** All the nodes in the current level. Finds node by ID. */
nodes: Map<string, DisplayNodeData>,
/** All the links in the current level. */
links: Array<LinkData>
}
export type IMAlgorithmKey = 'cwim' | 'contrast';
interface DisplayDataBase<
LevelData extends DisplayLevelJSONData | DisplayLevelData,
SeedContainer extends Array<string> | Set<string>,
> {
/** Graphs in each level. */
graphs: Array<LevelData>,
/** Seed nodes from different algorithms. */
seeds: {
/**
* Seed nodes obtained by C-w-IM algorithm in each level.
* It shall be guaranteed in prior that each seeds in the (i + 1)-th layer
* is coarsened from some seed in the i-th layer.
*/
cwim: Array<SeedContainer>,
/** Seeds nodes obtained by the contrast IM algorithm. */
contrast: SeedContainer,
},
/**
* Process of information propagation from seeds by discrete steps.
* simulation[t] represents propagation actions (u -> v) in the t-th iteration.
*/
simulation: {
cwim: Array<Array<LinkData>>,
contrast: Array<Array<LinkData>>,
},
/** Time usage of different algorithms */
timeUsed: {
cwim: number,
contrast: number,
},
/**
* Number of nodes in the original graph (other than the displayed).
* Due to the limitation of rendering performance, the graph size displayed is much smaller than the actual.
*/
nNodesRaw: number,
/**
* Number of nodes in the original graph (other than the displayed).
* Due to the limitation of rendering performance, the graph size displayed is much smaller than the actual.
*/
nLinksRaw: number,
}
/** Display data obtained from JSON input. */
export interface DisplayJSONData
extends DisplayDataBase<DisplayLevelJSONData, Array<string>> {}
/** Display data after preprocessing. */
export interface DisplayData
extends DisplayDataBase<DisplayLevelData, Set<string>> {}
export interface AnimationConfig {
/** Maximum FPS of the animation. */
fps: number,
/**
* # of animating nodes per link during simulation, integer value in range [1, +inf). Denoted as k.
* Let u -> v be a link with information propagation,
* there will be k nodes moving from u to v with differing speed in the animation.
*/
nAnimatingNodesPerLink: number,
/**
* Maximum speed of animating nodes, value in range [1, +inf). Denoted as lambda.
* For each link u -> v during animation, the i-th animation node (i = 1 ... k, k >= 2) requires
* Fm / Speed(i) frames to reach the destination v, where Speed(i) = (i - 1) / (k - 1) * (lambda - 1) + 1.
* If k == 1, then Speed(0) = 1.0.
*/
animatingNodeMaxSpeed: number,
/** # of frames for moving animation in each section, denoted as Fm. */
nMovingFrames: number,
/** # of waiting frames between two moving sections. Denoted as Fw. */
nWaitingFrames: number,
styles: {
nodes: {
/** Style of seed nodes. */
seed: NodeStyle,
/** Style of the visited nodes. */
visited: NodeStyle,
/** Style of the animation nodes. */
animating: NodeStyle,
/** Style of all other nodes. */
default?: NodeStyle,
},
links: {
/** Style of the animating links. */
animating: LinkStyle,
/** Style of all the other links. */
default: LinkStyle,
},
},
}
export type AnimationPhase =
'INITIAL' | 'WAITING' | 'COARSENING' | 'SHOWING_SEEDS' | 'EXPANDING' | 'SIMULATING';
export interface DisplayAnimationStates {
/** See above */
config: AnimationConfig,
/** Whether auto play is enabled. */
autoStarts: boolean,
/** Whether manual play is triggered for the next section. */
manualStarts: boolean,
/** Current phase of animation. */
phase: AnimationPhase,
/**
* For coarsening and expanding phase, step = current graph level.
* For simulating phase, step = current iteration index.
*/
step: number,
/** Used in simulating phase. # of iterations whose destination nodes are displayed as visited. */
nVisitedSteps: number,
/** Frame index of current animation section. */
frame: number,
}
export interface ChartsDataItem {
cwim: number,
contrast: number
}
export interface ChartsData {
nNodes: ChartsDataItem,
timeUsage: ChartsDataItem,
influence: ChartsDataItem
}
export type ChartsDataItemKey = keyof ChartsDataItem;
export type ChartsDataKey = keyof ChartsData;
\ No newline at end of file
import NetV from 'netv';
import * as d3 from 'd3';
import type {
LinkData,
LinkStyle,
NodeData,
NodeLinkData,
NodeStyle
} from 'netv/src/interfaces';
import type {
AnimationConfig,
DisplayAnimationStates,
DisplayData,
DisplayNodeData,
IMAlgorithmKey
} from './data_types';
import type Node from 'netv/src/elements/node';
function toStyledNode(node: NodeData, defaultNodeStyle?: NodeStyle): NodeData {
return {
...node,
style: node.style ?? defaultNodeStyle
};
}
function toStyledLink(link: LinkData, defaultLinkStyle?: LinkStyle): LinkData {
return {
...link,
style: link.style ?? defaultLinkStyle
};
}
function updateNodeStyle(node: Node, style: NodeStyle): void {
// Common properties
node.fill(style.fill);
node.strokeColor(style.strokeColor);
node.strokeWidth(style.strokeWidth);
if (node.shape() === 'circle') {
// (1) Circle: 'r'
node.r?.call(node, style.r);
} else if (node.shape() === 'cross' || node.shape() === 'rect') {
// (2) Rect: 'width' and 'height'
node.width?.call(node, style.width);
node.height?.call(node, style.height);
if (node.shape() === 'cross') {
// (3) Cross: 'width', 'height', 'innerWidth' and 'innerHeight'
node.innerWidth?.call(node, style.innerWidth);
node.innerHeight?.call(node, style.innerHeight);
}
} else if (node.shape() === 'triangle') {
// (4) Triangle: 'vertexAlpha', 'vertexBeta' and 'vertexGamma'
node.vertexAlpha?.call(node, style.vertexAlpha);
node.vertexBeta?.call(node, style.vertexBeta);
node.vertexGamma?.call(node, style.vertexGamma);
} else {
throw TypeError(`Invalid node shape '${node.shape()}'.`);
}
}
function nodeLinkLimitOfGraphCanvas(data: DisplayData) {
const n = data.graphs[0].nodes.size;
const m = data.graphs[0].links.length;
return {n: n, m: m};
}
function nodeLinkLimitOfAnimatingCanvas(data: DisplayData, config: AnimationConfig) {
const maxNLinks = Math.max(
d3.max(data.simulation.cwim.map((links) => links.length)) as number,
d3.max(data.simulation.contrast.map((links) => links.length)) as number
);
const n = data.graphs[0].nodes.size + config.nAnimatingNodesPerLink * maxNLinks;
const m = maxNLinks;
return {n: n, m: m};
}
export function nodeLinkLimitOfCanvases(data: DisplayData, config: AnimationConfig) {
return {
graphCanvas: nodeLinkLimitOfGraphCanvas(data),
animatingCanvas: nodeLinkLimitOfAnimatingCanvas(data, config)
};
}
export function createEmptyCanvas(div: HTMLDivElement, nodeLimit: number, linkLimit: number): NetV;
export function createEmptyCanvas(div: HTMLDivElement, limit: {n: number, m: number}): NetV;
export function createEmptyCanvas(
div: HTMLDivElement,
nodeLimit: number | {n: number, m: number},
linkLimit?: number
): NetV {
const n = (typeof nodeLimit === 'number') ? nodeLimit : nodeLimit.n;
const m = (typeof nodeLimit === 'number') ? linkLimit : nodeLimit.m;
const netv = new NetV({
container: div,
width: div.offsetWidth,
height: div.offsetHeight,
nodeLimit: n,
linkLimit: m,
backgroundColor: { r: 0, g: 0, b: 0, a: 0 } // Transparent
});
const L = Math.min(div.offsetWidth, div.offsetHeight);
const D = div.offsetWidth - div.offsetHeight;
if (D >= 0) {
// Coordinate range is transformed linearly from [0, 1], [0, 1] to [D/2, L + D/2], [0, L].
netv.transform({x: D / 2, y: 0, k: L});
} else {
// Coordinate range is transformed linearly from [0, 1], [0, 1] to [0, L], [L - D/2, -D/2].
netv.transform({x: 0, y: -D / 2, k: L});
}
return netv;
}
export function drawInitialCanvas(
netv: NetV,
rawNodes?: Array<NodeData> | Set<NodeData> | Map<string, NodeData>,
rawLinks?: Array<LinkData>,
defaultNodeStyle?: NodeStyle,
defaultLinkStyle?: LinkStyle
): void {
const nodes: Array<NodeData> = []
if (rawNodes !== null && rawNodes !== undefined) {
if (rawNodes instanceof Array || rawNodes instanceof Set) {
rawNodes.forEach((node) => {
nodes.push(toStyledNode(node, defaultNodeStyle));
});
} else if (rawNodes instanceof Map) {
rawNodes.forEach((node, _) => {
nodes.push(toStyledNode(node, defaultNodeStyle));
});
} else {
throw TypeError("Expects rawNodes to be either one of Array, Set or Map.");
}
}
const linksFn = () => {
if (rawLinks !== null && rawLinks !== undefined) {
return Array.from(rawLinks, (link) => toStyledLink(link, defaultLinkStyle));
}
return [];
}
const links = linksFn();
netv.data({ nodes: nodes, links: links });
netv.draw();
}
export function drawInitialGraphCanvas(
graphCanvas: NetV,
data: DisplayData,
config: AnimationConfig
): void {
drawInitialCanvas(
graphCanvas,
data.graphs[0].nodes,
data.graphs[0].links,
config.styles.nodes.default,
config.styles.links.default,
);
}
export function initialCoarseningAnimationStates(
config: AnimationConfig
): DisplayAnimationStates {
return {
config: config,
autoStarts: false,
manualStarts: false,
phase: 'INITIAL',
step: 0,
nVisitedSteps: 0,
frame: 0
};
}
function proceedFrameIndex(
states: DisplayAnimationStates,
nFrames: number,
nSteps?: number,
): boolean {
if (nSteps && states.step < 0) {
states.step = 0;
states.frame = 0;
}
// Proceeding
states.frame += 1;
if (states.frame >= nFrames) {
states.frame = 0;
if (nSteps) {
states.step += 1;
if (states.step >= nSteps) {
states.step = 0;
return false; // Stops
}
} else {
return false;
}
}
return true; // Continues
}
function proceedNoOpAnimationFrame(
states: DisplayAnimationStates,
nFrames: number,
nSteps?: number,
): boolean {
return proceedFrameIndex(states, nFrames, nSteps);
}
function proceedCoarseningAnimationFrame(
netv: NetV,
data: DisplayData,
states: DisplayAnimationStates
): boolean {
if (states.step < 0) {
states.step = 0;
states.frame = 0;
}
// Stops if the last layer is reached.
if (states.step >= data.graphs.length) {
return false;
}
const nTotalFrames = states.config.nMovingFrames + states.config.nWaitingFrames;
if (states.frame === 0) {
// (1) The first frame of new level
const rawNodes = Array.from(
data.graphs[states.step].nodes.values()
);
const rawLinks = data.graphs[states.step].links;
const styles = states.config.styles;
netv.data({
nodes: rawNodes.map(
(node) => toStyledNode(node, styles.nodes.default)
),
links: rawLinks.map(
(link) => toStyledLink(link, styles.links.default)
)
});
} else if (states.step < data.graphs.length - 1 && states.frame < states.config.nMovingFrames) {
// (2) The next frame of current level
const curNodeMap = data.graphs[states.step].nodes;
const nextNodeMap = data.graphs[states.step + 1].nodes;
netv.nodes().forEach((node: any) => {
const from = curNodeMap.get(node.id());
if (!from) {
throw RangeError(`Error with node ID '${node.id()}.`);
}
const to = nextNodeMap.get(from.to as string);
if (!to) {
throw RangeError(`Error with node ID ${from.to}.`);
}
const lambda = states.frame / (states.config.nMovingFrames - 1);
node.x(from.x + lambda * (to.x - from.x));
node.y(from.y + lambda * (to.y - from.y));
});
} // Otherwise, nothing to todo
// Refreshes the canvas
netv.draw();
// Proceeding
states.frame += 1;
if (states.frame == nTotalFrames) {
states.frame = 0;
states.step += 1;
}
return (states.step < data.graphs.length);
}
function proceedShowingSeedsFrame(
netv: NetV,
seeds: Array<string> | Set<string>,
states: DisplayAnimationStates,
): boolean {
const nTotalFrames = states.config.nMovingFrames + states.config.nWaitingFrames;
const seedStyle = states.config.styles.nodes.seed;
if (states.frame === 0) {
seeds.forEach((s) => {
const node = netv.getNodeById(s);
if (node) {
updateNodeStyle(node, seedStyle);
} else {
console.warn(`Seed displaying error: Seed ${s} is missing.`);
}
});
netv.draw(); // Refreshes the canvas
}
// Proceeding
states.frame += 1;
if (states.frame >= nTotalFrames) {
states.frame = 0;
return false; // Stops
}
return true; // Continues
}
function proceedExpandingAnimationFrame(
netv: NetV,
data: DisplayData,
states: DisplayAnimationStates
): boolean {
if (states.step > data.graphs.length - 1) {
states.step = data.graphs.length - 1;
states.frame = 0;
}
// Stops if the last layer is reached.
if (states.step < 0) {
return false;
}
const nTotalFrames = states.config.nMovingFrames + states.config.nWaitingFrames;
const styles = states.config.styles;
if (states.frame === 0) {
// (1) The first frame of new level
const curNodeMap = data.graphs[states.step].nodes;
const nonSeedStyle = (node: NodeData) => node.style ?? styles.nodes.default;
if (states.step > 0) {
// (1.1) Not the first level
const nodes: Array<DisplayNodeData> = [];
data.graphs[states.step - 1].nodes.forEach((node, id) => {
const from = curNodeMap.get(node.to as string);
if (from === undefined) {
throw Error(`Error with node ID '${node.to}'.`);
}
const style = data.seeds.cwim[states.step - 1].has(id) ? styles.nodes.seed : nonSeedStyle(node);
nodes.push({id: id, x: from.x, y: from.y, style: style});
});
netv.data({
nodes: nodes,
links: data.graphs[states.step - 1].links.map(
(link) => toStyledLink(link, styles.links.default)
)
});
} else {
// (1.2) The first level
const nodes: Array<DisplayNodeData> = [];
data.graphs[0].nodes.forEach((node, id) => {
const style = data.seeds.cwim[0].has(id) ? styles.nodes.seed : nonSeedStyle(node);
nodes.push({id: id, x: node.x, y: node.y, style: style});
});
netv.data({
nodes: nodes,
links: data.graphs[0].links.map(
(link) => toStyledLink(link, styles.links.default)
)
});
}
} else if (states.step > 0 && states.frame < states.config.nMovingFrames) {
// (2) The next frame of current level
const prevNodeMap = data.graphs[states.step - 1].nodes;
netv.nodes().forEach((node: any) => {
const from = prevNodeMap.get(node.id());
if (!from) {
throw Error(`Error with ID '${node.id()}.`);
}
const lambda = states.frame / (states.config.nMovingFrames - 1);
const x = node.x();
const y = node.y();
node.x(x + lambda * (from.x - x));
node.y(y + lambda * (from.y - y));
});
} // Otherwise, nothing to todo
// Refreshes the canvas
netv.draw();
// Proceeding
states.frame += 1;
if (states.frame == nTotalFrames) {
states.frame = 0;
states.step -= 1;
}
return (states.step >= 0);
}
function resetGraphCanvas(graphCanvas: NetV, data: DisplayData, config: AnimationConfig): void {
drawInitialGraphCanvas(graphCanvas, data, config);
}
function resetAnimatingCanvas(animatingCanvas: NetV): void {
animatingCanvas.data({ nodes: [], links: [] });
animatingCanvas.draw(); // Resets with empty data
}
function updateGraphCanvasByVisited(
graphCanvas: NetV,
toStep: number,
simData: Array<Array<LinkData>>,
states: DisplayAnimationStates,
): void {
if (states.nVisitedSteps >= toStep) {
return; // No-op
}
const visitedStyle = states.config.styles.nodes.visited;
for (; states.nVisitedSteps < toStep; ++states.nVisitedSteps) {
simData[states.nVisitedSteps].forEach((link) => {
const node = graphCanvas.getNodeById(link.target);
if (node) {
updateNodeStyle(node, visitedStyle);
} else {
console.warn(`Missing node with ID '${link.target}'.`);
}
});
}
graphCanvas.draw();
}
function proceedSimulatingAnimationFrame(
graphCanvas: NetV,
animatingCanvas: NetV,
data: DisplayData,
states: DisplayAnimationStates,
algorithmKey: IMAlgorithmKey,
): boolean {
const config = states.config;
const simData = data.simulation[algorithmKey];
const maxNSteps = d3.max(Object.entries(data.simulation).map(([_, arr]) => arr.length)) as number;
const nTotalFrames = config.nMovingFrames + config.nWaitingFrames;
if (simData.length === 0) {
console.warn("linksByStep is empty. Nothing for animation.");
return false; // Stops right now
}
// One extra step for resetting
if (states.step >= maxNSteps) {
if (states.frame === 0) {
resetGraphCanvas(graphCanvas, data, states.config);
resetAnimatingCanvas(animatingCanvas);
}
states.frame += 1;
if (states.frame >= nTotalFrames) {
states.step = states.nVisitedSteps = states.frame = 0;
return false; // Finishes
}
return true; // Continues
}
// Waiting for (1) other canvas; (2) animation pause interval
if (states.step >= simData.length || states.frame >= config.nMovingFrames) {
states.frame += 1;
if (states.frame >= nTotalFrames) {
states.step += 1;
states.frame = 0;
}
return true;
}
const lambda = states.frame / (config.nMovingFrames - 1);
const animationData: NodeLinkData = { nodes: [], links: [] };
// Animation Links
const animatingLinkStyle = config.styles.links.animating;
if (animatingLinkStyle.strokeColor) {
const strokeColor = animatingLinkStyle.strokeColor;
const linkStyle = {
strokeColor: {
r: strokeColor.r,
g: strokeColor.g,
b: strokeColor.b,
a: strokeColor.a * (1.0 - lambda)
},
strokeWidth: animatingLinkStyle.strokeWidth
};
if (states.frame === 0) {
animationData.links = simData[states.step].map((link) => {
return { source: link.source, target: link.target, style: linkStyle };
});
} else {
animatingCanvas.links().forEach((link: any) => {
link.strokeColor(linkStyle.strokeColor); // Modified intrusively
});
}
}
// Animation nodes
const pastRatioFn = () => {
if (config.nAnimatingNodesPerLink <= 1) {
return [1.0];
}
return d3.range(config.nAnimatingNodesPerLink).map((j) => {
const speed = 1.0 + j * (config.animatingNodeMaxSpeed - 1.0) / (config.nAnimatingNodesPerLink - 1);
return Math.min(1.0, speed * lambda);
});
};
const pastRatio = pastRatioFn();
// The first animation node reaches the destination.
if (pastRatio[pastRatio.length - 1] >= 1.0) {
updateGraphCanvasByVisited(graphCanvas, states.step + 1, simData, states);
}
const currentAnimatingLocationsOf = (link: LinkData) => {
const source = graphCanvas.getNodeById(link.source);
if (!source) {
throw Error(`Invalid link source '${link.source}'.`);
}
const sx = source.x();
const sy = source.y();
const target = graphCanvas.getNodeById(link.target);
if (!target) {
throw Error(`Invalid link target '${link.target}.`);
}
const tx = target.x();
const ty = target.y();
return pastRatio.map((r) => [sx + r * (tx - sx), sy + r * (ty - sy)]);
};
if (states.frame === 0) {
// (1) Animating nodes
simData[states.step].forEach((link: LinkData, i: number) => {
currentAnimatingLocationsOf(link).forEach(([x, y], j) => {
const newNode = { id: `$animating-${i}-${j}`, x: x, y: y, style: config.styles.nodes.animating };
animationData.nodes.push(newNode);
});
});
// (2) Link ends (Invisible)
const involvedNodeIDs = new Set<string>();
simData[states.step].forEach(({source, target}) => {
involvedNodeIDs.add(source);
involvedNodeIDs.add(target);
});
involvedNodeIDs.forEach((id) => {
const node = graphCanvas.getNodeById(id);
if (!node) {
throw Error(`Invalid node ID '${id}'.`);
}
const x = node.x();
const y = node.y();
const invisibleStyle = { r: 0, strokeWidth: 0, fill: { r: 0, g: 0, b: 0, a: 0 } };
animationData.nodes.push({ id: id, x: x, y: y, style: invisibleStyle });
});
} else {
// Updates locations of animating nodes intrusively
const nodes = animatingCanvas.nodes();
simData[states.step].forEach((link, i) => {
currentAnimatingLocationsOf(link).forEach(([x, y], j) => {
const cur = nodes[i * config.nAnimatingNodesPerLink + j];
cur.x(x);
cur.y(y); // Updates (x, y) intrusively
});
});
}
if (states.frame === 0) {
animatingCanvas.data(animationData);
}
animatingCanvas.draw();
// Proceeding
states.frame += 1;
if (states.frame >= nTotalFrames) {
states.step += 1;
states.frame = 0;
}
return true;
}
export function proceedCWIMAnimationFrame(
graphCanvas: NetV,
animatingCanvas: NetV,
data: DisplayData,
states: DisplayAnimationStates
): void {
const doesProceed = states.autoStarts || states.manualStarts || states.frame > 0;
if (!doesProceed) {
return; // No-op
}
states.manualStarts = false;
if (states.phase === 'INITIAL') {
states.phase = 'COARSENING';
}
// Animation phase loop
if (states.phase === 'COARSENING') {
if (!proceedCoarseningAnimationFrame(graphCanvas, data, states)) {
states.phase = 'SHOWING_SEEDS';
console.log("COARSENING => SHOWING_SEEDS");
}
} else if (states.phase === 'SHOWING_SEEDS') {
const seeds = data.seeds.cwim[data.seeds.cwim.length - 1];
const continues = proceedShowingSeedsFrame(graphCanvas, seeds, states);
if (!continues) {
states.phase = 'WAITING';
console.log("SHOWING_SEEDS => WAITING");
}
} else if (states.phase === 'WAITING') {
const nFrames = states.config.nMovingFrames + states.config.nWaitingFrames;
const continues = proceedNoOpAnimationFrame(states, nFrames);
if (!continues) {
states.phase = 'EXPANDING';
console.log("WAITING => EXPANDING");
}
} else if (states.phase === 'EXPANDING') {
if (!proceedExpandingAnimationFrame(graphCanvas, data, states)) {
states.phase = 'SIMULATING';
states.step = states.nVisitedSteps = states.frame = 0;
console.log("EXPANDING => SIMULATING");
}
} else if (states.phase === 'SIMULATING') {
if (!proceedSimulatingAnimationFrame(graphCanvas, animatingCanvas, data, states, 'cwim')) {
states.phase = 'COARSENING';
console.log("SIMULATING => COARSENING");
}
} else {
throw TypeError(`Invalid animation phase '${states.phase}'.`);
}
}
export function proceedContrastAnimationFrame(
graphCanvas: NetV,
animatingCanvas: NetV,
data: DisplayData,
states: DisplayAnimationStates
): void {
const doesProceed = states.autoStarts || states.manualStarts || states.frame > 0;
if (!doesProceed) {
return; // No-op
}
states.manualStarts = false;
if (states.phase === 'INITIAL') {
states.phase = 'WAITING';
}
// Animation phase loop
const nFrames = states.config.nMovingFrames + states.config.nWaitingFrames;
if (states.phase === 'WAITING') {
if (states.frame === 0 && states.step === 0) {
drawInitialGraphCanvas(graphCanvas, data, states.config);
}
const nLevels = data.graphs.length * 2 + 1;
if (!proceedNoOpAnimationFrame(states, nFrames, nLevels)) {
states.phase = 'SHOWING_SEEDS';
console.log("Contrast: WAITING (no-op) => SHOWING_SEEDS");
}
} else if (states.phase === 'SHOWING_SEEDS') {
const continues = proceedShowingSeedsFrame(graphCanvas, data.seeds.contrast, states);
if (!continues) {
states.phase = 'SIMULATING';
console.log("Contrast: SHOWING_SEEDS => SIMULATING");
}
} else if (states.phase === 'SIMULATING') {
if (!proceedSimulatingAnimationFrame(graphCanvas, animatingCanvas, data, states, 'contrast')) {
states.phase = 'WAITING';
console.log("Contrast: SIMULATING => WAITING (no-op)");
}
} else {
throw TypeError(`Invalid animation phase '${states.phase}'.`);
}
}
export function getProceedAnimationFrameFn(algorithmKey: string) {
if (algorithmKey === 'cwim') {
return proceedCWIMAnimationFrame;
} else if (algorithmKey === 'contrast') {
return proceedContrastAnimationFrame;
} else {
throw RangeError(`Invalid algorithm key '${algorithmKey}'.`);
}
}
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}
{
"extends": "@tsconfig/node20/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*"
],
"compilerOptions": {
"composite": true,
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}
import argparse
import json
import networkx as nx
import numpy as np
import random
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument(
'-n', '--n-nodes', type=int, help='# of nodes', required=True)
parser.add_argument(
'--n-links-per-node', type=int, help='# of links', required=True)
parser.add_argument(
'-p', '--p-triangle', type=float, help='Probability of adding a triangle after adding a random edge', required=True)
parser.add_argument(
'-k', '--n-seeds', type=int, help='# of seed vertices to be selected', required=True)
parser.add_argument(
'--alpha', type=float, help='Scale factor of edge propability during simulation', default=1.0)
parser.add_argument(
'--coarsening-threshold', type=int, help='Maximum # of nodes after coarsening', required=True)
parser.add_argument(
'--min-approximation-ratio', type=float, help='Mimimum approximation ratio', default=0.95)
parser.add_argument(
'--max-approximation-ratio', type=float, help='Maximum approximation ratio', default=1.1)
parser.add_argument(
'--layout-epsilon', type=float, help='Ratio of layout blank', default=0.1)
parser.add_argument(
'-o', '--output-file', type=str, help='JSON output file', default='')
args = parser.parse_args()
# Manual checking
# (1) 1 <= k <= T <= n
if not 1 <= args.n_seeds <= args.coarsening_threshold <= args.n_nodes:
raise Exception(f"1 <= n_seeds ({args.n_seeds} given) <= coarsening_threshold ({args.coarsening_threshold} given) "
f"<= |V| ({args.n_nodes} given) expected.")
# (2) p in [0, 1]
if not 0.0 <= args.p_triangle <= 1.0:
raise Exception(f"--p-triangle must fall in range [0, 1], while {args.p_triangle} is given.")
# (3) eps in [0, 1]
if not 0.0 <= args.layout_epsilon <= 1.0:
raise Exception(f"--layout-epsilon must fall in range [0, 1], while {args.layout_epsilon} is given.")
return args
def generate_initial_graph(n: int, m: int, p: float):
while True:
# Unidirectional graph
G = nx.powerlaw_cluster_graph(n, m, p)
if nx.is_connected(G):
n = G.number_of_nodes()
if n <= 20:
print(f"Result graph:")
for i in range(n):
print(f"\tG[{i}] = {list(G[i])}")
return G
# Assumes the graph G is connected
def coarsen_graph(G: nx.Graph):
n = G.number_of_nodes()
matches = [-1] * n
adopts = [-1] * n
# Step 1: Random Matching
for v in range(n):
if matches[v] != -1:
continue
candidates = []
for u in G[v]:
if matches[u] == -1:
candidates.append(u)
if len(candidates) > 0:
u = random.choice(candidates)
matches[u] = v
matches[v] = u
# Step 2: 3-Brotherly Matching or Adoptive Matching
for v in range(n):
if matches[v] != -1:
continue
pivot_candidates = []
for u in G[v]:
if matches[u] == -1:
raise Exception(f"Implementation error: Unmatched neighbors {u} and {v}.")
pivot_candidates.append(u)
# Corner case: v is isolated
if len(pivot_candidates) == 0:
matches[v] = v
continue
# Otherwise, moves the focus to a random pivot
pivot = random.choice(pivot_candidates)
unmatched_neighbors = []
for u in G[pivot]:
if matches[u] == -1:
unmatched_neighbors.append(u)
s = len(unmatched_neighbors)
if s <= 0:
raise Exception("Implementation error: no unmatched neighbors.")
elif s == 1:
# 2.1: Adoptive Matching
r = unmatched_neighbors[0]
x = matches[pivot]
ap, ax = adopts[pivot], adopts[x]
if ap != -1 and ax != -1:
raise Exception(f"Implementation error: Matched neighbors {pivot} and {x} "
f"have both adopted {ap} and {ax} respectively.")
if ap == -1 and ax == -1:
adopts[pivot] = r
matches[r] = pivot # Temporary single-direction matching relationship
elif ap != -1:
adopts[pivot] = -1
matches[r], matches[ap] = ap, r
else:
adopts[x] = -1
matches[r], matches[ax] = ax, r
else:
if s % 3 == 0:
head = 0 # 3, 3, 3...
elif s % 3 == 1:
head = 4 # 2, 2, 3, 3, 3...
else:
head = 2 # 2, 3, 3, 3...
# Groups the first by size 2
for i in range(0, head, 2):
a, b = unmatched_neighbors[i:i + 2]
matches[a], matches[b] = b, a
# Groups the rest by size 3
for i in range(head, s, 3):
a, b, c = unmatched_neighbors[i:i + 3]
matches[a], matches[b], matches[c] = b, c, a # Circular matching notation
# Step 3: Grouping nodes
res = [-1 for _ in range(n)]
next_index = -1
for v in range(n):
if res[v] != -1:
continue
next_index += 1
if matches[v] == v:
res[v] = next_index
continue
u = matches[v]
if matches[u] == v:
res[u] = res[v] = next_index
for a in [adopts[v], adopts[u]]:
if a != -1:
res[a] = next_index
continue
x = matches[u]
res[u] = res[v] = res[x] = next_index
# Step 4: Grouping Links
links = set()
for u, v in nx.edges(G):
y, z = res[u], res[v]
if y < z:
links.add((y, z))
elif y > z:
links.add((z, y))
# (nC, group_index, links), where nC = # of nodes after coarsening,
# group_index[v] = The node index to which v is coarsened
return next_index, res, list(links)
args = parse_args()
# Unidirectional graph
G = generate_initial_graph(args.n_nodes, args.n_links_per_node, args.p_triangle)
coarsened_levels = [G]
coarsened_group_info = []
results = {
"graphs": [],
"seeds": {
"cwim": [],
"contrast": [],
},
"simulation": {
"cwim": 0,
"contrast": 0,
}
}
def get_layout(G):
# For each node v, (x, y) = layout[v], x, y falls in range (-1, 1)
layout = nx.fruchterman_reingold_layout(G)
min_xy = min(map(lambda v: np.min(layout[v]), layout))
max_xy = max(map(lambda v: np.max(layout[v]), layout))
print(f"{min_xy = }, {max_xy = }")
# Transforms from (min, max) to (eps/2, 1 - eps/2)
A = (1.0 - args.layout_epsilon) / (max_xy - min_xy)
B = (args.layout_epsilon / 2) - A * min_xy
for v in layout:
layout[v] = layout[v] * A + B
min_xy = min(map(lambda v: np.min(layout[v]), layout))
max_xy = max(map(lambda v: np.max(layout[v]), layout))
print(f"After transforming: {min_xy = }, {max_xy = }")
return layout
# Step 1: Coarsening
while True:
prev = coarsened_levels[-1]
prev_level = len(coarsened_levels) - 1
print(f"{prev_level = }")
layout = get_layout(prev)
cur_step = {
'nodes': [],
'links': []
}
for v in prev.nodes:
cur_step['nodes'].append({
'id': f"{prev_level}-{v}",
'x': layout[v][0],
'y': layout[v][1]
})
for u, v in prev.edges:
cur_step['links'].append({
'source': f"{prev_level}-{u}",
'target': f"{prev_level}-{v}"
})
print(f"{nx.number_of_nodes(prev) = }")
if nx.number_of_nodes(prev) > args.coarsening_threshold:
n_coarsened, groups, coarsened_edges = coarsen_graph(prev)
print(f"Coarsening done. {n_coarsened = }")
coarsened_group_info.append((n_coarsened, groups))
for v in range(nx.number_of_nodes(prev)):
cur_step['nodes'][v]['to'] = f"{prev_level + 1}-{groups[v]}"
# Builds the coarsened graph
H = nx.Graph()
H.add_nodes_from(range(n_coarsened)) # Ensures that node indices are in ascending order
H.add_edges_from(coarsened_edges)
coarsened_levels.append(H)
results['graphs'].append(cur_step)
if nx.number_of_nodes(prev) <= args.coarsening_threshold:
break
# Step 2: Seed selection (by max-degree) & Seed expansion
n_levels = len(coarsened_levels)
results['seeds']['cwim'] = [[] for _ in range(n_levels)]
def get_seeds_max_degree(G: nx.Graph, n_seeds: int):
candidates = list(range(nx.number_of_nodes(G)))
candidates.sort(key=lambda x: G.degree[x], reverse=True)
# Takes the max-degree as result
return candidates[0:n_seeds]
def get_seeds(level: int):
seeds = results['seeds']['cwim']
if len(seeds[level]) > 0:
return seeds[level]
if level == n_levels - 1:
H = coarsened_levels[-1]
res = get_seeds_max_degree(H, args.n_seeds)
else:
next_seeds = get_seeds(level + 1)
print(f"Seed of level {level + 1} = {next_seeds}")
res = []
# Expands each seed s respectively
for s in next_seeds:
candidates = []
_, groups = coarsened_group_info[level]
for v in coarsened_levels[level].nodes():
if groups[v] == s:
candidates.append(v)
print(f"In level {level}: Candidate of seed {s} = {candidates}")
if len(candidates) <= 0:
raise Exception("Implementation error with candidates.")
expanded_s = max(candidates, key=lambda v: coarsened_levels[level].degree[v])
res.append(expanded_s)
seeds[level] = [f"{level}-{s}" for s in res]
return res
expanded_seeds = get_seeds(0)
contrast_seeds = get_seeds_max_degree(G, args.n_seeds)
results['seeds']['contrast'] = [f"0-{s}" for s in contrast_seeds]
def simulate(G: nx.Graph, seeds: list[int], alpha: float = 1.0):
n = nx.number_of_nodes(G)
p_in = [1.0 - (1.0 - 1.0 / G.degree[i]) ** alpha for i in range(n)]
vis = [-1] * n
for s in seeds:
vis[s] = 0
queue: list[tuple[int, int]] = [(0, s) for s in seeds]
res: list[list[object]] = []
pos = 0
while pos < len(queue):
t, cur = queue[pos]
pos += 1
for u in G[cur]:
if vis[u] == -1 and random.random() < p_in[u]:
vis[u] = t + 1
queue.append((t + 1, u))
if t < len(res):
res[t].append({'source': f"0-{cur}", 'target': f"0-{u}"})
else:
res.append([{'source': f"0-{cur}", 'target': f"0-{u}"}])
return len(queue), res
# Step 3: Simulation
while True:
ne, se = simulate(G, expanded_seeds, args.alpha)
nc, sc = simulate(G, contrast_seeds, args.alpha)
# Retries until given approximation ratio is reached.
r = ne / nc
if args.min_approximation_ratio <= r <= args.max_approximation_ratio:
print(f"Approximation ratio = {r} ({ne} vs {nc} traversed), with |V| = {nx.number_of_nodes(G)}.")
results['simulation']['cwim'] = se
results['simulation']['contrast'] = sc
break
json_str = json.dumps(results, indent=2)
if args.output_file == '':
print(json_str)
else:
with open(args.output_file, mode='w', newline='\n') as file:
file.write(json_str)
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment