xxxxxxxxxx
424
//CURRENTLY UNDER DEVELOPMENT
/*
TODO
--- Back up ---
Back up the whole project again.
---MODEL LABEL---
Display the IVP near the bottom of the canvas, e.g. using
KaTeX. For the exponential model, this would look like
y' = ry, y(0)=y_0; specifically, put this in terms of the
parameter name (e.g. r), not the parameter value (e.g. 3.1).
Using KaTeX seems straightforward:
https://discourse.processing.org/t/latex-in-processing/19691/3
---FITTING---
Finish the mock-up interface before adding the functionality below. All
that's left for the mock-up is to decide how to incorporate a submit
feature in such a way that its meaning is clear.
An html button that says "fit" might be good enough, but then I might
need to rethink the canvas buttons. In the steps below, I'm assuming
the canvas buttons will work as they do now.
Functionality:
1. When clicking the canvas fit button, an initial guess (based on current)
fitting options should be graphed, and the sliders should be initialized
to the guessed values.
2. Adjusting the sliders should adjust the graph of the model function,
in order to make a better guess.
3. There should be a way to submit the improved guesses as initial values
to the L-M algorithm.
4. Once the L-M algorithm is run, the sliders (and graph) should be
automatically updated to reflect the fitted values. I suppose there
may need to be some visual feedback to make it clear that the fit
button did its job (it might not be obvious if the guess was very good).
--- Testing ---
Test the features (at least with current values, e.g. 10 for initPopSize),
in Chrome (desktop and mobile), FF, Edge. Ask a friend to test in Safari (desktop and mobile) if you can't.
--- Reset ---
When you're finished testing, reset the initial population to 10.
--- Review code ---
Maybe at least quickly read over sketch.js to see if the comments
could be improved.
--- Back up ---
Back up the whole project once more when it's finished.
*/
//See Sketch Files for classes.
//See About folder for TODO, background, and references.
/**** VARIABLES ****/
let fps = 24; //frame rate (frames per second)
let grid; //grid in which cells live
let initPopSize = 10; //initial population size (<=capacity)
let population; //population of cells
let plot1, plot2; //separate plots for simulation and graph
let win; //graph window (includes origin and scale for axes)
let popCurve; //population curve
let meanPopCurve; //mean population curve
let meanData; //mean pop. curve data, formatted for fitting
let popCurves; //all population curves
let popIndex = 0; //index of population in current sim run
let refreshButton; //canvas button instance to restart sim
let refreshImg; //image from which to make refreshButton
let meanButton; //canvas button instance to show/hide mean curve
let fitButton; //canvas button instance to show/hide fit curve
let radioForm; //form with radio buttons for model selection
let sliders; //container for sliders
let sliderSize;
let rExponential; //slider for r in exponential model
let rExponentialLabel;
let rLogistic; //slider for r in logistic model
let rLogisticLabel;
let KLogistic; //slider for K in logistic model
let KLogisticLabel;
let rGompertz; //slider for r in Gompertz model
let rGompertzLabel;
let aGompertz; //slider for a in Gompertz model
let aGompertzLabel;
//fitting options
let exponentialOptions = {
initialValues: [4], //initial param value r
minValues: [0.000001], //r_min
maxIterations: 100,
centralDifference: true //use central (not forward) difference for Jacobian
}
let logisticOptions = {
initialValues: [4, 8712], //initial param values r, K
minValues: [0.000001, initPopSize], //r_min, K_min
maxIterations: 100,
centralDifference: true //use central (not forward) difference for Jacobian
}
let gompertzOptions = {
initialValues: [4, 0.6], //initial param values r, a
minValues: [0.000001, 0.000001], //r_min, a_min
maxIterations: 100,
centralDifference: true //use central (not forward) difference for Jacobian
}
/**** SETUP ****/
function preload() {
refreshImg = loadImage('icon.png');
}
function setup() {
frameRate(fps);
createCanvas(704, 396);
//create plots, graph window, and grid
plot1 = createGraphics(width / 2, height);
plot2 = createGraphics(width / 2, height);
win = createGraphWindow(15, height - 11, 22, 1 / 30);
grid = createGrid(width / 2, height, 4);
//create refresh button with no text, centered at given XY-coordinates
refreshImg.resize(70, 70);
refreshButton = createCanvasButton('', width / 4, height / 2)
refreshButton.image = refreshImg;
refreshButton.shape = 'circle'; //default shape is 'rect'
refreshButton.hidden = true;
//create buttons with given text and XY-coordinates (top-left corner)
meanButton = createCanvasButton('mean', width - 130, 10);
meanButton.hidden = true;
fitButton = createCanvasButton('fit', width - 50, 10);
fitButton.hidden = true;
fitButton.fill = color(232, 51, 232);
fitButton.fillHidden = color(232, 51, 232, 50);
//select radio button form from index.html,
//position it, and set its behavior
radioForm = select('#model-selection');
radioForm.position(width / 2 - 20, height + 20);
radioForm.input(showParams);
//select slider container, sliders, labels from index.html
sliders = select('#sliders'); //container
rExponential = select('#exponential-rate');
rExponentialLabel = select('#exponential-rate-label');
rLogistic = select('#logistic-rate');
rLogisticLabel = select('#logistic-rate-label');
KLogistic = select('#capacity');
KLogisticLabel = select('#capacity-label');
rGompertz = select('#gompertz-rate');
rGompertzLabel = select('#gompertz-rate-label');
aGompertz = select('#inhibitor');
aGompertzLabel = select('#inhibitor-label');
//set slider size and behavior
sliderSize = width / 5;
sliders.size(sliderSize);
rExponential.input(updateParam);
rLogistic.input(updateParam);
KLogistic.input(updateParam);
rGompertz.input(updateParam);
aGompertz.input(updateParam);
//set position of slider container and of labels (relative to container)
sliders.position(width - sliderSize - 80, height + 10);
rExponentialLabel.position(sliderSize + 10, 17);
rLogisticLabel.position(sliderSize + 10, 17);
KLogisticLabel.position(sliderSize + 10, 71);
rGompertzLabel.position(sliderSize + 10, 17);
aGompertzLabel.position(sliderSize + 10, 71);
//create population
population = createPopulation(initPopSize, grid);
//create population curve from
//population history (array of [time, population count] pairs),
//in given graphing window
popCurve = createCurve(population.history, win);
//include population curve in array that will store all curves,
//and build a Curves instance from that array
//(Curves class comes with a drawing method and an averaging method)
popCurves = createCurves([popCurve]);
//create mean population curve
meanPopCurve = popCurves.meanCurve();
}
/**** MODEL FUNCTIONS FOR DATA FITTING ****/
/*
exponential() receives the model parameter and
returns an exponential function of the independent variable
*/
function exponential([r]) {
return t => initPopSize * exp(r * t);
}
/*
logistic() receives the model parameters and
returns a logistic function of the independent variable
*/
function logistic([r, K]) {
let C = (K - initPopSize) / initPopSize;
return t => K / (1 + C * exp(-r * t));
}
/*
gompertz() receives the model parameters and
returns a Gompertz function of the independent variable
*/
function gompertz([r, a]) {
let y0 = initPopSize;
return t => y0 * exp((r / a) * (1 - exp(-a * t)));
}
/**** DRAWING ****/
/*
data: {x: [x0, ...], y: [y0, ...]}
model: function receiving array of params, returning model function
options: {initialValues: [a, b, c, ...], maxIterations: M}
*/
function drawFit(data, model, options) {
let N = data.x.length; //number of (x, y) data points
let xi, yi; //coordinates of ith data point
let fittedParams = levenbergMarquardt(data, model, options);
let fittedCurve = createCurve([], win);
for (let i = 0; i < N; i++) {
xi = data.x[i];
yi = model(fittedParams.parameterValues)(xi);
fittedCurve.points.push([xi, yi]);
}
fittedCurve.draw(plot2);
}
function drawPlot1() {
//make plot
plot1.background(229, 255, 234);
plot1.fill(232, 51, 232, 160);
population.draw(plot1);
//make label
plot1.textSize(28);
plot1.fill(0);
plot1.text("Population: " + population.count, 10, height - 10);
//show plot at given canvas position
imageMode(CORNER); //interpret coords as top-left of image
image(plot1, 0, 0)
}
function drawPlot2() {
//make window
plot2.background(51);
plot2.stroke(255);
plot2.strokeWeight(1);
win.axis('horizontal', plot2);
win.axis('vertical', plot2);
//make labels
plot2.noStroke();
plot2.fill(255);
plot2.textSize(18);
plot2.textStyle(NORMAL);
plot2.textAlign(LEFT, BASELINE);
plot2.text("Population", 30, 30);
plot2.text("Time", plot2.width - 50, plot2.height - 20);
//draw population curves
plot2.strokeWeight(2);
if (!meanButton.toggled && !fitButton.toggled) {//mean & fit off
//draw population curves only, in bright color
plot2.stroke(255);
popCurves.draw(plot2);
}
else {
//draw population curves in dim color
plot2.stroke('rgba(255, 255, 255, 0.25)');
popCurves.draw(plot2);
//draw mean, fit if toggled on
plot2.strokeWeight(3);
if (meanButton.toggled) {
plot2.stroke(meanButton.fill);
meanPopCurve.draw(plot2);
}
if (fitButton.toggled) {
plot2.stroke(fitButton.fill);
//See https://ultimatecourses.com/blog/get-value-checked-radio-buttons
//for radioForm.elt.elements.model.value approach;
//I saw something like radioForm.elt.model.value on Stack Overflow
//but am not sure why it works
switch(radioForm.elt.model.value) {
case 'exponential':
drawFit(meanPopCurve.values, exponential, exponentialOptions);
break;
case 'logistic':
drawFit(meanPopCurve.values, logistic, logisticOptions);
break;
case 'gompertz':
drawFit(meanPopCurve.values, gompertz, gompertzOptions);
}
}
}
//show plot at given canvas position
imageMode(CORNER); //interpret coords as top-left of image
image(plot2, width / 2, 0);
}
function drawScene() {
drawPlot1(); //simulation
drawPlot2(); //graph
refreshButton.draw(); //draws nothing if hidden
meanButton.draw();
fitButton.draw();
}
function draw() {
if (population.count < grid.capacity) {
drawScene();
population.update(fps); //update population
popCurves.curvesArray[popIndex].points = population.history; //update curve
}
else {//at capacity
meanPopCurve = popCurves.meanCurve(); //update mean
refreshButton.hidden = false;
meanButton.hidden = false;
fitButton.hidden = false;
drawScene();
noLoop(); //stop iterating
}
}
/**** INTERACTIVITY ****/
function mouseMoved() {
let isOnRefresh = refreshButton.isOn(mouseX, mouseY);
let isOnMean = meanButton.isOn(mouseX, mouseY);
let isOnFit = fitButton.isOn(mouseX, mouseY);
let isOnButton = isOnRefresh || isOnMean || isOnFit;
if ((population.count === grid.capacity) && isOnButton) {
cursor(HAND);
}
else {
cursor(ARROW);
}
}
function mouseClicked() {
if (population.count === grid.capacity) {
//if clicked on refresh button
if (refreshButton.isOn(mouseX, mouseY)) {
//create new grid (old grid has no more vacancies)
grid = createGrid(width / 2, height, 4);
//create new population and its curve
population = createPopulation(initPopSize, grid);
popCurve = createCurve(population.history, win);
//include curve at end of array of population curves
popCurves.curvesArray.push(popCurve);
popIndex++; //increment population index
refreshButton.hidden = true;
meanButton.hidden = true;
meanButton.toggled = false;
fitButton.hidden = true;
fitButton.toggled = false;
loop(); //restart draw loop
}
//else if clicked on mean button
else if (meanButton.isOn(mouseX, mouseY)) {
meanButton.toggled = !meanButton.toggled; //toggle button state
loop(); //restart draw loop
}
//else if clicked on fit button
else if (fitButton.isOn(mouseX, mouseY)) {
fitButton.toggled = !fitButton.toggled; //toggle button state
radioForm.elt.hidden = !radioForm.elt.hidden; //show/hide radio form
sliders.elt.hidden = !sliders.elt.hidden; //show/hide sliders
loop(); //restart draw loop
}
}
}
//callback for radio selection
function showParams() {
let exponentialSliders = select('#exponential-sliders');
let logisticSliders = select('#logistic-sliders');
let gompertzSliders = select('#gompertz-sliders');
let model = radioForm.elt.model.value; //string value of selected model
let modelSliders = select('#' + model + '-sliders'); //container for model sliders
//hide all sliders
exponentialSliders.elt.hidden = true;
logisticSliders.elt.hidden = true;
gompertzSliders.elt.hidden = true;
//show model sliders and restart draw loop
modelSliders.elt.hidden = false;
loop();
}
//callback for slider adjustment
function updateParam() {
//update slider label
let label = select(`#${this.id()}-label`); //select label element
let labelContent = label.html(); //extract label content
label.html(`${labelContent[0]} = ${this.value()}`); //update label content
}