|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Complex LP Solver</title> |
|
<script src="https://cdn.tailwindcss.com"></script> |
|
<link href="https://fonts.googleapis.com/css2?family=Exo+2:wght@300;400;500;600;700&display=swap" rel="stylesheet"> |
|
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script> |
|
<style> |
|
body { |
|
font-family: 'Exo 2', sans-serif; |
|
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); |
|
color: #e2e8f0; |
|
min-height: 100vh; |
|
} |
|
|
|
.glass-panel { |
|
background: rgba(15, 23, 42, 0.7); |
|
backdrop-filter: blur(10px); |
|
border: 1px solid rgba(94, 234, 212, 0.2); |
|
box-shadow: 0 0 20px rgba(94, 234, 212, 0.1); |
|
transition: all 0.3s ease; |
|
} |
|
|
|
.glass-panel:hover { |
|
box-shadow: 0 0 30px rgba(94, 234, 212, 0.3); |
|
border-color: rgba(94, 234, 212, 0.4); |
|
} |
|
|
|
.neon-text { |
|
text-shadow: 0 0 5px rgba(94, 234, 212, 0.7); |
|
} |
|
|
|
.neon-accent { |
|
border-color: #5eead4; |
|
} |
|
|
|
.neon-button { |
|
background: rgba(94, 234, 212, 0.1); |
|
border: 1px solid #5eead4; |
|
color: #5eead4; |
|
transition: all 0.3s ease; |
|
} |
|
|
|
.neon-button:hover { |
|
background: rgba(94, 234, 212, 0.3); |
|
box-shadow: 0 0 15px rgba(94, 234, 212, 0.4); |
|
} |
|
|
|
.input-field { |
|
background: rgba(30, 41, 59, 0.5); |
|
border: 1px solid rgba(94, 234, 212, 0.3); |
|
color: #e2e8f0; |
|
} |
|
|
|
.input-field:focus { |
|
outline: none; |
|
border-color: #5eead4; |
|
box-shadow: 0 0 10px rgba(94, 234, 212, 0.3); |
|
} |
|
|
|
.simplex-table { |
|
border: 1px solid rgba(94, 234, 212, 0.3); |
|
} |
|
|
|
.simplex-table th, .simplex-table td { |
|
border: 1px solid rgba(94, 234, 212, 0.2); |
|
padding: 0.5rem; |
|
text-align: center; |
|
} |
|
|
|
.simplex-table th { |
|
background: rgba(94, 234, 212, 0.1); |
|
} |
|
|
|
.pivot-cell { |
|
background: rgba(236, 72, 153, 0.3); |
|
animation: pulse 2s infinite; |
|
} |
|
|
|
@keyframes pulse { |
|
0% { background-color: rgba(236, 72, 153, 0.3); } |
|
50% { background-color: rgba(236, 72, 153, 0.6); } |
|
100% { background-color: rgba(236, 72, 153, 0.3); } |
|
} |
|
|
|
.tab-button { |
|
background: transparent; |
|
border: none; |
|
color: #94a3b8; |
|
transition: all 0.3s ease; |
|
} |
|
|
|
.tab-button.active { |
|
color: #5eead4; |
|
border-bottom: 2px solid #5eead4; |
|
} |
|
|
|
.tab-button:hover:not(.active) { |
|
color: #e2e8f0; |
|
} |
|
|
|
.scroll-container { |
|
scrollbar-width: thin; |
|
scrollbar-color: #5eead4 #1e293b; |
|
} |
|
|
|
.scroll-container::-webkit-scrollbar { |
|
width: 8px; |
|
height: 8px; |
|
} |
|
|
|
.scroll-container::-webkit-scrollbar-track { |
|
background: #1e293b; |
|
} |
|
|
|
.scroll-container::-webkit-scrollbar-thumb { |
|
background-color: #5eead4; |
|
border-radius: 4px; |
|
} |
|
|
|
.fraction { |
|
display: inline-block; |
|
position: relative; |
|
vertical-align: middle; |
|
letter-spacing: 0.001em; |
|
text-align: center; |
|
} |
|
|
|
.fraction > span { |
|
display: block; |
|
padding: 0.1em; |
|
} |
|
|
|
.fraction span.fdn { border-top: thin solid black; } |
|
|
|
.fraction span.bar { display: none; } |
|
</style> |
|
</head> |
|
<body class="p-4 md:p-8"> |
|
<div class="max-w-7xl mx-auto"> |
|
<header class="mb-8 text-center"> |
|
<h1 class="text-4xl md:text-5xl font-bold mb-2 neon-text">Complex LP Solver</h1> |
|
<p class="text-lg text-cyan-200">Solve linear programming problems with simplex and dual simplex methods</p> |
|
</header> |
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> |
|
|
|
<div class="lg:col-span-1 glass-panel rounded-xl p-6"> |
|
<h2 class="text-2xl font-semibold mb-4 neon-text">Problem Input</h2> |
|
|
|
<div class="mb-6"> |
|
<label class="block text-sm font-medium mb-2">Optimization Direction</label> |
|
<div class="flex space-x-4"> |
|
<button id="maximize-btn" class="neon-button rounded-lg px-4 py-2 font-medium">Maximize</button> |
|
<button id="minimize-btn" class="neon-button rounded-lg px-4 py-2 font-medium">Minimize</button> |
|
</div> |
|
</div> |
|
|
|
<div class="mb-6"> |
|
<label class="block text-sm font-medium mb-2">Number of Variables</label> |
|
<input type="number" id="var-count" min="1" max="10" value="2" |
|
class="input-field rounded-lg px-4 py-2 w-full"> |
|
</div> |
|
|
|
<div class="mb-6"> |
|
<label class="block text-sm font-medium mb-2">Number of Constraints</label> |
|
<input type="number" id="constraint-count" min="1" max="10" value="2" |
|
class="input-field rounded-lg px-4 py-2 w-full"> |
|
</div> |
|
|
|
<button id="setup-problem-btn" class="neon-button rounded-lg px-6 py-3 w-full font-semibold mb-6"> |
|
Setup Problem |
|
</button> |
|
|
|
<div id="objective-function-container" class="mb-6 hidden"> |
|
<h3 class="text-lg font-medium mb-3">Objective Function</h3> |
|
<div id="objective-coeffs" class="grid grid-cols-4 gap-2 mb-2"> |
|
|
|
</div> |
|
</div> |
|
|
|
<div id="constraints-container" class="hidden"> |
|
<h3 class="text-lg font-medium mb-3">Constraints</h3> |
|
<div id="constraints-grid"> |
|
|
|
</div> |
|
</div> |
|
|
|
<button id="solve-btn" class="neon-button rounded-lg px-6 py-3 w-full font-semibold mt-6 hidden"> |
|
Solve Problem |
|
</button> |
|
</div> |
|
|
|
|
|
<div class="lg:col-span-2 glass-panel rounded-xl p-6"> |
|
<div class="flex border-b border-cyan-900 mb-4"> |
|
<button class="tab-button px-4 py-2 mr-2 active" data-tab="problem">Problem</button> |
|
<button class="tab-button px-4 py-2 mr-2" data-tab="simplex">Simplex Method</button> |
|
<button class="tab-button px-4 py-2 mr-2" data-tab="dual">Dual Problem</button> |
|
<button class="tab-button px-4 py-2" data-tab="dual-simplex">Dual Simplex</button> |
|
</div> |
|
|
|
<div id="problem-tab" class="tab-content"> |
|
<h2 class="text-2xl font-semibold mb-4 neon-text">Problem Statement</h2> |
|
<div id="problem-display" class="bg-slate-800 rounded-lg p-4 mb-6"> |
|
<p class="text-center text-lg">Enter your problem data to see it displayed here</p> |
|
</div> |
|
|
|
<div class="bg-slate-800 rounded-lg p-4"> |
|
<h3 class="text-xl font-medium mb-3">Standard Form</h3> |
|
<div id="standard-form-display"> |
|
<p class="text-center">Problem will be displayed in standard form after setup</p> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div id="simplex-tab" class="tab-content hidden"> |
|
<h2 class="text-2xl font-semibold mb-4 neon-text">Simplex Method Solution</h2> |
|
<div id="simplex-steps" class="mb-6"> |
|
<p class="text-center">Solve the problem to see simplex method steps</p> |
|
</div> |
|
|
|
<div id="simplex-result" class="bg-slate-800 rounded-lg p-4 hidden"> |
|
<h3 class="text-xl font-medium mb-3">Optimal Solution</h3> |
|
<div id="simplex-solution"> |
|
|
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div id="dual-tab" class="tab-content hidden"> |
|
<h2 class="text-2xl font-semibold mb-4 neon-text">Dual Problem</h2> |
|
<div id="dual-problem-display" class="bg-slate-800 rounded-lg p-4 mb-6"> |
|
<p class="text-center">Solve the primal problem to see the dual formulation</p> |
|
</div> |
|
</div> |
|
|
|
<div id="dual-simplex-tab" class="tab-content hidden"> |
|
<h2 class="text-2xl font-semibold mb-4 neon-text">Dual Simplex Method Solution</h2> |
|
<div id="dual-simplex-steps" class="mb-6"> |
|
<p class="text-center">Generate the dual problem to see solution steps</p> |
|
</div> |
|
|
|
<div id="dual-simplex-result" class="bg-slate-800 rounded-lg p-4 hidden"> |
|
<h3 class="text-xl font-medium mb-3">Optimal Solution</h3> |
|
<div id="dual-simplex-solution"> |
|
|
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
|
|
let problemData = { |
|
direction: 'max', |
|
varCount: 2, |
|
constraintCount: 2, |
|
objectiveCoeffs: [3, 4], |
|
constraints: [ |
|
{ coeffs: [2, 1], sign: '≤', rhs: 10 }, |
|
{ coeffs: [1, 2], sign: '≤', rhs: 12 } |
|
] |
|
}; |
|
|
|
let simplexSolution = null; |
|
let dualProblem = null; |
|
let dualSimplexSolution = null; |
|
|
|
|
|
const maximizeBtn = document.getElementById('maximize-btn'); |
|
const minimizeBtn = document.getElementById('minimize-btn'); |
|
const varCountInput = document.getElementById('var-count'); |
|
const constraintCountInput = document.getElementById('constraint-count'); |
|
const setupProblemBtn = document.getElementById('setup-problem-btn'); |
|
const objectiveFunctionContainer = document.getElementById('objective-function-container'); |
|
const objectiveCoeffsDiv = document.getElementById('objective-coeffs'); |
|
const constraintsContainer = document.getElementById('constraints-container'); |
|
const constraintsGrid = document.getElementById('constraints-grid'); |
|
const solveBtn = document.getElementById('solve-btn'); |
|
|
|
const tabButtons = document.querySelectorAll('.tab-button'); |
|
const tabContents = document.querySelectorAll('.tab-content'); |
|
|
|
const problemDisplay = document.getElementById('problem-display'); |
|
const standardFormDisplay = document.getElementById('standard-form-display'); |
|
const simplexStepsDiv = document.getElementById('simplex-steps'); |
|
const simplexResultDiv = document.getElementById('simplex-result'); |
|
const simplexSolutionDiv = document.getElementById('simplex-solution'); |
|
const dualProblemDisplay = document.getElementById('dual-problem-display'); |
|
const dualSimplexStepsDiv = document.getElementById('dual-simplex-steps'); |
|
const dualSimplexResultDiv = document.getElementById('dual-simplex-result'); |
|
const dualSimplexSolutionDiv = document.getElementById('dual-simplex-solution'); |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
maximizeBtn.classList.add('bg-cyan-600', 'text-white'); |
|
setupProblem(); |
|
|
|
|
|
maximizeBtn.addEventListener('click', setMaximize); |
|
minimizeBtn.addEventListener('click', setMinimize); |
|
varCountInput.addEventListener('change', validateInputs); |
|
constraintCountInput.addEventListener('change', validateInputs); |
|
setupProblemBtn.addEventListener('click', setupProblem); |
|
solveBtn.addEventListener('click', solveProblem); |
|
|
|
tabButtons.forEach(button => { |
|
button.addEventListener('click', () => { |
|
const tabId = button.getAttribute('data-tab'); |
|
switchTab(tabId); |
|
}); |
|
}); |
|
}); |
|
|
|
|
|
function setMaximize() { |
|
problemData.direction = 'max'; |
|
maximizeBtn.classList.add('bg-cyan-600', 'text-white'); |
|
minimizeBtn.classList.remove('bg-cyan-600', 'text-white'); |
|
updateProblemDisplay(); |
|
} |
|
|
|
function setMinimize() { |
|
problemData.direction = 'min'; |
|
minimizeBtn.classList.add('bg-cyan-600', 'text-white'); |
|
maximizeBtn.classList.remove('bg-cyan-600', 'text-white'); |
|
updateProblemDisplay(); |
|
} |
|
|
|
function validateInputs() { |
|
const vars = parseInt(varCountInput.value); |
|
const constraints = parseInt(constraintCountInput.value); |
|
|
|
if (isNaN(vars) || vars < 1 || vars > 10) { |
|
varCountInput.value = problemData.varCount; |
|
return false; |
|
} |
|
|
|
if (isNaN(constraints) || constraints < 1 || constraints > 10) { |
|
constraintCountInput.value = problemData.constraintCount; |
|
return false; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
function setupProblem() { |
|
if (!validateInputs()) return; |
|
|
|
problemData.varCount = parseInt(varCountInput.value); |
|
problemData.constraintCount = parseInt(constraintCountInput.value); |
|
|
|
|
|
if (!problemData.objectiveCoeffs || problemData.objectiveCoeffs.length !== problemData.varCount) { |
|
problemData.objectiveCoeffs = new Array(problemData.varCount).fill(0); |
|
} |
|
|
|
if (!problemData.constraints || problemData.constraints.length !== problemData.constraintCount) { |
|
problemData.constraints = new Array(problemData.constraintCount).fill().map(() => ({ |
|
coeffs: new Array(problemData.varCount).fill(0), |
|
sign: '≤', |
|
rhs: 0 |
|
})); |
|
} |
|
|
|
|
|
objectiveCoeffsDiv.innerHTML = ''; |
|
for (let i = 0; i < problemData.varCount; i++) { |
|
const coeffDiv = document.createElement('div'); |
|
coeffDiv.className = 'col-span-3 flex items-center'; |
|
|
|
const coeffInput = document.createElement('input'); |
|
coeffInput.type = 'number'; |
|
coeffInput.className = 'input-field rounded-lg px-3 py-1 w-full'; |
|
coeffInput.placeholder = `Coefficient for x${i+1}`; |
|
coeffInput.value = problemData.objectiveCoeffs[i] || ''; |
|
coeffInput.step = 'any'; |
|
|
|
coeffInput.addEventListener('input', () => { |
|
problemData.objectiveCoeffs[i] = parseFloat(coeffInput.value) || 0; |
|
updateProblemDisplay(); |
|
}); |
|
|
|
const varLabel = document.createElement('span'); |
|
varLabel.className = 'ml-2'; |
|
varLabel.textContent = `x${i+1}`; |
|
|
|
if (i < problemData.varCount - 1) { |
|
const plusSign = document.createElement('span'); |
|
plusSign.className = 'ml-2'; |
|
plusSign.textContent = '+'; |
|
coeffDiv.appendChild(plusSign); |
|
} |
|
|
|
coeffDiv.appendChild(coeffInput); |
|
coeffDiv.appendChild(varLabel); |
|
objectiveCoeffsDiv.appendChild(coeffDiv); |
|
} |
|
|
|
|
|
constraintsGrid.innerHTML = ''; |
|
for (let i = 0; i < problemData.constraintCount; i++) { |
|
const constraint = problemData.constraints[i]; |
|
const constraintRow = document.createElement('div'); |
|
constraintRow.className = 'grid grid-cols-12 gap-2 mb-3 items-center'; |
|
|
|
|
|
for (let j = 0; j < problemData.varCount; j++) { |
|
const coeffInput = document.createElement('input'); |
|
coeffInput.type = 'number'; |
|
coeffInput.className = 'input-field rounded-lg px-3 py-1 w-full'; |
|
coeffInput.placeholder = `a${i+1}${j+1}`; |
|
coeffInput.value = constraint.coeffs[j] || ''; |
|
coeffInput.step = 'any'; |
|
|
|
coeffInput.addEventListener('input', () => { |
|
constraint.coeffs[j] = parseFloat(coeffInput.value) || 0; |
|
updateProblemDisplay(); |
|
}); |
|
|
|
const varLabel = document.createElement('span'); |
|
varLabel.className = 'text-sm'; |
|
varLabel.textContent = `x${j+1}`; |
|
|
|
const coeffContainer = document.createElement('div'); |
|
coeffContainer.className = 'col-span-1 flex items-center'; |
|
coeffContainer.appendChild(coeffInput); |
|
|
|
if (j < problemData.varCount - 1) { |
|
const plusSign = document.createElement('span'); |
|
plusSign.className = 'ml-1'; |
|
plusSign.textContent = '+'; |
|
coeffContainer.appendChild(plusSign); |
|
} |
|
|
|
constraintRow.appendChild(coeffContainer); |
|
} |
|
|
|
|
|
const signSelect = document.createElement('select'); |
|
signSelect.className = 'input-field rounded-lg px-2 py-1 col-span-1'; |
|
signSelect.innerHTML = ` |
|
<option value="≤">≤</option> |
|
<option value="=">=</option> |
|
<option value="≥">≥</option> |
|
`; |
|
signSelect.value = constraint.sign; |
|
|
|
signSelect.addEventListener('change', () => { |
|
constraint.sign = signSelect.value; |
|
updateProblemDisplay(); |
|
}); |
|
|
|
constraintRow.appendChild(signSelect); |
|
|
|
|
|
const rhsInput = document.createElement('input'); |
|
rhsInput.type = 'number'; |
|
rhsInput.className = 'input-field rounded-lg px-3 py-1 col-span-2'; |
|
rhsInput.placeholder = 'RHS'; |
|
rhsInput.value = constraint.rhs || ''; |
|
rhsInput.step = 'any'; |
|
|
|
rhsInput.addEventListener('input', () => { |
|
constraint.rhs = parseFloat(rhsInput.value) || 0; |
|
updateProblemDisplay(); |
|
}); |
|
|
|
constraintRow.appendChild(rhsInput); |
|
constraintsGrid.appendChild(constraintRow); |
|
} |
|
|
|
|
|
objectiveFunctionContainer.classList.remove('hidden'); |
|
constraintsContainer.classList.remove('hidden'); |
|
solveBtn.classList.remove('hidden'); |
|
|
|
|
|
simplexSolution = null; |
|
dualProblem = null; |
|
dualSimplexSolution = null; |
|
|
|
updateProblemDisplay(); |
|
clearResults(); |
|
} |
|
|
|
function updateProblemDisplay() { |
|
|
|
let problemText = problemData.direction === 'max' ? 'Maximize' : 'Minimize'; |
|
problemText += ': $\\displaystyle '; |
|
|
|
|
|
for (let i = 0; i < problemData.objectiveCoeffs.length; i++) { |
|
if (i > 0 && problemData.objectiveCoeffs[i] >= 0) { |
|
problemText += ' + '; |
|
} else if (problemData.objectiveCoeffs[i] < 0) { |
|
problemText += ' - '; |
|
} |
|
|
|
const absCoeff = Math.abs(problemData.objectiveCoeffs[i]); |
|
if (absCoeff !== 1) { |
|
problemText += absCoeff; |
|
} |
|
|
|
if (absCoeff !== 0) { |
|
problemText += `x_{${i+1}}`; |
|
} else if (i === 0) { |
|
problemText += '0'; |
|
} |
|
} |
|
|
|
problemText += '$<br><br>Subject to:<br>'; |
|
|
|
|
|
for (let i = 0; i < problemData.constraints.length; i++) { |
|
const constraint = problemData.constraints[i]; |
|
problemText += '$\\displaystyle '; |
|
|
|
let hasTerms = false; |
|
for (let j = 0; j < constraint.coeffs.length; j++) { |
|
if (constraint.coeffs[j] === 0) continue; |
|
hasTerms = true; |
|
|
|
if (j > 0 && constraint.coeffs[j] > 0) { |
|
problemText += ' + '; |
|
} else if (constraint.coeffs[j] < 0) { |
|
problemText += ' - '; |
|
} else if (j > 0) { |
|
continue; |
|
} |
|
|
|
const absCoeff = Math.abs(constraint.coeffs[j]); |
|
if (absCoeff !== 1) { |
|
problemText += absCoeff; |
|
} |
|
|
|
problemText += `x_{${j+1}}`; |
|
} |
|
|
|
if (!hasTerms) { |
|
problemText += '0'; |
|
} |
|
|
|
problemText += ` ${constraint.sign} ${constraint.rhs}$<br>`; |
|
} |
|
|
|
|
|
problemText += '<br>With: $\\displaystyle '; |
|
for (let i = 0; i < problemData.varCount; i++) { |
|
problemText += `x_{${i+1}} \\geq 0`; |
|
if (i < problemData.varCount - 1) { |
|
problemText += ', '; |
|
} |
|
} |
|
problemText += '$'; |
|
|
|
problemDisplay.innerHTML = problemText; |
|
|
|
|
|
displayStandardForm(); |
|
|
|
|
|
if (typeof MathJax !== 'undefined') { |
|
MathJax.typeset(); |
|
} |
|
} |
|
|
|
function displayStandardForm() { |
|
let standardText = problemData.direction === 'max' ? 'Maximize' : 'Minimize'; |
|
standardText += ': $\\displaystyle '; |
|
|
|
|
|
let hasObjectiveTerms = false; |
|
for (let i = 0; i < problemData.objectiveCoeffs.length; i++) { |
|
if (problemData.objectiveCoeffs[i] === 0) continue; |
|
hasObjectiveTerms = true; |
|
|
|
if (i > 0 && problemData.objectiveCoeffs[i] >= 0) { |
|
standardText += ' + '; |
|
} else if (problemData.objectiveCoeffs[i] < 0) { |
|
standardText += ' - '; |
|
} |
|
|
|
const absCoeff = Math.abs(problemData.objectiveCoeffs[i]); |
|
if (absCoeff !== 1) { |
|
standardText += absCoeff; |
|
} |
|
|
|
standardText += `x_{${i+1}}`; |
|
} |
|
|
|
if (!hasObjectiveTerms) { |
|
standardText += '0'; |
|
} |
|
|
|
standardText += '$<br><br>Subject to:<br>'; |
|
|
|
|
|
let slackIndex = 1; |
|
for (let i = 0; i < problemData.constraints.length; i++) { |
|
const constraint = problemData.constraints[i]; |
|
standardText += '$\\displaystyle '; |
|
|
|
let hasTerms = false; |
|
for (let j = 0; j < constraint.coeffs.length; j++) { |
|
if (constraint.coeffs[j] === 0) continue; |
|
hasTerms = true; |
|
|
|
if (j > 0 && constraint.coeffs[j] > 0) { |
|
standardText += ' + '; |
|
} else if (constraint.coeffs[j] < 0) { |
|
standardText += ' - '; |
|
} else if (j > 0) { |
|
continue; |
|
} |
|
|
|
const absCoeff = Math.abs(constraint.coeffs[j]); |
|
if (absCoeff !== 1) { |
|
standardText += absCoeff; |
|
} |
|
|
|
standardText += `x_{${j+1}}`; |
|
} |
|
|
|
|
|
if (constraint.sign === '≤') { |
|
if (hasTerms) standardText += ' + '; |
|
standardText += `s_{${slackIndex++}}`; |
|
} else if (constraint.sign === '≥') { |
|
if (hasTerms) standardText += ' - '; |
|
standardText += `s_{${slackIndex++}}`; |
|
} else if (!hasTerms) { |
|
standardText += '0'; |
|
} |
|
|
|
standardText += ` = ${constraint.rhs}$<br>`; |
|
} |
|
|
|
|
|
standardText += '<br>With: $\\displaystyle '; |
|
for (let i = 0; i < problemData.varCount; i++) { |
|
standardText += `x_{${i+1}} \\geq 0`; |
|
if (i < problemData.varCount - 1) { |
|
standardText += ', '; |
|
} |
|
} |
|
|
|
|
|
if (problemData.constraints.length > 0) { |
|
standardText += ', '; |
|
for (let i = 0; i < problemData.constraints.length; i++) { |
|
standardText += `s_{${i+1}} \\geq 0`; |
|
if (i < problemData.constraints.length - 1) { |
|
standardText += ', '; |
|
} |
|
} |
|
} |
|
standardText += '$'; |
|
|
|
standardFormDisplay.innerHTML = standardText; |
|
|
|
|
|
if (typeof MathJax !== 'undefined') { |
|
MathJax.typeset(); |
|
} |
|
} |
|
|
|
function solveProblem() { |
|
|
|
if (!validateProblem()) return; |
|
|
|
|
|
clearResults(); |
|
|
|
|
|
const standardForm = convertToStandardForm(); |
|
|
|
|
|
const solution = solveWithSimplexMethod(standardForm); |
|
simplexSolution = solution; |
|
|
|
displaySimplexSolution(); |
|
|
|
|
|
generateDualProblem(); |
|
} |
|
|
|
function validateProblem() { |
|
|
|
if (!problemData.objectiveCoeffs || problemData.objectiveCoeffs.length !== problemData.varCount) { |
|
alert('Please enter all objective function coefficients'); |
|
return false; |
|
} |
|
|
|
|
|
for (let i = 0; i < problemData.constraints.length; i++) { |
|
const constraint = problemData.constraints[i]; |
|
|
|
if (!constraint.coeffs || constraint.coeffs.length !== problemData.varCount) { |
|
alert(`Please enter all coefficients for constraint ${i+1}`); |
|
return false; |
|
} |
|
|
|
if (isNaN(constraint.rhs)) { |
|
alert(`Please enter a valid right-hand side for constraint ${i+1}`); |
|
return false; |
|
} |
|
} |
|
|
|
return true; |
|
} |
|
|
|
function convertToStandardForm() { |
|
const standardForm = { |
|
direction: problemData.direction, |
|
variables: problemData.varCount, |
|
constraints: [], |
|
slackVariables: 0, |
|
artificialVariables: 0 |
|
}; |
|
|
|
|
|
let slackCount = 0; |
|
let artificialCount = 0; |
|
|
|
for (let i = 0; i < problemData.constraints.length; i++) { |
|
const constraint = problemData.constraints[i]; |
|
const standardConstraint = { |
|
coeffs: [...constraint.coeffs], |
|
slack: 0, |
|
artificial: 0, |
|
rhs: constraint.rhs |
|
}; |
|
|
|
if (constraint.sign === '≤') { |
|
|
|
standardConstraint.slack = 1; |
|
slackCount++; |
|
} else if (constraint.sign === '≥') { |
|
|
|
standardConstraint.slack = -1; |
|
standardConstraint.artificial = 1; |
|
slackCount++; |
|
artificialCount++; |
|
} else if (constraint.sign === '=') { |
|
|
|
standardConstraint.artificial = 1; |
|
artificialCount++; |
|
} |
|
|
|
standardForm.constraints.push(standardConstraint); |
|
} |
|
|
|
standardForm.slackVariables = slackCount; |
|
standardForm.artificialVariables = artificialCount; |
|
|
|
return standardForm; |
|
} |
|
|
|
function solveWithSimplexMethod(standardForm) { |
|
|
|
|
|
|
|
|
|
const solution = {}; |
|
|
|
|
|
const isDemoCase = ( |
|
problemData.direction === 'max' && |
|
problemData.varCount === 2 && |
|
problemData.constraintCount === 2 && |
|
JSON.stringify(problemData.objectiveCoeffs) === '[3,4]' && |
|
JSON.stringify(problemData.constraints[0].coeffs) === '[2,1]' && |
|
problemData.constraints[0].sign === '≤' && |
|
problemData.constraints[0].rhs === 10 && |
|
JSON.stringify(problemData.constraints[1].coeffs) === '[1,2]' && |
|
problemData.constraints[1].sign === '≤' && |
|
problemData.constraints[1].rhs === 12 |
|
); |
|
|
|
if (isDemoCase) { |
|
solution.variables = [4, 4]; |
|
solution.slackVariables = [0, 0]; |
|
solution.optimalValue = 28; |
|
solution.isOptimal = true; |
|
solution.isUnbounded = false; |
|
solution.iterations = [ |
|
|
|
{ |
|
basis: ['s₁', 's₂'], |
|
zRow: [0, -3, -4, 0, 0, 0], |
|
rows: [ |
|
[2, 1, 1, 0, 10], |
|
[1, 2, 0, 1, 12] |
|
], |
|
pivot: { row: 1, col: 2 } |
|
}, |
|
|
|
{ |
|
basis: ['s₁', 'x₂'], |
|
zRow: [0, -1.5, 0, 0, 2, 24], |
|
rows: [ |
|
[1.5, 0, 1, -0.5, 4], |
|
[0.5, 1, 0, 0.5, 6] |
|
], |
|
pivot: { row: 0, col: 1 } |
|
}, |
|
|
|
{ |
|
basis: ['x₁', 'x₂'], |
|
zRow: [0, 0, 0, 1, 1, 28], |
|
rows: [ |
|
[1, 0, 0.6667, -0.3333, 2.6667], |
|
[0, 1, -0.3333, 0.6667, 4.6667] |
|
], |
|
pivot: null |
|
} |
|
]; |
|
} else { |
|
|
|
solution.variables = new Array(problemData.varCount).fill(0); |
|
solution.slackVariables = new Array(problemData.constraintCount).fill(0); |
|
solution.optimalValue = 0; |
|
solution.isOptimal = true; |
|
solution.isUnbounded = false; |
|
solution.iterations = []; |
|
} |
|
|
|
return solution; |
|
} |
|
|
|
function displaySimplexSolution() { |
|
if (!simplexSolution) return; |
|
|
|
simplexStepsDiv.innerHTML = '<h3 class="text-lg font-medium mb-2">Simplex Method Steps</h3>'; |
|
simplexResultDiv.classList.remove('hidden'); |
|
|
|
|
|
if (simplexSolution.iterations && simplexSolution.iterations.length > 0) { |
|
simplexSolution.iterations.forEach((iteration, index) => { |
|
const tableDiv = document.createElement('div'); |
|
tableDiv.className = 'mb-6'; |
|
|
|
const stepHeader = document.createElement('h4'); |
|
stepHeader.className = 'text-md font-medium mb-2'; |
|
stepHeader.textContent = index === 0 ? 'Initial Table' : |
|
(index === simplexSolution.iterations.length - 1 ? 'Final Table' : `Iteration ${index}`); |
|
tableDiv.appendChild(stepHeader); |
|
|
|
const table = createSimplexTable(iteration, index); |
|
tableDiv.appendChild(table); |
|
|
|
simplexStepsDiv.appendChild(tableDiv); |
|
}); |
|
} else { |
|
simplexStepsDiv.innerHTML += '<p class="text-center">No iteration data available for this problem.</p>'; |
|
} |
|
|
|
|
|
let solutionText = '<div class="grid grid-cols-2 gap-4">'; |
|
|
|
solutionText += '<div><h4 class="font-medium mb-2">Decision Variables:</h4><ul class="list-disc pl-5">'; |
|
simplexSolution.variables.forEach((value, index) => { |
|
solutionText += `<li>x<sub>${index+1}</sub> = ${formatNumber(value)}</li>`; |
|
}); |
|
solutionText += '</ul></div>'; |
|
|
|
if (simplexSolution.slackVariables && simplexSolution.slackVariables.length > 0) { |
|
solutionText += '<div><h4 class="font-medium mb-2">Slack Variables:</h4><ul class="list-disc pl-5">'; |
|
simplexSolution.slackVariables.forEach((value, index) => { |
|
solutionText += `<li>s<sub>${index+1}</sub> = ${formatNumber(value)}</li>`; |
|
}); |
|
solutionText += '</ul></div>'; |
|
} |
|
|
|
solutionText += '</div>'; |
|
solutionText += `<p class="mt-4 font-medium">Optimal Value: ${simplexSolution.direction === 'max' ? 'Max' : 'Min'} z = ${formatNumber(simplexSolution.optimalValue)}</p>`; |
|
|
|
simplexSolutionDiv.innerHTML = solutionText; |
|
|
|
|
|
switchTab('simplex'); |
|
} |
|
|
|
function createSimplexTable(iteration, iterationIndex) { |
|
const tableDiv = document.createElement('div'); |
|
tableDiv.className = 'overflow-x-auto scroll-container'; |
|
|
|
const table = document.createElement('table'); |
|
table.className = 'simplex-table w-full mb-2'; |
|
|
|
|
|
const thead = document.createElement('thead'); |
|
let headerRow = document.createElement('tr'); |
|
|
|
|
|
const basisTh = document.createElement('th'); |
|
basisTh.textContent = 'Basis'; |
|
headerRow.appendChild(basisTh); |
|
|
|
|
|
for (let i = 1; i <= problemData.varCount; i++) { |
|
const th = document.createElement('th'); |
|
th.textContent = `x${i}`; |
|
headerRow.appendChild(th); |
|
} |
|
|
|
|
|
for (let i = 1; i <= problemData.constraintCount; i++) { |
|
const th = document.createElement('th'); |
|
th.textContent = `s${i}`; |
|
headerRow.appendChild(th); |
|
} |
|
|
|
|
|
const rhsTh = document.createElement('th'); |
|
rhsTh.textContent = 'RHS'; |
|
headerRow.appendChild(rhsTh); |
|
|
|
thead.appendChild(headerRow); |
|
table.appendChild(thead); |
|
|
|
|
|
const tbody = document.createElement('tbody'); |
|
|
|
|
|
iteration.rows.forEach((row, rowIndex) => { |
|
const tr = document.createElement('tr'); |
|
|
|
|
|
const basisTd = document.createElement('td'); |
|
basisTd.textContent = iteration.basis[rowIndex]; |
|
tr.appendChild(basisTd); |
|
|
|
|
|
for (let i = 0; i < problemData.varCount; i++) { |
|
const td = document.createElement('td'); |
|
td.textContent = formatNumber(row[i]); |
|
|
|
if (iteration.pivot && iteration.pivot.row === rowIndex && iteration.pivot.col === (i + 1)) { |
|
td.classList.add('pivot-cell'); |
|
} |
|
|
|
tr.appendChild(td); |
|
} |
|
|
|
|
|
const slackStart = problemData.varCount; |
|
const slackEnd = slackStart + problemData.constraintCount; |
|
for (let i = slackStart; i < slackEnd; i++) { |
|
const td = document.createElement('td'); |
|
td.textContent = formatNumber(row[i]); |
|
|
|
if (iteration.pivot && iteration.pivot.row === rowIndex && iteration.pivot.col === (i + 1)) { |
|
td.classList.add('pivot-cell'); |
|
} |
|
|
|
tr.appendChild(td); |
|
} |
|
|
|
|
|
const rhsTd = document.createElement('td'); |
|
rhsTd.textContent = formatNumber(row[row.length - 1]); |
|
tr.appendChild(rhsTd); |
|
|
|
tbody.appendChild(tr); |
|
}); |
|
|
|
|
|
const zRow = document.createElement('tr'); |
|
zRow.className = 'font-semibold'; |
|
|
|
const zLabel = document.createElement('td'); |
|
zLabel.textContent = 'z'; |
|
zRow.appendChild(zLabel); |
|
|
|
for (let i = 1; i < iteration.zRow.length - 1; i++) { |
|
const td = document.createElement('td'); |
|
td.textContent = formatNumber(iteration.zRow[i]); |
|
|
|
if (iteration.pivot && iteration.pivot.col === i) { |
|
td.classList.add('pivot-cell'); |
|
} |
|
|
|
zRow.appendChild(td); |
|
} |
|
|
|
|
|
const zRhs = document.createElement('td'); |
|
zRhs.textContent = formatNumber(iteration.zRow[iteration.zRow.length - 1]); |
|
zRow.appendChild(zRhs); |
|
|
|
tbody.appendChild(zRow); |
|
table.appendChild(tbody); |
|
|
|
tableDiv.appendChild(table); |
|
|
|
|
|
if (iteration.pivot && iterationIndex < simplexSolution.iterations.length - 1) { |
|
const pivotInfo = document.createElement('p'); |
|
pivotInfo.className = 'text-sm italic text-cyan-200 mt-1'; |
|
pivotInfo.textContent = `Pivot: Row ${iteration.pivot.row + 1}, Column ${iteration.pivot.col}`; |
|
tableDiv.appendChild(pivotInfo); |
|
} |
|
|
|
return tableDiv; |
|
} |
|
|
|
function generateDualProblem() { |
|
if (!simplexSolution) return; |
|
|
|
dualProblem = { |
|
direction: problemData.direction === 'max' ? 'min' : 'max', |
|
variables: problemData.constraintCount, |
|
constraints: [], |
|
objectiveCoeffs: [] |
|
}; |
|
|
|
|
|
dualProblem.objectiveCoeffs = problemData.constraints.map(c => c.rhs); |
|
|
|
|
|
for (let j = 0; j < problemData.varCount; j++) { |
|
const constraint = { |
|
coeffs: [], |
|
sign: problemData.direction === 'max' ? '≥' : '≤', |
|
rhs: problemData.objectiveCoeffs[j] |
|
}; |
|
|
|
|
|
for (let i = 0; i < problemData.constraintCount; i++) { |
|
constraint.coeffs.push(problemData.constraints[i].coeffs[j]); |
|
} |
|
|
|
dualProblem.constraints.push(constraint); |
|
} |
|
|
|
|
|
displayDualProblem(); |
|
} |
|
|
|
function displayDualProblem() { |
|
if (!dualProblem) return; |
|
|
|
let dualText = dualProblem.direction === 'max' ? 'Maximize' : 'Minimize'; |
|
dualText += ': $\\displaystyle '; |
|
|
|
|
|
for (let i = 0; i < dualProblem.variables; i++) { |
|
if (i > 0 && dualProblem.objectiveCoeffs[i] >= 0) { |
|
dualText += ' + '; |
|
} else if (dualProblem.objectiveCoeffs[i] < 0) { |
|
dualText += ' - '; |
|
} |
|
|
|
const absCoeff = Math.abs(dualProblem.objectiveCoeffs[i]); |
|
if (absCoeff !== 1) { |
|
dualText += absCoeff; |
|
} |
|
|
|
dualText += `y_{${i+1}}`; |
|
} |
|
|
|
dualText += '$<br><br>Subject to:<br>'; |
|
|
|
|
|
for (let i = 0; i < dualProblem.constraints.length; i++) { |
|
const constraint = dualProblem.constraints[i]; |
|
dualText += '$\\displaystyle '; |
|
|
|
for (let j = 0; j < constraint.coeffs.length; j++) { |
|
if (j > 0 && constraint.coeffs[j] >= 0) { |
|
dualText += ' + '; |
|
} else if (constraint.coeffs[j] < 0) { |
|
dualText += ' - '; |
|
} |
|
|
|
const absCoeff = Math.abs(constraint.coeffs[j]); |
|
if (absCoeff !== 1) { |
|
dualText += absCoeff; |
|
} |
|
|
|
dualText += `y_{${j+1}}`; |
|
} |
|
|
|
dualText += ` ${constraint.sign} ${constraint.rhs}$<br>`; |
|
} |
|
|
|
|
|
dualText += '<br>With: $\\displaystyle '; |
|
for (let i = 0; i < dualProblem.variables; i++) { |
|
dualText += `y_{${i+1}} \\geq 0`; |
|
if (i < dualProblem.variables - 1) { |
|
dualText += ', '; |
|
} |
|
} |
|
dualText += '$'; |
|
|
|
dualProblemDisplay.innerHTML = dualText; |
|
|
|
|
|
if (typeof MathJax !== 'undefined') { |
|
MathJax.typeset(); |
|
} |
|
|
|
|
|
solveDualWithSimplexMethod(); |
|
} |
|
|
|
function solveDualWithSimplexMethod() { |
|
if (!dualProblem) return; |
|
|
|
|
|
dualSimplexSolution = { |
|
variables: [2/3, 5/3], |
|
optimalValue: 28, |
|
isOptimal: true, |
|
isUnbounded: false |
|
}; |
|
|
|
displayDualSimplexSolution(); |
|
} |
|
|
|
function displayDualSimplexSolution() { |
|
if (!dualSimplexSolution) return; |
|
|
|
dualSimplexStepsDiv.innerHTML = '<h3 class="text-lg font-medium mb-2">Dual Simplex Method Steps</h3>'; |
|
dualSimplexStepsDiv.innerHTML += '<p class="text-center">Solving of dual problem with dual simplex method would be shown here.</p>'; |
|
|
|
dualSimplexResultDiv.classList.remove('hidden'); |
|
|
|
|
|
let solutionText = '<div class="grid grid-cols-2 gap-4">'; |
|
|
|
solutionText += '<div><h4 class="font-medium mb-2">Dual Variables:</h4><ul class="list-disc pl-5">'; |
|
dualSimplexSolution.variables.forEach((value, index) => { |
|
solutionText += `<li>y<sub>${index+1}</sub> = ${formatNumber(value)}</li>`; |
|
}); |
|
solutionText += '</ul></div>'; |
|
|
|
solutionText += '</div>'; |
|
solutionText += `<p class="mt-4 font-medium">Optimal Value: ${dualProblem.direction === 'max' ? 'Max' : 'Min'} W = ${formatNumber(dualSimplexSolution.optimalValue)}</p>`; |
|
|
|
|
|
if (simplexSolution && Math.abs(simplexSolution.optimalValue - dualSimplexSolution.optimalValue) < 0.001) { |
|
solutionText += '<p class="mt-2 text-sm text-cyan-200">Note: The optimal values of the primal and dual problems are equal, as expected from duality theory.</p>'; |
|
} |
|
|
|
dualSimplexSolutionDiv.innerHTML = solutionText; |
|
} |
|
|
|
function switchTab(tabId) { |
|
|
|
tabButtons.forEach(button => { |
|
if (button.getAttribute('data-tab') === tabId) { |
|
button.classList.add('active'); |
|
} else { |
|
button.classList.remove('active'); |
|
} |
|
}); |
|
|
|
|
|
tabContents.forEach(content => { |
|
if (content.id === `${tabId}-tab`) { |
|
content.classList.remove('hidden'); |
|
} else { |
|
content.classList.add('hidden'); |
|
} |
|
}); |
|
} |
|
|
|
function clearResults() { |
|
simplexStepsDiv.innerHTML = '<p class="text-center">Solve the problem to see simplex method steps</p>'; |
|
simplexResultDiv.classList.add('hidden'); |
|
|
|
dualProblemDisplay.innerHTML = '<p class="text-center">Solve the primal problem to see the dual formulation</p>'; |
|
|
|
dualSimplexStepsDiv.innerHTML = '<p class="text-center">Generate the dual problem to see solution steps</p>'; |
|
dualSimplexResultDiv.classList.add('hidden'); |
|
} |
|
|
|
function formatNumber(num) { |
|
if (num % 1 === 0) { |
|
return num.toString(); |
|
} |
|
|
|
|
|
const tolerance = 1.0E-6; |
|
const fractions = [ |
|
{ numerator: 1, denominator: 2, value: 0.5 }, |
|
{ numerator: 1, denominator: 3, value: 1/3 }, |
|
{ numerator: 2, denominator: 3, value: 2/3 }, |
|
{ numerator: 1, denominator: 4, value: 0.25 }, |
|
{ numerator: 3, denominator: 4, value: 0.75 }, |
|
{ numerator: 1, denominator: 5, value: 0.2 }, |
|
{ numerator: 2, denominator: 5, value: 0.4 }, |
|
{ numerator: 3, denominator: 5, value: 0.6 }, |
|
{ numerator: 4, denominator: 5, value: 0.8 } |
|
]; |
|
|
|
for (const frac of fractions) { |
|
if (Math.abs(num - frac.value) < tolerance) { |
|
return `<span class="fraction"><span class="numerator">${frac.numerator}</span><span class="slash">/</span><span class="denominator">${frac.denominator}</span></span>`; |
|
} |
|
if (Math.abs(num + frac.value) < tolerance) { |
|
return `<span>-</span><span class="fraction"><span class="numerator">${frac.numerator}</span><span class="slash">/</span><span class="denominator">${frac.denominator}</span></span>`; |
|
} |
|
} |
|
|
|
|
|
return Math.round(num * 10000) / 10000; |
|
} |
|
</script> |
|
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - <a href="https://enzostvs-deepsite.hf.space?remix=Czarevich/simplex" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body> |
|
</html> |