switch to web app
This commit is contained in:
680
js/matching-algorithms.js
Normal file
680
js/matching-algorithms.js
Normal file
@ -0,0 +1,680 @@
|
||||
/**
|
||||
* LiXX Cell Pack Matcher - Matching Algorithms
|
||||
*
|
||||
* Implements optimized algorithms for lithium cell matching:
|
||||
* - Genetic Algorithm (default, fast)
|
||||
* - Simulated Annealing
|
||||
* - Exhaustive search (for small configurations)
|
||||
*
|
||||
* Based on research:
|
||||
* - Shi et al., 2013: "Internal resistance matching for parallel-connected
|
||||
* lithium-ion cells and impacts on battery pack cycle life"
|
||||
* DOI: 10.1016/j.jpowsour.2013.11.064
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// Utility Functions
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Calculate the coefficient of variation (CV) as a percentage.
|
||||
* CV = (standard deviation / mean) * 100
|
||||
* @param {number[]} values - Array of numeric values
|
||||
* @returns {number} CV as percentage, or 0 if invalid
|
||||
*/
|
||||
function coefficientOfVariation(values) {
|
||||
if (!values || values.length === 0) return 0;
|
||||
const mean = values.reduce((a, b) => a + b, 0) / values.length;
|
||||
if (mean === 0) return 0;
|
||||
const variance = values.reduce((acc, val) => acc + Math.pow(val - mean, 2), 0) / values.length;
|
||||
return (Math.sqrt(variance) / mean) * 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuffle array in place using Fisher-Yates algorithm.
|
||||
* @param {Array} array - Array to shuffle
|
||||
* @returns {Array} The same array, shuffled
|
||||
*/
|
||||
function shuffleArray(array) {
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep clone an array of arrays.
|
||||
* @param {Array[]} arr - Array to clone
|
||||
* @returns {Array[]} Cloned array
|
||||
*/
|
||||
function cloneConfiguration(arr) {
|
||||
return arr.map(group => [...group]);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Scoring Functions
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Calculate the match score for a pack configuration.
|
||||
* Lower score = better match.
|
||||
*
|
||||
* The score combines:
|
||||
* - Capacity variance between parallel groups (weighted by capacityWeight)
|
||||
* - Internal resistance variance within parallel groups (weighted by irWeight)
|
||||
*
|
||||
* @param {Object[][]} configuration - Array of parallel groups, each containing cell objects
|
||||
* @param {number} capacityWeight - Weight for capacity matching (0-1)
|
||||
* @param {number} irWeight - Weight for IR matching (0-1)
|
||||
* @returns {Object} Score breakdown
|
||||
*/
|
||||
function calculateScore(configuration, capacityWeight = 0.7, irWeight = 0.3) {
|
||||
// Calculate total capacity for each parallel group
|
||||
const groupCapacities = configuration.map(group =>
|
||||
group.reduce((sum, cell) => sum + cell.capacity, 0)
|
||||
);
|
||||
|
||||
// Calculate average IR for each parallel group
|
||||
const groupIRs = configuration.map(group => {
|
||||
const irsWithValues = group.filter(cell => cell.ir !== null && cell.ir !== undefined);
|
||||
if (irsWithValues.length === 0) return null;
|
||||
return irsWithValues.reduce((sum, cell) => sum + cell.ir, 0) / irsWithValues.length;
|
||||
}).filter(ir => ir !== null);
|
||||
|
||||
// Calculate IR variance within each parallel group (important for parallel cells)
|
||||
const withinGroupIRVariances = configuration.map(group => {
|
||||
const irsWithValues = group.filter(cell => cell.ir !== null && cell.ir !== undefined);
|
||||
if (irsWithValues.length < 2) return 0;
|
||||
const irs = irsWithValues.map(cell => cell.ir);
|
||||
return coefficientOfVariation(irs);
|
||||
});
|
||||
|
||||
// Capacity CV between groups (should be low for balanced pack)
|
||||
const capacityCV = coefficientOfVariation(groupCapacities);
|
||||
|
||||
// Average IR CV within groups (should be low for parallel cells)
|
||||
const avgWithinGroupIRCV = withinGroupIRVariances.length > 0
|
||||
? withinGroupIRVariances.reduce((a, b) => a + b, 0) / withinGroupIRVariances.length
|
||||
: 0;
|
||||
|
||||
// Combined score (lower is better)
|
||||
const score = (capacityWeight * capacityCV) + (irWeight * avgWithinGroupIRCV);
|
||||
|
||||
return {
|
||||
score,
|
||||
capacityCV,
|
||||
irCV: avgWithinGroupIRCV,
|
||||
groupCapacities,
|
||||
groupIRs,
|
||||
withinGroupIRVariances
|
||||
};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Genetic Algorithm
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Genetic Algorithm for cell matching.
|
||||
* Fast and effective for most configurations.
|
||||
*/
|
||||
class GeneticAlgorithm {
|
||||
/**
|
||||
* @param {Object[]} cells - Array of cell objects {label, capacity, ir}
|
||||
* @param {number} serial - Number of series groups
|
||||
* @param {number} parallel - Number of cells in parallel per group
|
||||
* @param {Object} options - Algorithm options
|
||||
*/
|
||||
constructor(cells, serial, parallel, options = {}) {
|
||||
this.cells = cells;
|
||||
this.serial = serial;
|
||||
this.parallel = parallel;
|
||||
this.totalCellsNeeded = serial * parallel;
|
||||
|
||||
// Options with defaults
|
||||
this.populationSize = options.populationSize || 50;
|
||||
this.maxIterations = options.maxIterations || 5000;
|
||||
this.mutationRate = options.mutationRate || 0.15;
|
||||
this.eliteCount = options.eliteCount || 5;
|
||||
this.capacityWeight = options.capacityWeight ?? 0.7;
|
||||
this.irWeight = options.irWeight ?? 0.3;
|
||||
this.onProgress = options.onProgress || (() => { });
|
||||
|
||||
this.stopped = false;
|
||||
this.bestSolution = null;
|
||||
this.bestScore = Infinity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the algorithm.
|
||||
*/
|
||||
stop() {
|
||||
this.stopped = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a random individual (configuration).
|
||||
* @param {Object[]} cellPool - Cells to choose from
|
||||
* @returns {Object[][]} Configuration
|
||||
*/
|
||||
createIndividual(cellPool) {
|
||||
const shuffled = shuffleArray([...cellPool]).slice(0, this.totalCellsNeeded);
|
||||
const configuration = [];
|
||||
|
||||
for (let i = 0; i < this.serial; i++) {
|
||||
const group = [];
|
||||
for (let j = 0; j < this.parallel; j++) {
|
||||
group.push(shuffled[i * this.parallel + j]);
|
||||
}
|
||||
configuration.push(group);
|
||||
}
|
||||
|
||||
return configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert configuration to flat array of cell indices for crossover.
|
||||
* @param {Object[][]} config - Configuration
|
||||
* @returns {number[]} Flat array of cell indices
|
||||
*/
|
||||
configToIndices(config) {
|
||||
const flat = config.flat();
|
||||
return flat.map(cell => this.cells.findIndex(c => c.label === cell.label));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert indices back to configuration.
|
||||
* @param {number[]} indices - Array of cell indices
|
||||
* @returns {Object[][]} Configuration
|
||||
*/
|
||||
indicesToConfig(indices) {
|
||||
const configuration = [];
|
||||
for (let i = 0; i < this.serial; i++) {
|
||||
const group = [];
|
||||
for (let j = 0; j < this.parallel; j++) {
|
||||
const idx = indices[i * this.parallel + j];
|
||||
group.push(this.cells[idx]);
|
||||
}
|
||||
configuration.push(group);
|
||||
}
|
||||
return configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform crossover between two parents using Order Crossover (OX).
|
||||
* @param {number[]} parent1 - First parent indices
|
||||
* @param {number[]} parent2 - Second parent indices
|
||||
* @returns {number[]} Child indices
|
||||
*/
|
||||
crossover(parent1, parent2) {
|
||||
const length = parent1.length;
|
||||
const start = Math.floor(Math.random() * length);
|
||||
const end = start + Math.floor(Math.random() * (length - start));
|
||||
|
||||
const child = new Array(length).fill(-1);
|
||||
const usedIndices = new Set();
|
||||
|
||||
// Copy segment from parent1
|
||||
for (let i = start; i <= end; i++) {
|
||||
child[i] = parent1[i];
|
||||
usedIndices.add(parent1[i]);
|
||||
}
|
||||
|
||||
// Fill remaining from parent2
|
||||
let childIdx = (end + 1) % length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
const parent2Idx = (end + 1 + i) % length;
|
||||
if (!usedIndices.has(parent2[parent2Idx])) {
|
||||
while (child[childIdx] !== -1) {
|
||||
childIdx = (childIdx + 1) % length;
|
||||
}
|
||||
child[childIdx] = parent2[parent2Idx];
|
||||
usedIndices.add(parent2[parent2Idx]);
|
||||
childIdx = (childIdx + 1) % length;
|
||||
}
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutate an individual by swapping cells.
|
||||
* @param {number[]} indices - Individual indices
|
||||
* @param {Object[]} unusedCells - Cells not in this configuration
|
||||
* @returns {number[]} Mutated indices
|
||||
*/
|
||||
mutate(indices, unusedCells) {
|
||||
const mutated = [...indices];
|
||||
|
||||
if (Math.random() < this.mutationRate) {
|
||||
if (unusedCells.length > 0 && Math.random() < 0.3) {
|
||||
// Replace a cell with an unused one
|
||||
const replaceIdx = Math.floor(Math.random() * mutated.length);
|
||||
const unusedCell = unusedCells[Math.floor(Math.random() * unusedCells.length)];
|
||||
const unusedIdx = this.cells.findIndex(c => c.label === unusedCell.label);
|
||||
mutated[replaceIdx] = unusedIdx;
|
||||
} else {
|
||||
// Swap two cells within the configuration
|
||||
const i = Math.floor(Math.random() * mutated.length);
|
||||
const j = Math.floor(Math.random() * mutated.length);
|
||||
[mutated[i], mutated[j]] = [mutated[j], mutated[i]];
|
||||
}
|
||||
}
|
||||
|
||||
return mutated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the genetic algorithm.
|
||||
* @returns {Promise<Object>} Best solution found
|
||||
*/
|
||||
async run() {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Initialize population
|
||||
let population = [];
|
||||
for (let i = 0; i < this.populationSize; i++) {
|
||||
population.push(this.createIndividual(this.cells));
|
||||
}
|
||||
|
||||
// Evaluate initial population
|
||||
let evaluated = population.map(config => ({
|
||||
config,
|
||||
indices: this.configToIndices(config),
|
||||
...calculateScore(config, this.capacityWeight, this.irWeight)
|
||||
}));
|
||||
|
||||
// Sort by score
|
||||
evaluated.sort((a, b) => a.score - b.score);
|
||||
|
||||
if (evaluated[0].score < this.bestScore) {
|
||||
this.bestScore = evaluated[0].score;
|
||||
this.bestSolution = evaluated[0];
|
||||
}
|
||||
|
||||
// Main evolution loop
|
||||
for (let iteration = 0; iteration < this.maxIterations && !this.stopped; iteration++) {
|
||||
// Selection (tournament selection)
|
||||
const newPopulation = [];
|
||||
|
||||
// Keep elite individuals
|
||||
for (let i = 0; i < this.eliteCount && i < evaluated.length; i++) {
|
||||
newPopulation.push(evaluated[i].indices);
|
||||
}
|
||||
|
||||
// Generate rest through crossover and mutation
|
||||
while (newPopulation.length < this.populationSize) {
|
||||
// Tournament selection
|
||||
const tournament1 = evaluated.slice(0, Math.ceil(evaluated.length / 2));
|
||||
const tournament2 = evaluated.slice(0, Math.ceil(evaluated.length / 2));
|
||||
const parent1 = tournament1[Math.floor(Math.random() * tournament1.length)];
|
||||
const parent2 = tournament2[Math.floor(Math.random() * tournament2.length)];
|
||||
|
||||
// Crossover
|
||||
let child = this.crossover(parent1.indices, parent2.indices);
|
||||
|
||||
// Determine unused cells
|
||||
const usedLabels = new Set(child.map(idx => this.cells[idx].label));
|
||||
const unusedCells = this.cells.filter(c => !usedLabels.has(c.label));
|
||||
|
||||
// Mutation
|
||||
child = this.mutate(child, unusedCells);
|
||||
|
||||
newPopulation.push(child);
|
||||
}
|
||||
|
||||
// Evaluate new population
|
||||
evaluated = newPopulation.map(indices => {
|
||||
const config = this.indicesToConfig(indices);
|
||||
return {
|
||||
config,
|
||||
indices,
|
||||
...calculateScore(config, this.capacityWeight, this.irWeight)
|
||||
};
|
||||
});
|
||||
|
||||
// Sort by score
|
||||
evaluated.sort((a, b) => a.score - b.score);
|
||||
|
||||
// Update best solution
|
||||
if (evaluated[0].score < this.bestScore) {
|
||||
this.bestScore = evaluated[0].score;
|
||||
this.bestSolution = evaluated[0];
|
||||
}
|
||||
|
||||
// Progress callback
|
||||
if (iteration % 50 === 0 || iteration === this.maxIterations - 1) {
|
||||
this.onProgress({
|
||||
iteration,
|
||||
maxIterations: this.maxIterations,
|
||||
bestScore: this.bestScore,
|
||||
currentBest: this.bestSolution,
|
||||
elapsedTime: Date.now() - startTime
|
||||
});
|
||||
|
||||
// Allow UI to update
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
}
|
||||
}
|
||||
|
||||
// Determine excluded cells
|
||||
const usedLabels = new Set(this.bestSolution.config.flat().map(c => c.label));
|
||||
const excludedCells = this.cells.filter(c => !usedLabels.has(c.label));
|
||||
|
||||
return {
|
||||
configuration: this.bestSolution.config,
|
||||
score: this.bestScore,
|
||||
capacityCV: this.bestSolution.capacityCV,
|
||||
irCV: this.bestSolution.irCV,
|
||||
groupCapacities: this.bestSolution.groupCapacities,
|
||||
excludedCells,
|
||||
iterations: this.maxIterations,
|
||||
elapsedTime: Date.now() - startTime
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Simulated Annealing
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Simulated Annealing algorithm for cell matching.
|
||||
* Good for escaping local minima.
|
||||
*/
|
||||
class SimulatedAnnealing {
|
||||
/**
|
||||
* @param {Object[]} cells - Array of cell objects
|
||||
* @param {number} serial - Number of series groups
|
||||
* @param {number} parallel - Number of cells in parallel per group
|
||||
* @param {Object} options - Algorithm options
|
||||
*/
|
||||
constructor(cells, serial, parallel, options = {}) {
|
||||
this.cells = cells;
|
||||
this.serial = serial;
|
||||
this.parallel = parallel;
|
||||
this.totalCellsNeeded = serial * parallel;
|
||||
|
||||
this.maxIterations = options.maxIterations || 5000;
|
||||
this.initialTemp = options.initialTemp || 100;
|
||||
this.coolingRate = options.coolingRate || 0.995;
|
||||
this.capacityWeight = options.capacityWeight ?? 0.7;
|
||||
this.irWeight = options.irWeight ?? 0.3;
|
||||
this.onProgress = options.onProgress || (() => { });
|
||||
|
||||
this.stopped = false;
|
||||
this.bestSolution = null;
|
||||
this.bestScore = Infinity;
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.stopped = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create initial configuration.
|
||||
*/
|
||||
createInitialConfig() {
|
||||
const shuffled = shuffleArray([...this.cells]).slice(0, this.totalCellsNeeded);
|
||||
const configuration = [];
|
||||
|
||||
for (let i = 0; i < this.serial; i++) {
|
||||
const group = [];
|
||||
for (let j = 0; j < this.parallel; j++) {
|
||||
group.push(shuffled[i * this.parallel + j]);
|
||||
}
|
||||
configuration.push(group);
|
||||
}
|
||||
|
||||
return configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a neighbor solution by making a small change.
|
||||
*/
|
||||
getNeighbor(config) {
|
||||
const newConfig = cloneConfiguration(config);
|
||||
const usedLabels = new Set(config.flat().map(c => c.label));
|
||||
const unusedCells = this.cells.filter(c => !usedLabels.has(c.label));
|
||||
|
||||
const moveType = Math.random();
|
||||
|
||||
if (unusedCells.length > 0 && moveType < 0.3) {
|
||||
// Replace a cell with an unused one
|
||||
const groupIdx = Math.floor(Math.random() * this.serial);
|
||||
const cellIdx = Math.floor(Math.random() * this.parallel);
|
||||
const unusedCell = unusedCells[Math.floor(Math.random() * unusedCells.length)];
|
||||
newConfig[groupIdx][cellIdx] = unusedCell;
|
||||
} else if (moveType < 0.65) {
|
||||
// Swap cells between different groups
|
||||
const group1 = Math.floor(Math.random() * this.serial);
|
||||
let group2 = Math.floor(Math.random() * this.serial);
|
||||
while (group2 === group1 && this.serial > 1) {
|
||||
group2 = Math.floor(Math.random() * this.serial);
|
||||
}
|
||||
const cell1 = Math.floor(Math.random() * this.parallel);
|
||||
const cell2 = Math.floor(Math.random() * this.parallel);
|
||||
|
||||
const temp = newConfig[group1][cell1];
|
||||
newConfig[group1][cell1] = newConfig[group2][cell2];
|
||||
newConfig[group2][cell2] = temp;
|
||||
} else {
|
||||
// Swap cells within the same group
|
||||
const groupIdx = Math.floor(Math.random() * this.serial);
|
||||
if (this.parallel >= 2) {
|
||||
const cell1 = Math.floor(Math.random() * this.parallel);
|
||||
let cell2 = Math.floor(Math.random() * this.parallel);
|
||||
while (cell2 === cell1) {
|
||||
cell2 = Math.floor(Math.random() * this.parallel);
|
||||
}
|
||||
const temp = newConfig[groupIdx][cell1];
|
||||
newConfig[groupIdx][cell1] = newConfig[groupIdx][cell2];
|
||||
newConfig[groupIdx][cell2] = temp;
|
||||
}
|
||||
}
|
||||
|
||||
return newConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run simulated annealing.
|
||||
*/
|
||||
async run() {
|
||||
const startTime = Date.now();
|
||||
|
||||
let current = this.createInitialConfig();
|
||||
let currentScore = calculateScore(current, this.capacityWeight, this.irWeight);
|
||||
|
||||
this.bestSolution = { config: cloneConfiguration(current), ...currentScore };
|
||||
this.bestScore = currentScore.score;
|
||||
|
||||
let temperature = this.initialTemp;
|
||||
|
||||
for (let iteration = 0; iteration < this.maxIterations && !this.stopped; iteration++) {
|
||||
const neighbor = this.getNeighbor(current);
|
||||
const neighborScore = calculateScore(neighbor, this.capacityWeight, this.irWeight);
|
||||
|
||||
const delta = neighborScore.score - currentScore.score;
|
||||
|
||||
// Accept if better, or with probability based on temperature
|
||||
if (delta < 0 || Math.random() < Math.exp(-delta / temperature)) {
|
||||
current = neighbor;
|
||||
currentScore = neighborScore;
|
||||
|
||||
if (currentScore.score < this.bestScore) {
|
||||
this.bestScore = currentScore.score;
|
||||
this.bestSolution = { config: cloneConfiguration(current), ...currentScore };
|
||||
}
|
||||
}
|
||||
|
||||
// Cool down
|
||||
temperature *= this.coolingRate;
|
||||
|
||||
// Progress callback
|
||||
if (iteration % 100 === 0 || iteration === this.maxIterations - 1) {
|
||||
this.onProgress({
|
||||
iteration,
|
||||
maxIterations: this.maxIterations,
|
||||
bestScore: this.bestScore,
|
||||
currentBest: this.bestSolution,
|
||||
temperature,
|
||||
elapsedTime: Date.now() - startTime
|
||||
});
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
}
|
||||
}
|
||||
|
||||
const usedLabels = new Set(this.bestSolution.config.flat().map(c => c.label));
|
||||
const excludedCells = this.cells.filter(c => !usedLabels.has(c.label));
|
||||
|
||||
return {
|
||||
configuration: this.bestSolution.config,
|
||||
score: this.bestScore,
|
||||
capacityCV: this.bestSolution.capacityCV,
|
||||
irCV: this.bestSolution.irCV,
|
||||
groupCapacities: this.bestSolution.groupCapacities,
|
||||
excludedCells,
|
||||
iterations: this.maxIterations,
|
||||
elapsedTime: Date.now() - startTime
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Exhaustive Search (for small configurations)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Exhaustive search - finds the globally optimal solution.
|
||||
* Only practical for small configurations due to factorial complexity.
|
||||
*/
|
||||
class ExhaustiveSearch {
|
||||
constructor(cells, serial, parallel, options = {}) {
|
||||
this.cells = cells;
|
||||
this.serial = serial;
|
||||
this.parallel = parallel;
|
||||
this.totalCellsNeeded = serial * parallel;
|
||||
|
||||
this.capacityWeight = options.capacityWeight ?? 0.7;
|
||||
this.irWeight = options.irWeight ?? 0.3;
|
||||
this.onProgress = options.onProgress || (() => { });
|
||||
this.maxIterations = options.maxIterations || 100000;
|
||||
|
||||
this.stopped = false;
|
||||
this.bestSolution = null;
|
||||
this.bestScore = Infinity;
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.stopped = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate all combinations of k elements from array.
|
||||
*/
|
||||
*combinations(array, k) {
|
||||
if (k === 0) {
|
||||
yield [];
|
||||
return;
|
||||
}
|
||||
if (array.length < k) return;
|
||||
|
||||
const [first, ...rest] = array;
|
||||
for (const combo of this.combinations(rest, k - 1)) {
|
||||
yield [first, ...combo];
|
||||
}
|
||||
yield* this.combinations(rest, k);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate all partitions of cells into groups.
|
||||
*/
|
||||
*generatePartitions(cells, groupSize, numGroups) {
|
||||
if (numGroups === 0) {
|
||||
yield [];
|
||||
return;
|
||||
}
|
||||
|
||||
if (cells.length < groupSize * numGroups) return;
|
||||
|
||||
for (const group of this.combinations(cells, groupSize)) {
|
||||
const remaining = cells.filter(c => !group.includes(c));
|
||||
for (const rest of this.generatePartitions(remaining, groupSize, numGroups - 1)) {
|
||||
yield [group, ...rest];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async run() {
|
||||
const startTime = Date.now();
|
||||
let iteration = 0;
|
||||
|
||||
// Select best subset if we have more cells than needed
|
||||
const cellCombos = this.cells.length > this.totalCellsNeeded
|
||||
? this.combinations(this.cells, this.totalCellsNeeded)
|
||||
: [[...this.cells]];
|
||||
|
||||
for (const cellSubset of cellCombos) {
|
||||
if (this.stopped) break;
|
||||
|
||||
for (const partition of this.generatePartitions(cellSubset, this.parallel, this.serial)) {
|
||||
if (this.stopped) break;
|
||||
|
||||
const scoreResult = calculateScore(partition, this.capacityWeight, this.irWeight);
|
||||
|
||||
if (scoreResult.score < this.bestScore) {
|
||||
this.bestScore = scoreResult.score;
|
||||
this.bestSolution = { config: partition, ...scoreResult };
|
||||
}
|
||||
|
||||
iteration++;
|
||||
|
||||
if (iteration % 1000 === 0) {
|
||||
this.onProgress({
|
||||
iteration,
|
||||
maxIterations: this.maxIterations,
|
||||
bestScore: this.bestScore,
|
||||
currentBest: this.bestSolution,
|
||||
elapsedTime: Date.now() - startTime
|
||||
});
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
}
|
||||
|
||||
if (iteration >= this.maxIterations) {
|
||||
this.stopped = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const usedLabels = new Set(this.bestSolution.config.flat().map(c => c.label));
|
||||
const excludedCells = this.cells.filter(c => !usedLabels.has(c.label));
|
||||
|
||||
return {
|
||||
configuration: this.bestSolution.config,
|
||||
score: this.bestScore,
|
||||
capacityCV: this.bestSolution.capacityCV,
|
||||
irCV: this.bestSolution.irCV,
|
||||
groupCapacities: this.bestSolution.groupCapacities,
|
||||
excludedCells,
|
||||
iterations: iteration,
|
||||
elapsedTime: Date.now() - startTime
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Export
|
||||
// =============================================================================
|
||||
|
||||
// Make available globally for the main app
|
||||
window.CellMatchingAlgorithms = {
|
||||
GeneticAlgorithm,
|
||||
SimulatedAnnealing,
|
||||
ExhaustiveSearch,
|
||||
calculateScore,
|
||||
coefficientOfVariation
|
||||
};
|
||||
Reference in New Issue
Block a user