JavaScript / WebAssembly
These examples assume that you have loaded scs.js, either in Node.js via
const createSCS = require('scs-solver'); // if using CommonJS
import createSCS from 'scs-solver'; // if using ES6 modules
or in the browser via a script tag or an ES6 module import (see the install page).
Live Demo
Here is a live demo, which computes the point \(x\) that is closest to the origin, subject to lying in the half-plane \(x_1 + x_2 \ge b\) and within a disc of radius \(r\) centered at \(c = (-2, 2.5)\). Here, the values of \(b\) and \(r\) are set by sliders. The solution is computed using two second-order cones, one for the disc constraint, and one to encode \(\|x\|_2 \leq d\), where \(d\) is the distance from the origin, which is the quantity to be minimized.
Loading...
Show code
<script src="https://unpkg.com/scs-solver/dist/scs.js"></script>
<style>
#solution {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.5rem;
}
#canvas {
border: 1px solid #ccc;
max-width: 100%;
}
#canvas text {
font-style: italic;
}
pre#output {
font-size: 75%;
margin: 0;
max-width: 180px;
overflow-x: auto;
}
</style>
<div>
<label for="radiusSlider">Radius (<i>r</i>):</label>
<input type="range" id="radiusSlider" min="0.5" max="6" step="0.1" value="5">
<span id="radiusVal">5</span>
</div>
<div>
<label for="lineSlider">Line Offset (<i>b</i>):</label>
<input type="range" id="lineSlider" min="-3" max="8" step="0.1" value="3">
<span id="lineVal">3</span>
</div>
<div id="solution">
<svg id="canvas" width="500" height="500"></svg>
<pre id="output">Loading...</pre>
</div>
<script>
(async function () {
const SCS = await createSCS();
const svg = document.getElementById('canvas');
const output = document.getElementById('output');
let r = parseFloat(document.getElementById('radiusSlider').value);
let b_val = parseFloat(document.getElementById('lineSlider').value);
const c = [-2, 2.5]; // Center of circle
const a = [1, 1]; // Coefficients for linear constraint
let solutionPoint = [0, 0];
document.getElementById('radiusSlider').addEventListener('input', e => {
r = parseFloat(e.target.value);
document.getElementById('radiusVal').textContent = r;
solveAndDraw();
});
document.getElementById('lineSlider').addEventListener('input', e => {
b_val = parseFloat(e.target.value);
document.getElementById('lineVal').textContent = b_val;
solveAndDraw();
});
async function solveAndDraw() {
// Dimensions
const m = 7; // total constraint rows: 1 for line and 3+3 for SOC
const n = 3; // variables: x1, x2, d
// Way to construct the problem using math.js:
// // Create a sparse matrix A of size m x n using math.js
// let A = math.sparse(math.zeros(m, n));
// let b = Array(m).fill(0);
// // Indices for rows in constraints:
// // Linear constraint: 0, SOC1: 1, 2, 3, SOC2: 4, 5, 6
// // Linear constraint: a^T x >= b_val -> -x1 + -x2 + slack == -b_val
// A.set([0, 0], -a[0]);
// A.set([0, 1], -a[1]);
// b[0] = -b_val;
// // Constraint SOC1: ||[x1,x2]||_2 <= d -> (d, x1, x2) ∈ SOC
// A.set([1, 2], -1); // coefficient for d in row0
// A.set([2, 0], 1); // coefficient for x1 in row1
// A.set([3, 1], 1); // coefficient for x2 in row2
// // No additional b entries needed (remains 0)
// // Constraint SOC2: ||[x1 - c1, x2 - c2]||_2 <= r -> (r, x1-c1, x2-c2) ∈ SOC
// b[4] = r; // row3: constant term = r
// A.set([5, 0], 1); // row4: coefficient for x1
// b[5] = c[0]; // adjust for x1 - c1
// A.set([6, 1], 1); // row5: coefficient for x2
// b[6] = c[1]; // adjust for x2 - c2
// // Extract CSC arrays from math.js sparse matrix
// const A_data = A._values;
// const A_index = A._index;
// const A_ptr = A._ptr;
// Directly construct the problem using arrays:
// Objective: minimize t
const c_vec = [0, 0, 1];
const A_index = [5, 2, 0, 6, 3, 0, 1];
const A_ptr = [0, 3, 6, 7];
const A_data = [1, 1, -1, 1, 1, -1, -1];
const b = [-b_val, 0, 0, 0, r, c[0], c[1]];
const data = {
m: m,
n: n,
A_x: A_data,
A_i: A_index,
A_p: A_ptr,
b: b,
c: c_vec
};
console.log(data);
// Cone specification: one linear inequality, two SOC cones of size 3 each
const cone = {
l: 1,
q: [3, 3],
qsize: 2,
};
const settings = new SCS.ScsSettings();
SCS.setDefaultSettings(settings);
settings.epsAbs = 1e-6;
settings.epsRel = 1e-6;
settings.verbose = 0;
const solution = SCS.solve(data, cone, settings);
for (const key in solution.info) {
if (typeof solution.info[key] === 'number') {
solution.info[key] = Math.round(solution.info[key] * 10000) / 10000;
}
}
if (solution.status == "SOLVED") {
const x_sol = solution.x.map(x => Math.round(x * 1000) / 1000);
solutionPoint = [x_sol[0], x_sol[1]];
let text = JSON.stringify({
status: solution.status,
x: x_sol,
info: solution.info
}, null, 2).split('\n');
text[3] = text[3].padEnd(12) + "// x1";
text[4] = text[4].padEnd(12) + "// x2";
text[5] = text[5].padEnd(12) + "// d";
output.textContent = text.join('\n');
console.log(text.join('\n'));
} else if (solution.status == "INFEASIBLE") {
solutionPoint = null;
output.textContent = JSON.stringify({
status: solution.status,
info: solution.info
}, null, 2);
} else {
output.textContent = JSON.stringify({
status: solution.status,
info: solution.info
}, null, 2);
}
drawScene();
}
function drawScene() {
// Clear existing SVG content
while (svg.firstChild) {
svg.removeChild(svg.firstChild);
}
// Coordinate transformation
function toSVGCoords(x, y) {
const scale = 500 / 20;
const cx = 250 + x * scale;
const cy = 250 - y * scale;
return [cx, cy];
}
// Draw circle
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
const [c_svgX, c_svgY] = toSVGCoords(c[0], c[1]);
circle.setAttribute("cx", c_svgX);
circle.setAttribute("cy", c_svgY);
circle.setAttribute("r", r * (500 / 20));
circle.setAttribute("stroke", "blue");
circle.setAttribute("fill", "rgba(0, 0, 255, 0.05)");
svg.appendChild(circle);
// Draw point c
const cCircle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
cCircle.setAttribute("cx", c_svgX);
cCircle.setAttribute("cy", c_svgY);
cCircle.setAttribute("r", 2);
cCircle.setAttribute("fill", "blue");
svg.appendChild(cCircle);
if (r >= 1.5) {
// label
const cText = document.createElementNS("http://www.w3.org/2000/svg", "text");
cText.setAttribute("x", c_svgX + 3);
cText.setAttribute("y", c_svgY - 3);
cText.textContent = "c";
svg.appendChild(cText);
// Radius line
const radiusLine = document.createElementNS("http://www.w3.org/2000/svg", "line");
const rEnd = toSVGCoords(c[0] - r / Math.sqrt(2), c[1] - r / Math.sqrt(2));
radiusLine.setAttribute("x1", c_svgX);
radiusLine.setAttribute("y1", c_svgY);
radiusLine.setAttribute("x2", rEnd[0]);
radiusLine.setAttribute("y2", rEnd[1]);
radiusLine.setAttribute("stroke", "blue");
svg.appendChild(radiusLine);
// label
const rText = document.createElementNS("http://www.w3.org/2000/svg", "text");
const radiusLabelPos = toSVGCoords(c[0] - r / Math.sqrt(2) * 0.5 - 0.2, c[1] - r / Math.sqrt(2) * 0.5 + 0.2);
rText.setAttribute("x", radiusLabelPos[0]);
rText.setAttribute("y", radiusLabelPos[1]);
rText.textContent = "r";
radiusLine.setAttribute("stroke-dasharray", "5,2");
svg.appendChild(rText);
}
// Draw line
const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
const pt1 = toSVGCoords(-10, b_val + 10);
const pt2 = toSVGCoords(10, b_val - 10);
line.setAttribute("x1", pt1[0]);
line.setAttribute("y1", pt1[1]);
line.setAttribute("x2", pt2[0]);
line.setAttribute("y2", pt2[1]);
line.setAttribute("stroke", "green");
svg.appendChild(line);
// Shade area above the line
const polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon");
polygon.setAttribute("points", `${pt1[0]},${pt1[1]} ${pt2[0]},${pt2[1]} ${pt2[0]},-500 ${pt1[0]},-500`);
polygon.setAttribute("fill", "rgba(0, 255, 0, 0.05)");
svg.appendChild(polygon);
// label offset b
const bText = document.createElementNS("http://www.w3.org/2000/svg", "text");
const bTextPos = toSVGCoords(0, b_val);
bText.setAttribute("x", bTextPos[0] - 17);
bText.setAttribute("y", bTextPos[1] + 5);
bText.textContent = "b";
svg.appendChild(bText);
// tick
const bTick = document.createElementNS("http://www.w3.org/2000/svg", "line");
bTick.setAttribute("x1", bTextPos[0] - 3);
bTick.setAttribute("y1", bTextPos[1]);
bTick.setAttribute("x2", bTextPos[0] + 3);
bTick.setAttribute("y2", bTextPos[1]);
bTick.setAttribute("stroke", "black");
svg.appendChild(bTick);
// Draw axes
const axisX = document.createElementNS("http://www.w3.org/2000/svg", "line");
axisX.setAttribute("x1", 0);
axisX.setAttribute("y1", 250);
axisX.setAttribute("x2", 500);
axisX.setAttribute("y2", 250);
axisX.setAttribute("stroke", "black");
svg.appendChild(axisX);
const axisY = document.createElementNS("http://www.w3.org/2000/svg", "line");
axisY.setAttribute("x1", 250);
axisY.setAttribute("y1", 0);
axisY.setAttribute("x2", 250);
axisY.setAttribute("y2", 500);
axisY.setAttribute("stroke", "black");
svg.appendChild(axisY);
// Draw optimal point or infeasibility message
if (solutionPoint) {
const [optX, optY] = toSVGCoords(solutionPoint[0], solutionPoint[1]);
const optCircle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
optCircle.setAttribute("cx", optX);
optCircle.setAttribute("cy", optY);
optCircle.setAttribute("r", 4);
optCircle.setAttribute("fill", "red");
svg.appendChild(optCircle);
// label
const optText = document.createElementNS("http://www.w3.org/2000/svg", "text");
optText.setAttribute("x", optX + 5);
optText.setAttribute("y", optY - 5);
optText.textContent = "x";
svg.appendChild(optText);
} else {
const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
text.setAttribute("x", 400);
text.setAttribute("y", 100);
text.setAttribute("text-anchor", "middle");
text.setAttribute("dominant-baseline", "middle");
text.textContent = "Infeasible";
svg.appendChild(text);
}
}
// Initial solve and draw
solveAndDraw();
})();
</script>
Basic Usage
Here’s the basic example from C translated to JavaScript:
createSCS().then(SCS => {
const data = {
m: 3,
n: 2,
A_x: [-1.0, 1.0, 1.0, 1.0],
A_i: [0, 1, 0, 2],
A_p: [0, 2, 4],
P_x: [3.0, -1.0, 2.0],
P_i: [0, 0, 1],
P_p: [0, 1, 3],
b: [-1.0, 0.3, -0.5],
c: [-1.0, -1.0]
};
const cone = {
z: 1,
l: 2,
};
const settings = new SCS.ScsSettings();
SCS.setDefaultSettings(settings);
settings.epsAbs = 1e-9;
settings.epsRel = 1e-9;
const solution = SCS.solve(data, cone, settings);
console.log(solution);
// re-solve using warm start (will be faster)
settings.warmStart = true;
const solution2 = SCS.solve(data, cone, settings, solution);
});
This prints the solution object to the console:
{
x: [ 0.3000000000043908, -0.6999999999956144 ],
y: [ 2.699999999995767, 2.0999999999869825, 0 ],
s: [ 0, 0, 0.1999999999956145 ],
info: {
iter: 100,
status: 'solved',
linSysSolver: 'sparse-direct-amd-qdldl',
statusVal: 1,
scaleUpdates: 0,
pobj: 1.2349999999907928,
dobj: 1.2350000000001042,
resPri: 4.390808429506794e-12,
resDual: 1.4869081633461182e-13,
gap: 9.311465734712679e-12,
resInfeas: 1.3043478260851176,
resUnbddA: NaN,
resUnbddP: NaN,
compSlack: 0,
setupTime: 2.796667,
solveTime: 0.505584,
scale: 0.1,
rejectedAccelSteps: 0,
acceptedAccelSteps: 0,
linSysTime: 0.047704000000000024,
coneTime: 0.07804600000000002,
accelTime: 0
},
statusVal: 1,
status: 'SOLVED'
}
Entropy Example
Next, we will consider a problem involving maximum entropy. Given a vector \(y \in \mathbf{R}^n\), we want to optimize a function involving entropy over the unit simplex.
It is known that for the optimal solution, we have \(x_i \propto e^{y_i}\).
This problem can be formulated using the (primal) exponential cone, defined as
Our formulation is then:
To implement this problem in JavaScript, we will use the sparse matrix implementation from the Math.js library.
const createSCS = require('./out/scs.js');
const math = require('./math.js');
createSCS().then(SCS => {
const n = 5;
const y = Array.from({ length: n }, () => Math.random());
const A = math.matrix('sparse');
const b = [];
let constraintIndex = 0;
const x_vars = Array.from({ length: n }, (_, i) => i);
const t_vars = Array.from({ length: n }, (_, i) => i + n);
// equality constraint (zero cone)
let numEqCones = 0;
for (let i = 0; i < n; i++) {
A.set([constraintIndex, x_vars[i]], 1);
}
b.push(1);
constraintIndex++;
numEqCones++;
// inequality constraints (positive cone)
let numPosCones = 0;
for (let i = 0; i < n; i++) {
A.set([constraintIndex, x_vars[i]], -1);
b.push(0);
constraintIndex++;
numPosCones++;
}
// exponential cone constraints
let numExpCones = 0;
for (let i = 0; i < n; i++) {
// (-t_i, x_i, 1) in exponential cone
A.set([constraintIndex, t_vars[i]], 1);
b.push(0);
constraintIndex++;
A.set([constraintIndex, x_vars[i]], -1);
b.push(0);
constraintIndex++;
// last element is constant, so A has a 0-row; set arbitrary index to 0
A.set([constraintIndex, x_vars[i]], 0);
b.push(1);
constraintIndex++;
numExpCones++;
}
// objective function
const c = Array.from({ length: 2 * n }, (_, i) => 0);
for (let i = 0; i < n; i++) {
c[x_vars[i]] = -y[i];
c[t_vars[i]] = 1;
}
const data = {
m: A._size[0],
n: A._size[1],
A_x: A._values,
A_i: A._index,
A_p: A._ptr,
b: b,
c: c,
};
const cone = {
z: numEqCones,
l: numPosCones,
ep: numExpCones,
};
const settings = new SCS.ScsSettings();
SCS.setDefaultSettings(settings);
settings.epsAbs = 1e-9;
settings.epsRel = 1e-9;
const solution = SCS.solve(data, cone, settings);
console.log("SCS solution:", solution.x.slice(0, n));
const denominator = y.map(y_i => Math.exp(y_i)).reduce((a, b) => a + b, 0);
const predicted_solution = y.map(y_i => Math.exp(y_i) / denominator);
console.log("Predicted solution:", predicted_solution);
});