<div class="canvas" name="PID">
<canvas id="PID" width="300" height="200"></canvas>
<br><input type="range" id="kp_input"> kp
<br><input type="range" id="ki_input"> ki
<br><input type="range" id="kd_input"> kd
<style>
input[type="range"]:hover {opacity: 1;}
input[type="range"] {width: 275; cursor: pointer;}
</style>
<script id="PID.js">
(function() {
	// parameters in numerical simulation
	const rt = [0, 10];	// time interval
	const Δt = .01;		// time step
	const nt = Math.floor((rt[1] - rt[0]) / Δt) + 1;

	// parameters in differential equation
	var kp = 1.0;
	var ki = 0.0;
	var kd = 0.0;
	const c0 = 2;
	const c1 = 3;
	const c2 = 1;

	// numerical simulation
	var R    = new Float64Array(nt+1);	// reference
	var E    = new Float64Array(nt+1);	// error
	var Esum = new Float64Array(nt+1);	// cumulative error
	var U    = new Float64Array(nt+1);	// plant input
	var Y    = new Float64Array(nt+1);	// plant output

	function initial_condition() {
		// unit step function
		R[0] = 0;
		for (let t=1; t<=nt; t++)
			R[t] = 1;

		E[0] = Esum[0] = U[0] = Y[0] = 0;
	}

	function iteration() {
		for (let t=1; t<=nt; t++) {
			// closed-loop controller
			E[t] = R[t-1] - Y[t-1];
			Esum[t] = Esum[t-1] + E[t];

			// controller: PID controller
			//                     ⌠              d
			// u(t) = kp e(t) + ki ⎮ e(t) dt + kd —— e(t)
			//                     ⌡              dt
			U[t] = kp * E[t]
			     + ki * Esum[t] * Δt
			     + kd * (E[t] - E[t-1]) / Δt;

			// plate: LTI system (implicit form)
			//              d            d²
			// c₀ y(t) + c₁ —— y(t) + c₂ ——— y(t) = u(t)
			//              dt           dt²
			//
			// causal finite difference
			//   c₀ y(t)
			// + c₁ (y(t) - y(t-1)) / (2Δt)
			// + c₂ (y(t) - 2y(t-1) + y(t-2) / (Δt)²)
			// = u(t)
			//
			//   (c₀ + c₁/(2Δt) + c₂/(Δt)²) y(t)
			// + (-c₁/(2Δt) - 2c₂/(Δt)²) y(t-1)
			// + (c₂/(Δt)²) y(t-2)
			// = u(t)

			var k0 = c0 + c1/(2*Δt) + c2/(Δt*Δt);
			var k1 = - c1/(2*Δt) - 2*c2/(Δt*Δt);
			k1 *= (t-1 >= 0) ? Y[t-1] : 0;
			var k2 = c2/(Δt*Δt);
			k2 *= (t-2 >= 0) ? Y[t-2] : 0;

			Y[t] = (U[t] - k1 - k2) / k0;
		}
	}

	function simulation() {
		initial_condition();
		iteration();
	}

	var canvas  = document.getElementById('PID');
	var ctx     = canvas.getContext('2d');

	const offset = [20, 20];
	function draw() {
		simulation();
		ctx.clearRect(0, 0, canvas.width, canvas.height);

		ctx.save();
		ctx.scale(1, -1);
		ctx.translate(0, -canvas.height);
		ctx.translate(offset[0], offset[1]);
		draw_solution();
		draw_axis(canvas.width - offset[0] * 2, canvas.height - offset[1] * 2.5);
		ctx.restore();

		draw_text();
	}

	function draw_solution() {
		const x_scale = (canvas.width - offset[0] * 2) / nt;
		const y_scale = 100;

		// R: unit step function
		ctx.beginPath();
		ctx.moveTo(0, 0);
		for (let t=1; t<=nt; ++t) {
			var x = (t-1) * x_scale;
			var y = R[t] * y_scale;
			ctx.lineTo(x, y);
		}
		ctx.lineWidth = 2;
		ctx.setLineDash([3]);
		ctx.strokeStyle = 'rgb(24,198,24)';
		ctx.stroke();

		// Y
		ctx.beginPath();
		ctx.moveTo(0, 0);
		for (let t=1; t<=nt; ++t) {
			var x = (t-1) * x_scale;
			var y = Y[t] * y_scale;
			ctx.lineTo(x, y);
		}
		ctx.lineWidth = 3;
		ctx.setLineDash([]);
		ctx.strokeStyle = 'rgb(198,24,24)';
		ctx.stroke();
	}

	function DrawArrow(x1, y1, x2, y2, len, angle, gap) {
		var slope = Math.atan2(y2 - y1, x2 - x1);
		x1 += gap * Math.cos(slope);
		y1 += gap * Math.sin(slope);
		x2 -= gap * Math.cos(slope);
		y2 -= gap * Math.sin(slope);
	
		ctx.beginPath();
		ctx.moveTo(x1, y1);
		ctx.lineTo(x2, y2);
		ctx.lineTo(x2 - len * Math.cos(slope-angle), y2 - len * Math.sin(slope-angle));
		ctx.moveTo(x2, y2);
		ctx.lineTo(x2 - len * Math.cos(slope+angle), y2 - len * Math.sin(slope+angle));
		ctx.stroke();
	}

	function draw_axis(scalarX, scalarY) {
		ctx.lineWidth = 1;
		ctx.strokeStyle = "gray";
		DrawArrow(-16, 0, +scalarX, 0, 8, Math.PI / 5, 0);
		DrawArrow(0, -16, 0, +scalarY, 8, Math.PI / 5, 0);

		ctx.scale(1, -1);
		ctx.fillStyle = "gray";
		ctx.font = "16pt Arial";
		ctx.textAlign = "center";
		ctx.textBaseline = "middle";
		ctx.fillText("t", +scalarX+8, 0);
		ctx.textBaseline = "bottom";
		ctx.fillText("y(t)", 8/2, -scalarY-8);
		ctx.scale(1, -1);
	}

	function draw_text() {
		ctx.font = "16pt Arial";
		ctx.textAlign = "right";
		ctx.textBaseline = "top";
		ctx.fillStyle = "rgb(127,127,127)";
		var s = `kp=${kp.toFixed(1)}  ki=${ki.toFixed(1)}  kd=${kd.toFixed(1)}`;
		ctx.fillText(s, canvas.width - offset[0], 0);
	}

	var kp_input = document.querySelector("#kp_input");
	var ki_input = document.querySelector("#ki_input");
	var kd_input = document.querySelector("#kd_input");

	kp_init();
	ki_init();
	kd_init();
	function kp_init() {
		kp_input.min = 0;
		kp_input.max = 99;
		kp_input.valueAsNumber = kp * 10;
	}
	function ki_init() {
		ki_input.min = 0;
		ki_input.max = 99;
		ki_input.valueAsNumber = ki * 10;
	}
	function kd_init() {
		kd_input.min = 0;
		kd_input.max = 99;
		kd_input.valueAsNumber = kd * 10;
	}
	kp_input.oninput = function(e) {
		kp = this.valueAsNumber / 10;
		requestAnimationFrame(draw, canvas);
	}
	ki_input.oninput = function(e) {
		ki = this.valueAsNumber / 10;
		requestAnimationFrame(draw, canvas);
	}
	kd_input.oninput = function(e) {
		kd = this.valueAsNumber / 10;
		requestAnimationFrame(draw, canvas);
	}

	draw();
})();
</script>
</div><div class="canvas" name="production">
<canvas id="production" width="300" height="200"></canvas>
<script id="chemical_kinetics.js">
(function(){
var production = function() {
	const k = 0.5;
	var x0 = [1];
	var f = [function(x) {return k;}];
	var label = ['A'];
	return {f, x0, label, xscale: 100, tscale: 2};
}();

var delay = function() {
	const k = 2;
	var x0 = [1];
	var f = [function(x) {return -k*x[0];}];
	var label = [''];
	return {f, x0, label, xscale: 100, tscale: 2};
}();

var production_and_delay = function() {
	const k0 = 0.5, k1 = 2;
	var x0 = [1];
	var f = [function(x) {return k0 - k1*x[0];}];
	var label = ['aₛₛ'];
	return {f, x0, label, xscale: 100, tscale: 2};
}();

var irreversible_conversion = function() {
	var k = 0.5;
	var x0 = [1,0];
	var f = [function(x) {return -k*x[0];},
			 function(x) {return +k*x[0];}];
	var label = ['aₛₛ','bₛₛ'];
	return {f, x0, label, xscale: 100, tscale: 2,
			seed: 'boundary', step: 1000, size: 1, arrow: [20, 500]};
}();

var reversible_conversion = function() {
	const k1 = 0.5, k_1 = 1;
	var x0 = [1,0];
	var f = [function(x) {return -k1*x[0]+k_1*x[1];},
	         function(x) {return +k1*x[0]-k_1*x[1];}];
	var label = ['aₛₛ','bₛₛ'];
	return {f, x0, label, xscale: 100, tscale: 2};
}();

var rapid_equilibrium_assumption = function() {
	const k1 = 1, k_1 = .5, k2 = .1;
	var x0 = [1,0];
	var f = [function(x) {return -k1*x[0]+k_1*x[1];},
	         function(x) {return +k1*x[0]-k_1*x[1]-k2*x[1];}];
	var label = ['',''];
	return {f, x0, label, xscale: 100, tscale: 10};
}();

var quasi_steady_state_assumption = function() {
	const k1 = 1, k_1 = .5, k0 = .1, k2 = .1;
	var x0 = [1,0];
	var f = [function(x) {return k0-k1*x[0]+k_1*x[1];},
	         function(x) {return   +k1*x[0]-k_1*x[1]-k2*x[1];}];
	var label = ['',''];
	return {f, x0, label, xscale: 100, tscale: 10};
}();

var inhibition = function() {
	var k1 = 1, k2 = 1, k3 = .1, k4 = .1;
	var K1 = 1, K2 = 1, n1 = 2, n2 = 2;
	var x0 = [10, 5];
	var f = [function(x) {return k1/(1 + Math.pow(x[1]/K2, n1)) - k3*x[0];},
			 function(x) {return k2                             - k4*x[1];}];
	var label = ['A', 'B'];
	return {f, x0, label, xscale: 15, tscale: 10,
			seed: 'boundary', step: 1000, size: 1, arrow: [20]};
}();

var cross_inhibition = function() {
	var k1 = 1, k2 = 1, k3 = .1, k4 = .1;
	var K1 = 1, K2 = 1, n1 = 2, n2 = 2;
	var x0 = [10, 5];
	var f = [function(x) {return k1/(1 + Math.pow(x[1]/K2, n1)) - k3*x[0];},
			 function(x) {return k2/(1 + Math.pow(x[0]/K1, n2)) - k4*x[1];}];
	var label = ['A', 'B'];
	return {f, x0, label, xscale: 15, tscale: 10,
			seed: 'boundary', step: 1000, size: 1, arrow: [20]};
}();

var Collins_toggle_switch = function() {
	var α0 = 1, α1 = 1, b = 2, c = 2, δ1 = 1, δ2 = 1;
	var x0 = [10,1];
	var f = [function(x) {return α0/(1+(x[1]**b)) - δ1*x[0];},
			 function(x) {return α1/(1+(x[0]**c)) - δ1*x[1];}];
	var label = ['',''];
	return {f, x0, label, xscale: 10, tscale: 10,
			seed: 'boundary', step: 1000, size: 1, arrow: [20]};
}();

var Goodwin_oscillator = function() {
	var a = 1, b = 1, c = 1, d = 1, e = 1, _f = 1;
	var k = .1, n = 10;
	var x0 = [15, 15, 15];
	var f = [function(x) {return a/((k**n)+(x[2]**n)) - b*x[0];},
			 function(x) {return c*x[0] - d*x[1];},
			 function(x) {return e*x[1] - _f*x[2];}];
	var label = ['','',''];
	return {f, x0, label, xscale: 10, tscale: 10,
			seed: 'boundary', step: 1000, size: 1, arrow: [20]};
}();

var Elowitz_Leibler_repressilator = function() {
	var α0 = .5, α = 1, β = 1, n = 10;
	var x0 = [0,0,0,1,0,0];
	var f = [function(x) {return α0 + α/(1+(x[5]**n)) - x[0];},
			 function(x) {return α0 + α/(1+(x[3]**n)) - x[1];},
			 function(x) {return α0 + α/(1+(x[4]**n)) - x[2];},
			 function(x) {return β*(x[0]-x[3]);},
			 function(x) {return β*(x[1]-x[4]);},
			 function(x) {return β*(x[2]-x[5]);}];
	var label = ['','','','','',''];
	return {f, x0, label, xscale: 100, tscale: 10,
			seed: 'boundary', step: 1000, size: 1, arrow: [20]};
}();

var autocatalysis = function() {
	const α = .5, K = 1, δ = .1;
	var x0 = [.1];
	var f = [function(x) {return α*(x[0]/K)/(1+x[0]/K) - δ*x[0];}];
	var label = [''];
	return {f, x0, label, xscale: 100, tscale: 10};
}();

var autoinhibition = function() {
	const α = .5, K = 1, δ = .1;
	var x0 = [.1];
	var f = [function(x) {return α/(1+x[0]/K) - δ*x[0];}];
	var label = [''];
	return {f, x0, label, xscale: 100, tscale: 10};
}();

	new Plot(production, 'TimeSeries', 'production');
	new Plot(delay, 'TimeSeries', 'delay');
	new Plot(production_and_delay, 'TimeSeries', 'production_and_delay');

	new Plot(irreversible_conversion, 'TimeSeries', 'irreversible_conversion');
	new Plot(reversible_conversion, 'TimeSeries', 'reversible_conversion');
	new Plot(rapid_equilibrium_assumption, 'TimeSeries', 'rapid_equilibrium_assumption');
	new Plot(quasi_steady_state_assumption, 'TimeSeries', 'quasi_steady_state_assumption');

	new Plot(inhibition, 'Phase', 'inhibition');
	new Plot(cross_inhibition, 'Phase', 'cross_inhibition');

	new Plot(Collins_toggle_switch, 'Phase', 'Collins_toggle_switch');
	new Plot(Goodwin_oscillator, 'TimeSeries', 'Goodwin_oscillator');
	new Plot(Elowitz_Leibler_repressilator, 'TimeSeries', 'Elowitz_Leibler_repressilator');

	new Plot(autocatalysis, 'TimeSeries', 'autocatalysis');
	new Plot(autoinhibition, 'TimeSeries', 'autoinhibition');

function Plot(pde, type, canvas_name) {
	var canvas = document.getElementById(canvas_name);
	var ctx    = canvas.getContext('2d');

	var [f, x0, label, xscale, tscale, seed, step, size, arrow] = Object.values(pde);

	var draw   = null;
	if      (type === 'TimeSeries') draw = DrawTimeSeries;
	else if (type === 'Phase')      draw = DrawPhase;

	var labelX, labelY;
	if      (type === 'TimeSeries') [labelX, labelY] = ['t', ' f(t)'];
	else if (type === 'Phase')      [labelX, labelY] = label;

	const _axis  = 10;	// offset for axis 
	const _label = 20;	// offset for label
	const width  = canvas.width  - _axis - _label;	// x-axis length
	const height = canvas.height - _axis - _label;	// y-axis length

	main();

	function main() {
		ctx.save();
		ctx.translate(0, canvas.height);
		ctx.scale(1, -1);
		ctx.translate(_axis, _axis);
		DrawXY(width, height, labelX, labelY);
		draw();
		ctx.restore();
	}

	function DrawTimeSeries() {
		const n = f.length;
		const dt = 0.01;
		const colors = ["rgb(192,0,0)", "rgb(0,192,0)", "rgb(0,0,192)",
                        "rgb(0,0,0,0)", "rgb(0,0,0,0)", "rgb(0,0,0,0)"];

		for (let k=n-1; k>=0; --k) {
			var x     = x0.slice(0);
			var xnext = x0.slice(0);

			// curve
			ctx.beginPath();
			for (let w=0; w<width; w++) {
				ctx.lineTo(w, x[k] * xscale);
				for (let t=0; t<tscale; ++t) {
					for (let i=0; i<n; ++i) xnext[i] = x[i] + f[i](x) * dt;
					for (let i=0; i<n; ++i) x[i] = xnext[i];

				}
			}
			ctx.strokeStyle = colors[k];
			ctx.lineWidth   = 2.25;
			ctx.stroke();

			// label
			ctx.save();
			ctx.scale(1, -1);
			ctx.font = "12pt Tahoma";
			ctx.textBaseline = "middle";
			ctx.textAlign = "left";
			ctx.fillStyle = "rgb(127,127,127)";
			ctx.fillText(label[k], width, -(x[k] * xscale));
			ctx.restore();
		}
	}

	function DrawPhase() {
		const n = f.length;
		if (n !== 2) return;
		const dt = 0.01;

		var x0s = [];
		if (seed === 'boundary') {
			var coords = [20, 60, 120, 180];
			for (let c of coords) {
				// axis label overlapping
				if (c < width) x0s.push([c,0]);
				if (c < height) x0s.push([0,c]);
				x0s.push([canvas.width-_axis,c], [c,canvas.height-_axis]);
			}
		} else if (seed === 'line') {
			var coords = [10, 20, 30, 40, 50];
			for (let c of coords) x0s.push([c,c]);
		}

		for (let x0 of x0s) {
			var x     = [x0[0] / xscale, x0[1] / xscale];
			var xnext = [x0[0] / xscale, x0[1] / xscale];

			// curve
			ctx.beginPath();
			for (let s=0; s<step; ++s) {
				ctx.lineTo(x[0] * xscale, x[1] * xscale);

				// arraw head
				if (arrow.includes(s)) {
					var slope = Math.atan2(f[1](x), f[0](x));
					var angle = Math.PI / 8;
					var len   = .35;
					var l0 = x[0] - len * Math.cos(slope-angle);
					var l1 = x[1] - len * Math.sin(slope-angle);
					var r0 = x[0] - len * Math.cos(slope+angle);
					var r1 = x[1] - len * Math.sin(slope+angle);
					ctx.lineTo(l0   * xscale, l1   * xscale);
					ctx.lineTo(r0   * xscale, r1   * xscale);
					ctx.lineTo(x[0] * xscale, x[1] * xscale);
				}

				for (let t=0; t<tscale; ++t) {
					for (let i=0; i<n; ++i) xnext[i] = x[i] + f[i](x) * dt * size;
					for (let i=0; i<n; ++i) x[i] = xnext[i];
				}
			}
			ctx.strokeStyle = "rgb(192,0,0)";
			ctx.lineWidth   = 1.5;
			ctx.stroke();
		}
	}

	function DrawArrow(x1, y1, x2, y2, len, angle, gap) {
		var slope = Math.atan2(y2 - y1, x2 - x1);
		x1 += gap * Math.cos(slope);
		y1 += gap * Math.sin(slope);
		x2 -= gap * Math.cos(slope);
		y2 -= gap * Math.sin(slope);

		ctx.beginPath();
		ctx.moveTo(x1, y1);
		ctx.lineTo(x2, y2);
		ctx.lineTo(x2 - len * Math.cos(slope-angle), y2 - len * Math.sin(slope-angle));
		ctx.moveTo(x2, y2);
		ctx.lineTo(x2 - len * Math.cos(slope+angle), y2 - len * Math.sin(slope+angle));
		ctx.stroke();
	}

	function DrawXY(scalarX, scalarY, labelX, labelY) {
		ctx.lineWidth = 1;
		ctx.strokeStyle = "gray";
		DrawArrow(0, 0, +scalarX, 0, 8, Math.PI / 5, 0);
		DrawArrow(0, 0, 0, +scalarY, 8, Math.PI / 5, 0);

		ctx.scale(1, -1);
		ctx.font = "12pt Arial";
		ctx.textAlign = "center";
		ctx.textBaseline = "middle";
		ctx.fillStyle = "gray";
		ctx.fillText(labelX, +scalarX+10, 0);
		ctx.textBaseline = "bottom";
		ctx.fillText(labelY, 0, -scalarY-2);
		ctx.scale(1, -1);
	}
}
})();
</script>
</div>