function SOMSim(sim, controls, {rows = 40, columns = 40, depth = 3, data = [], timeConstant = 1000, learningFactor = 0.1, distanceFactor = 40} = {}) {
if (controls.reset || !sim) {
sim = {};
sim.timeConstant = timeConstant;
sim.learningFactor = learningFactor;
sim.distanceFactor = distanceFactor;
sim.rows = rows;
sim.columns = columns;
sim.depth = depth;
sim.som = math.random([sim.rows, sim.columns, sim.depth], 0.05, 0.95);
sim.data = data;
sim.inputs = data.length;
sim.step = 0;
} else {
sim.step++;
const datum = sim.data[math.randomInt(0, sim.inputs)];
const best = bestMatchNode(datum, sim.som);
const stepUpdate = Math.exp(-sim.step / sim.timeConstant);
const stepUpdate2 = Math.exp(-sim.step / (sim.timeConstant * 2));
const distanceRate = distanceFactor * stepUpdate;
const learningRate = learningFactor * stepUpdate2;
for (let row = 0; row < sim.rows; row++) {
for (let column = 0; column < sim.columns; column++) {
const nodeDistance = euclideanDistance([best.row, best.column], [row, column]);
const influence = Math.exp(-Math.pow(nodeDistance, 2) / (2 * Math.pow(distanceRate, 2)));
for (let depth = 0; depth < sim.depth; depth++) {
sim.som[row][column][depth] += learningRate * influence * (datum[depth] - sim.som[row][column][depth]);
}
}
}
}
return sim;
}