Skip to content

Commit

Permalink
MCF based
Browse files Browse the repository at this point in the history
  • Loading branch information
michielbdejong committed Oct 17, 2024
1 parent 5975547 commit 8feccc2
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 68 deletions.
100 changes: 50 additions & 50 deletions src/BirdsEyeWorm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,63 +89,63 @@ export class BirdsEyeWorm {
await writeFile(this.solutionFile, '');
}

// eslint-disable-next-line no-constant-condition
let counter = 0;
while (counter++ < MAX_NUM_STEPS) {
// console.log('Step', path, newStep);
path.push(newStep);
// console.log('picking first option from', newStep);
// console.log(path);
const backtracked = [];
while (path.length > 0 && !this.graph.hasOutgoingLinks(path[path.length - 1])) {
// console.log('no outgoing links', path);
// backtrack
const previousStep = path.pop();
backtracked.push(previousStep);
if (path.length > 0) {
this.graph.removeLink(path[path.length - 1], previousStep);
}
}
// we now now that either newStep has outgoing links, or path is empty
if (path.length === 0) {
if (backtracked.length > 0) {
// this.printLine('finished ', path, backtracked.reverse());
try {
// eslint-disable-next-line no-constant-condition
while (counter++ < MAX_NUM_STEPS) {
// console.log('Step', path, newStep);
path.push(newStep);
// console.log('picking first option from', newStep);
// console.log(path);
const backtracked = [];
while (path.length > 0 && !this.graph.hasOutgoingLinks(path[path.length - 1])) {
// console.log('no outgoing links', path);
// backtrack
const previousStep = path.pop();
backtracked.push(previousStep);
if (path.length > 0) {
this.graph.removeLink(path[path.length - 1], previousStep);
}
}
// no paths left, start with a new worm
path = [];
try {
// we now now that either newStep has outgoing links, or path is empty
if (path.length === 0) {
if (backtracked.length > 0) {
// this.printLine('finished ', path, backtracked.reverse());
}
// no paths left, start with a new worm
path = [];
newStep = this.graph.getFirstNode();
} catch (e) {
if (e.message === 'Graph is empty') {
// We're done!
console.log(`Done after ${counter} steps`);
return;
} else {
throw e;
} else {
if (backtracked.length > 0) {
// this.printLine('backtracked', path, backtracked.reverse());
newStep = path[path.length - 1];
// console.log('continuing from', path, newStep);
}
}
} else {
if (backtracked.length > 0) {
// this.printLine('backtracked', path, backtracked.reverse());
newStep = path[path.length - 1];
// console.log('continuing from', path, newStep);
}

newStep = this.graph.getFirstNode(newStep);
// console.log('considering', path, newStep);
}
// check for loops in path
const pos = path.indexOf(newStep);
if (pos !== -1) {
const loop = path.splice(pos).concat(newStep);
const smallestWeight = this.netLoop(loop);
this.printLine(`found loop `, path, loop);
if (this.solutionFile) {
await appendFile(this.solutionFile, loop.slice(0, loop.length - 1).concat(smallestWeight).join(' ') + '\n');
newStep = this.graph.getFirstNode(newStep);
// console.log('considering', path, newStep);
}
// check for loops in path
const pos = path.indexOf(newStep);
if (pos !== -1) {
const loop = path.splice(pos).concat(newStep);
const smallestWeight = this.netLoop(loop);
this.printLine(`found loop `, path, loop);
if (this.solutionFile) {
await appendFile(this.solutionFile, loop.slice(0, loop.length - 1).concat(smallestWeight).join(' ') + '\n');
}

newStep = this.graph.getFirstNode(path[path.length - 1]);
// console.log(`Continuing with`, path, newStep);
newStep = this.graph.getFirstNode(path[path.length - 1]);
// console.log(`Continuing with`, path, newStep);
}
}
} catch (e) {
if (e.message === 'Graph is empty') {
// We're done!
console.log(`Done after ${counter} steps`);
return;
} else {
throw e;
}
}
}
Expand Down
19 changes: 4 additions & 15 deletions src/analyse-sarafu-challenge-solution.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,12 @@
import { readCsv } from "./readCsv.js";
import { scale } from "./util.js";

const DEBTFILE = process.argv[2] || '../debt.csv';
const DEBTFILE = process.argv[2] || './debt.csv';
const SOLUTIONFILE = process.argv[3] || './solution.csv';

const LEDGER_SCALE = 1000;
const ROUNDING_MARGIN = 0.0000001;

function scale(amountStr: string, filetype: string): number {
const amount = parseFloat(amountStr);
if (isNaN(amount)) {
throw new Error(`Cannot parse float '${amountStr}'`);
}
const scaledAmount = Math.round(amount * LEDGER_SCALE);
if (Math.abs(scaledAmount - amount * LEDGER_SCALE) > ROUNDING_MARGIN) {
throw new Error(`Ledger scale insufficient for amount in ${filetype} file: ${amountStr} -> ${amount * LEDGER_SCALE} -> ${scaledAmount}`);
}
return scaledAmount;
}

async function run(): Promise<void> {
const graph: {
[pair: string]: number;
Expand All @@ -38,7 +27,7 @@ async function run(): Promise<void> {
if (typeof graph[edge] === 'undefined') {
graph[edge] = 0;
}
const scaledAmount = scale(amountStr, 'debt');
const scaledAmount = scale(amountStr, 'debt', LEDGER_SCALE, ROUNDING_MARGIN);
graph[edge] += scaledAmount;
numTrans++;
totalAmount += scaledAmount;
Expand All @@ -57,7 +46,7 @@ async function run(): Promise<void> {
await readCsv(SOLUTIONFILE, ' ', (cells: string[]) => {
// cells would be e.g. ['8', '5', '21', '3', '5.3']
const amountStr = cells.pop();
const scaledAmount = scale(amountStr, 'solution');
const scaledAmount = scale(amountStr, 'solution', LEDGER_SCALE, ROUNDING_MARGIN);
const nodes = cells.concat(cells[0]);
// nodes would now be e.g. ['8', '5', '21', '3', 8']
let possible = true;
Expand Down
5 changes: 2 additions & 3 deletions src/birdsEyeChallenge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ import { createInterface } from 'readline';
import { createReadStream } from 'fs';
import { BirdsEyeWorm } from './BirdsEyeWorm.js';

// const SARAFU_CSV = '../Sarafu2021_UKdb_submission/sarafu_xDAI/sarafu_txns_20200125-20210615.csv';
const DEBTCSV = process.argv[2] || '../strategy-pit/debt.csv';
const SOLUTIONCSV = process.argv[3] || '../strategy-pit/solution.csv';
const DEBTCSV = process.argv[2] || './debt.csv';
const SOLUTIONCSV = process.argv[3] || './solution.csv';
console.log('Opening', DEBTCSV);

const lineReader = createInterface({
Expand Down
82 changes: 82 additions & 0 deletions src/mcfBased.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { createInterface } from 'readline';
import { createReadStream, promises } from 'fs';
import { Edge, minCostFlow } from 'min-cost-flow';
import { scale } from './util.js';

const LEDGER_SCALE = 1000;
const ROUNDING_MARGIN = 0.0000001;

const DEBTCSV = process.argv[2] || './/debt.csv';
const SOLUTIONCSV = process.argv[3] || './solution.csv';
console.log('Opening', DEBTCSV);

const lineReader = createInterface({
input: createReadStream(DEBTCSV),
});
const graph: {
[pair: string]: number;
} = {};
const edges: Edge<string>[] = [];
lineReader.on('line', function (line) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [ from, to, amountStr ] = line.split(' ');
const amount = scale(amountStr, 'debt', LEDGER_SCALE, ROUNDING_MARGIN);
edges.push({
from,
to,
capacity: amount,
cost: 1,
} as Edge<string>);
const edge = `${from} ${to}`;
if (typeof graph[edge] === 'undefined') {
graph[edge] = 0;
}
const scaledAmount = scale(amountStr, 'debt', LEDGER_SCALE, ROUNDING_MARGIN);
graph[edge] += scaledAmount;
});

lineReader.on('close', async function () {
const netPositions: {
[nodeNum: string]: number;
}= {};
Object.keys(graph).forEach(pair => {
const participants = pair.split(' ');
participants.forEach(participant => {
if (typeof netPositions[participant] === 'undefined') {
netPositions[participant] = 0;
}
});
netPositions[participants[0]] -= graph[pair]; // a negative net position means outgoing transfers predominantly happened
netPositions[participants[1]] += graph[pair]; // a positive net position means incoming transfers predominantly happened
});
// console.log(netPositions);
Object.keys(netPositions).forEach(node => {
if (netPositions[node] < 0) { // a negative net position means outgoing transfers predominantly happened
// console.log(`adding link from source to ${node} for net position ${netPositions[node]}`);
edges.push({
from: 'SOURCE',
to: node,
capacity: -netPositions[node],
cost: 0
} as Edge<string>);
}
if (netPositions[node] > 0) { // a positive net position means incoming transfers predominantly happened
// console.log(`adding link from ${node} to drain for net position ${netPositions[node]}`);
edges.push({
from: node,
to: 'DRAIN',
capacity: netPositions[node],
cost: 0
} as Edge<string>);
}
});
console.log(`Starting Min Cost Flow algorithm on ${edges.length} edges`);
const result = minCostFlow(edges);
console.log(`Finished Min Cost Flow algorithm; determined flow for ${result.length} edges`);
await promises.writeFile(SOLUTIONCSV, '');
await Promise.all(result.map(async (edge: Edge<string>) => {
if (edge.capacity > edge.flow) {
return promises.appendFile(SOLUTIONCSV, `${edge.from} ${edge.to} ${(edge.capacity - edge.flow) / LEDGER_SCALE}\n`);
}
}));
});
12 changes: 12 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,16 @@ function createEpilogue(): string {
}
export function createPlantUml(log: Entry[]): string {
return createPreamble() + log.map(line => createLine(line)).join('') + createEpilogue();
}

export function scale(amountStr: string, filetype: string, LEDGER_SCALE: number, ROUNDING_MARGIN: number): number {
const amount = parseFloat(amountStr);
if (isNaN(amount)) {
throw new Error(`Cannot parse float '${amountStr}'`);
}
const scaledAmount = Math.round(amount * LEDGER_SCALE);
if (Math.abs(scaledAmount - amount * LEDGER_SCALE) > ROUNDING_MARGIN) {
throw new Error(`Ledger scale insufficient for amount in ${filetype} file: ${amountStr} -> ${amount * LEDGER_SCALE} -> ${scaledAmount}`);
}
return scaledAmount;
}

0 comments on commit 8feccc2

Please sign in to comment.