This commit is contained in:
Lucas Tadeu Marculino 2025-11-17 19:52:44 -03:00
commit fb20596fba
36 changed files with 8679 additions and 0 deletions

131
www/src/Objective.js Normal file
View file

@ -0,0 +1,131 @@
import { RK4, RK4getvalue } from "./runge-kutta.js";
export class Objective {
constructor(exper, f, res, mSize) {
/*
* res: a resolução utilizada para a resolução com RK4
* mSize: numero de variáveis do modelo atual
* exper: tabela de valores obtidos experimentalmente
*/
this.exper = exper;
this.sampleSize = exper.length;
// Tamanho da amostra obtida experimentalmente (número de pontos)
this.f = f;
// Função f(t) a ser utilizada para o ajuste
// Vetor de tempos para resolução numérica
let tf = exper[this.sampleSize - 1][0];
this.timeArray = [];
for (let i = 0; i <= res; i++) {
this.timeArray[i] = (i * tf) / res;
}
// valor inicial é o primeiro ponto experimental
this.y0 = [exper[1][2], exper[1][1]];
// Se o número de variaveis na tabela (descontando a coluna tempo)
// for maior que o numero de variáveis do modelo,
// a função objetivo é calculada com o número de variáveis do modelo.
// Caso contrário, ela é calculada com o numero de variáveis da tabela.
if (exper[1].length - 1 >= mSize) {
this.nVar = mSize;
} else {
this.nVar = exper[1].length - 1;
}
const headerRow = Array.isArray(exper[0]) ? exper[0] : [];
const normalizedHeaders = headerRow.map((value) => {
if (typeof value !== "string") {
return "";
}
return value
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase();
});
const dataColumns = exper[1] ? exper[1].length : 0;
const maxValidIndex = Math.max(1, dataColumns - 1);
const clampColumnIndex = (candidate) => {
if (!Number.isFinite(candidate) || candidate < 1 || candidate >= dataColumns) {
return Math.min(Math.max(1, candidate || 1), maxValidIndex);
}
return candidate;
};
const findColumnIndex = (keywords, fallback) => {
const idx = normalizedHeaders.findIndex((name, columnIndex) => {
if (columnIndex === 0) {
return false;
}
return keywords.some((keyword) => name.includes(keyword));
});
const candidate = idx > 0 ? idx : fallback;
return clampColumnIndex(candidate);
};
this.stateColumnIndex = [];
if (this.nVar >= 1) {
const fallbackCells = Math.min(2, maxValidIndex);
this.stateColumnIndex[0] = findColumnIndex(
["celula", "celulas", "celula", "cell", "cells", "biomassa", "biomass", "x"],
fallbackCells,
);
}
if (this.nVar >= 2) {
this.stateColumnIndex[1] = findColumnIndex([
"substrato",
"substrate",
"substrat",
"s",
], 1);
}
for (let j = 2; j < this.nVar; j++) {
this.stateColumnIndex[j] = clampColumnIndex(Math.min(j + 1, maxValidIndex));
}
// Média das colunas para normalização
this.mean = new Array(this.nVar).fill(0);
for (let i = 1; i < this.sampleSize; i++) {
for (let j = 0; j < this.nVar; j++) {
const columnIndex = this.stateColumnIndex[j];
if (columnIndex < exper[i].length) {
this.mean[j] += exper[i][columnIndex];
}
}
}
for (let j = 0; j < this.nVar; j++) {
this.mean[j] /= this.sampleSize - 1;
}
}
objective(params) {
// Resolve o modelo pelo método RK4
let sol = RK4(this.f, this.timeArray, this.y0, params);
// Pega predições pontuais do modelo
let pSol = [];
for (let i = 1; i < this.sampleSize; i++) {
pSol[i - 1] = RK4getvalue(
sol,
this.timeArray,
this.exper[i][0],
this.f,
params,
);
}
// Cálculo da função objetivo
let obj = 0;
for (let i = 2; i < this.sampleSize; i++) {
for (let j = 0; j < this.nVar; j++) {
const columnIndex = this.stateColumnIndex[j];
if (columnIndex >= this.exper[i].length) {
continue;
}
const observed = this.exper[i][columnIndex];
const predicted = pSol[i - 1][j];
const denom = this.mean[j] !== 0 ? this.mean[j] : 1;
obj += ((predicted - observed) / denom) ** 2;
}
}
// console.log(obj)
return obj;
}
}

93
www/src/PSO.js Normal file
View file

@ -0,0 +1,93 @@
import { rrandom } from "./rrandom.js";
export class PSO {
constructor(obj, n, bounds) {
/*
obj: função objetivo a ser minimizada
n: numero de iterações a serem performadas
bounds: vetor com os mínimos e maximos dos parametros da busca
*/
this.obj = obj;
this.n = n;
this.bounds = bounds;
this.pos = [];
this.vel = [];
this.err = [];
this.pos_best = [];
this.err_best = [];
this.dimensions = bounds.length; // número de dimensões da busca (parâmetros)
for (var i = 0; i < this.n; i++) {
/*
inicia o vetor vel com valores aleatórios entre -1 e 1
inicia o vetor pos com posições aleatórias dentro dos limites especificados
*/
var v = [];
var x = [];
for (var j = 0; j < this.dimensions; j++) {
v.push(rrandom(-1, 1));
x.push(rrandom(bounds[j][0], bounds[j][1]));
}
this.vel.push(v.slice());
this.pos.push(x.slice());
this.pos_best.push(x.slice()); // inicia a melhor posição como a posiçaõ atual (aleatória)
this.err_best.push(Infinity);
}
this.err_best_g = Infinity;
// inicia o melhor erro global como infinito (quanto menor melhor)
this.pos_best_g = this.pos[0].slice();
// inicia a melhor posição global como a posição da particula 0
}
run(c1, c2, w, iteration) {
for (let i = 0; i < iteration; i++) {
this.update(c1, c2, w);
// console.log(i, "/", iteration, "\t", this.err_best_g);
}
console.log(this.pos_best_g);
// console.log(this.err_best_g)
// console.table(this.pos)
}
update(c1, c2, w) {
for (let i = 0; i < this.n; i++) {
for (let j = 0; j < this.dimensions; j++) {
let r1 = rrandom(0, 1);
let r2 = rrandom(0, 1);
let vel_cognitive = c1 * r1 * (this.pos_best[i][j] - this.pos[i][j]);
let vel_social = c2 * r2 * (this.pos_best_g[j] - this.pos[i][j]);
const updatedVelocity = w * this.vel[i][j] + vel_cognitive + vel_social;
let candidatePosition = this.pos[i][j] + updatedVelocity;
if (candidatePosition > this.bounds[j][1]) {
candidatePosition = this.bounds[j][1];
this.vel[i][j] = 0;
} else if (candidatePosition < this.bounds[j][0]) {
candidatePosition = this.bounds[j][0];
this.vel[i][j] = 0;
} else {
this.vel[i][j] = updatedVelocity;
}
this.pos[i][j] = candidatePosition;
}
// Update error value
this.err[i] = this.obj.objective(this.pos[i]);
// Update global values
if (this.err[i] < this.err_best[i]) {
this.pos_best[i] = this.pos[i].slice();
this.err_best[i] = this.err[i];
if (this.err[i] < this.err_best_g) {
this.pos_best_g = this.pos[i].slice();
this.err_best_g = this.err[i];
}
}
}
}
}

49
www/src/conhecidos.js Normal file
View file

@ -0,0 +1,49 @@
// MONOD, 1942
export function monod(S, mu_max, K_S) {
return (mu_max * S) / (K_S + S);
}
// MOSER, 1958
export function moser(S, mu_max, K_S, n) {
return (mu_max * S ** n) / (K_S + S ** n);
}
// CONTOIS, 1959
export function contois(S, X, mu_max, K_S) {
return (mu_max * S) / (K_S * X + S);
}
// BERGTER, 1983
export function bergter(S, t, mu_max, K_S, T) {
return ((mu_max * S) / (K_S + S)) * (1 - Math.exp(-t / T));
}
// TESSIER, 1942
export function tessier(S, mu_max, K_S) {
return mu_max * (1 - Math.exp(-S / K_S));
}
// ANDREWS, 1968
export function andrews(S, mu_max, K_S, K_I) {
return (mu_max * S) / (K_S + S + (S ** 2) / K_I);
}
// AIBA; SHODA; NAGATANI, 1968
export function aiba(S, mu_max, K_S, K_I) {
return ((mu_max * S) / (K_S + S)) * Math.exp(-K_I * S);
}
// Morte celular
// SINCLAIR; KRISTIANSEN, 1987
export function death(K_d) {
return -K_d;
}
// Consumo do substrato limitante para manutenção
// PIRT, 1965
export function pirt(mu, Y_XS, m_S) {
return (1 / Y_XS) * mu + m_S;
}

13
www/src/consulta/model.js Normal file
View file

@ -0,0 +1,13 @@
function model(time, y, params) {
let K_S = params[0];
let mu_max = params[1];
let m_S = params[2];
let Y_XS = params[3];
let X = y[0];
let S = y[1];
let dmu = monod(S, mu_max, K_S);
let dX = X * dmu;
const qS = pirt(dmu, Y_XS, m_S);
let dS = -qS * X;
return [dX, dS];
}

170
www/src/consulta/modelos.js Normal file
View file

@ -0,0 +1,170 @@
// Crescimento num único substrato limitante:
// MONOD, 1942
class monod {
constructor() {
this.nPar = 2;
this.parName = ["mu_max", "K_S"]
this.varName = ["Substrato"]
this.varUsed = [1, 0, 0, 0, 0] // sub1, sub2, cel, prod, tempo
this.equation = "\\mu_{X} = \\frac{\\mu_{max} S}{Ks + S}"
this.name = "MONOD, 1942"
}
model(variavel, parametros) {
return (parametros[0] * variavel[0]) / (parametros[1] + variavel[0])
}
}
// MOSER, 1958
class moser {
constructor() {
this.nPar = 3;
this.parName = ["mu_max", "K_S", "n"]
this.varName = ["Substrato"]
this.varUsed = [1, 0, 0, 0, 0] // sub1, sub2, cel, prod, tempo
this.equation = "\\mu_{X} = \\frac{\\mu_{max} S^n}{Ks + S^n}"
this.name = "MOSER, 1958"
}
model(variavel, parametros) {
// moser(S, mu_max, K_S, n) {
return (parametros[0] * variavel[0] ** parametros[2]) / (parametros[1] + variavel[0] ** parametros[2])
}
}
// CONTOIS, 1959
class contois {
constructor() {
this.nPar = 2;
this.parName = ["mu_max", "K_S"]
this.varName = ["Substrato", "Células"]
this.varUsed = [1, 0, 1, 0, 0] // sub1, sub2, cel, prod, tempo
}
model(variavel, parametros) {
// model(S, X, mu_max, K_S) {
return (parametros[0] * variavel[0]) / (parametros[1] * variavel[1] + variavel[0])
}
}
// BERGTER, 1983
class bergter {
constructor() {
this.nPar = 3;
this.parName = ["mu_max", "K_S", "T"]
this.varName = ["Substrato", "tempo"]
this.varUsed = [1, 0, 0, 0, 1] // sub1, sub2, cel, prod, tempo
}
model(variavel, parametros) {
// (S, t, mu_max, K_S, T) {
return ((parametros[0] * variavel[0]) / (parametros[1] + variavel[0])) * (1 - Math.exp(-variavel[1] / parametros[2]))
}
}
// Morte celular
// SINCLAIR; KRISTIANSEN, 1987
class death {
constructor() {
this.nPar = 1;
this.parName = ["k_d"]
this.varName = []
this.varUsed = [0, 0, 0, 0, 0] // sub1, sub2, cel, prod, tempo
}
model(parametros) {
return -parametros[0]
}
}
// Crescimento em um único substrato limitante e inibidor
// AIBA; SHODA; NAGATANI; 1968
class asn {
constructor() {
this.nPar = 3
this.parName = ["mu_max", "K_S", "K_i"]
this.varName = ["Substrato"]
this.varUsed = [1, 0, 0, 0, 0] // sub1, sub2, cel, prod, tempo
}
model(variavel, parametros) {
return ((parametros[0] * variavel[0]) / (parametros[1] + variavel[0])) * Math.exp(-variavel[0] / parametros[2])
}
}
// HALDANE, 1930
class haldane {
constructor() {
this.nPar = 3
this.parName = ["mu_xa", "K_S", "K_i"]
this.varName = ["Substrato"]
this.varUsed = [1, 0, 0, 0, 0] // sub1, sub2, cel, prod, tempo
}
model(variavel, parametros) {
return (parametros[0] * variavel[0]) / (1 + parametros[1] / variavel[0] + variavel[0] / parametros[2])
}
}
// ANDREWS, 1968
class andrews {
constructor() {
this.nPar = 3
this.parName = ["mu_xa", "K_S", "K_i"]
this.varName = ["Substrato"]
this.varUsed = [1, 0, 0, 0, 0] // sub1, sub2, cel, prod, tempo
}
model(variavel, parametros) {
return (parametros[0] * variavel[0]) / (variavel[0] + parametros[1] + (variavel[0] ** 2 / parametros[2]))
}
}
// EDWARDS, 1970
class edwards {
constructor() {
this.nPar = 3
this.parName = ["mu_xa", "K_S", "K_i"]
this.varName = ["Substrato"]
this.varUsed = [1, 0, 0, 0, 0] // sub1, sub2, cel, prod, tempo
}
model(variavel, parametros) {
return (parametros[0] * variavel[0]) / (variavel[0] + parametros[1] + (variavel[0] ** 2 / parametros[2]) + (1 + variavel[0] / parametros[1]))
}
}
// WEBB, 1963
class webb {
constructor() {
this.nPar = 3
this.parName = ["mu_xa", "K_S", "K_i"]
this.varName = ["Substrato"]
this.varUsed = [1, 0, 0, 0, 0] // sub1, sub2, cel, prod, tempo
}
model(variavel, parametros) {
return (parametros[0] * variavel[0] * (1 + variavel[0] / parametros[2])) / (variavel[0] + parametros[1] + (variavel[0] ** 2 / parametros[2]))
}
}
// TEISSIER, 1942
class teissier {
constructor() {
this.nPar = 3
this.parName = ["mu_max", "K_S", "K_i"]
this.varName = ["Substrato"]
this.varUsed = [1, 0, 0, 0, 0] // sub1, sub2, cel, prod, tempo
}
model(variavel, parametros) {
return parametros[0] * (Math.exp(-variavel[0] / parametros[1]) - Math.exp(-variavel[0] / parametros[2]))
}
}
// WU et al., 1988
class wu {
constructor() {
this.nPar = 4
this.parName = ["mu_xa", "K_S", "K_i", "n"]
this.varName = ["Substrato"]
this.varUsed = [1, 0, 0, 0, 0] // sub1, sub2, cel, prod, tempo
}
model(variavel, parametros) {
// return mu_xa / (1 + K_S / S + (S / K_i) ** n)
return parametros[0] / (1 + parametros[1] / variavel[0] + (variavel[0] / parametros[2]) ** parametros[3])
}
}

69
www/src/jquery.jslatex.js Normal file
View file

@ -0,0 +1,69 @@
/*
* jsLaTeX v1.2 - jQuery plugin
*
* Copyright (c) 2009 Andreas Grech
*
* Dual licensed under the MIT and GPL licenses:
* http://www.opensource.org/licenses/mit-license.php
* http://www.gnu.org/licenses/gpl.html
*
* http://knowledge-aholic.blogspot.com
*/
(function ($) {
var attachToImage = function () {
return $("<img/>").attr({
src: this.src
});
},
formats = {
'gif': attachToImage,
'png': attachToImage,
'swf': function () {
return $("<embed/>").attr({
src: this.src,
type: 'application/x-shockwave-flash'
});
}
},
sections = {
'{f}': 'format',
'{e}': 'equation'
},
escapes = {
'+': '2B',
'=': '3D'
};
$.fn.latex = function (opts) {
opts = $.extend({},
$.fn.latex.defaults, opts);
opts.format = formats[opts.format] ? opts.format : 'gif';
return this.each(function () {
var $this = $(this),
format, s, element, url = opts.url;
opts.equation = $.trim($this.text());
for (s in sections) {
if (sections.hasOwnProperty(s) && (format = url.indexOf(s)) >= 0) {
url = url.replace(s, opts[sections[s]]);
}
}
for (s in escapes) {
if (escapes.hasOwnProperty(s) && (format = url.indexOf(s)) >= 0) {
url = url.replace(s, '%' + escapes[s]);
}
}
opts.src = url;
element = formats[opts.format].call(opts);
$this.html('').append(element);
if (opts.callback) {
opts.callback.call(element);
}
});
};
$.fn.latex.defaults = {
format: 'gif',
url: 'https://latex.codecogs.com/{f}.latex?{e}'
};
}(jQuery));

4
www/src/rrandom.js Normal file
View file

@ -0,0 +1,4 @@
export function rrandom(min, max) {
// cria um número aleatório dentro do intervalo
return Math.random() * (max - min) + min;
}

70
www/src/runge-kutta.js Normal file
View file

@ -0,0 +1,70 @@
export function RK4(f, t, Y0, params) {
/*
* f: trata-se da função f(t) ser integrada
* t: trata-se do vetor de pontos no tempo
* Y0: trata-se do vetor de condições iniciais
* params: paremetros passados para o modelo de crescimento
*/
let resolution = t.length; // resolução é o numero de pontos amostrados
let h = t[1]; // h é o tamanho do passo de integração
let y = []; // inicializa o vetor que será devolvido como eixo y
y[0] = Y0; //inicializa o vetor com as condições iniciais
for (let i = 0; i < resolution; i++) {
y[i + 1] = RK4step(f, t[i], y[i], h, params);
}
return y;
}
function RK4step(f, t, y0, h, params) {
// Algorítimo Runge-kutta 4
let s = [];
let y = [];
let nVar = y0.length;
const k1 = f(t, y0, params);
for (let i = 0; i < nVar; i++) {
s[i] = y0[i] + (k1[i] * h) / 2;
}
const k2 = f(t + h / 2, s, params);
for (let i = 0; i < nVar; i++) {
s[i] = y0[i] + (k2[i] * h) / 2;
}
const k3 = f(t + h / 2, s, params);
for (let i = 0; i < nVar; i++) {
s[i] = y0[i] + k3[i] * h;
}
const k4 = f(t + h, s, params);
for (let i = 0; i < nVar; i++) {
s[i] = y0[i] + k3[i] * h;
y[i] = y0[i] + (k1[i] / 6 + k2[i] / 3 + k3[i] / 3 + k4[i] / 6) * h;
}
return y;
}
export function RK4getvalue(sol, timearray, time, f, params) {
/* A partir de uma solução gerada pela função RK4,
* esta função permite pegar o valor de y para um ponto arbitrário de tempo,
* para tal, é pego o valor mais próximo do ponto desejado
* e calculado com um passo de Runge-kutta para o valor exato de tempo desejado
*/
let stepsize = timearray[1];
// o tamanho do passo é igual o ponto 1 do vetor tempo (ponto 0 é igual a 0)
let h = time % stepsize;
// o passo dado é igual ao modulo entre o tempo arbitrário e o passo na resolução original
let y0 = sol[Math.floor(time / stepsize)];
// inicia-se no ponto da curva imediatamente anterior ao ponto desejado
let y = RK4step(f, time, y0, h, params);
// calcula-se o y com um único passo RK4 a partir do ponto anterior
return y;
}

559
www/src/search.js Normal file
View file

@ -0,0 +1,559 @@
import { PSO } from "./PSO.js";
import { RK4, RK4getvalue } from "./runge-kutta.js";
import { Objective } from "./Objective.js";
import {
monod,
moser,
contois,
bergter,
tessier,
andrews,
aiba,
pirt,
} from "./conhecidos.js";
import "https://cdn.plot.ly/plotly-2.29.1.min.js";
const parameterCatalog = {
K_S: {
latex: "K_S",
unitText: "g/L",
unitLatex: "\\mathrm{g\\,L^{-1}}",
overrides: {
contois: {
unitText: "g_S/g_X",
unitLatex: "\\frac{\\mathrm{g}_{S}}{\\mathrm{g}_{X}}",
},
},
},
mu_max: {
latex: "\\mu_{max}",
unitText: "h⁻¹",
unitLatex: "\\mathrm{h^{-1}}",
},
K_I: {
latex: "K_I",
unitText: "g/L",
unitLatex: "\\mathrm{g\\,L^{-1}}",
overrides: {
aiba: {
unitText: "L/g",
unitLatex: "\\mathrm{L\\,g^{-1}}",
},
},
},
m_S: {
latex: "m_S",
unitText: "g_S/(g_X·h)",
unitLatex: "\\frac{\\mathrm{g}_{S}}{\\mathrm{g}_{X}\\,\\mathrm{h}}",
},
Y_XS: {
latex: "Y_{XS}",
unitText: "g_X/g_S",
unitLatex: "\\frac{\\mathrm{g}_{X}}{\\mathrm{g}_{S}}",
},
T: {
latex: "T",
unitText: "h",
unitLatex: "\\mathrm{h}",
},
n: {
latex: "n",
unitText: null,
unitLatex: null,
},
};
const modelParameters = {
aiba: [
{ key: "K_S", bounds: [0.005, 2] },
{ key: "mu_max", bounds: [0.05, 0.9] },
{ key: "K_I", bounds: [0.01, 1] },
{ key: "m_S", bounds: [0.0015, 0.05] },
{ key: "Y_XS", bounds: [0.3, 0.7] },
],
andrews: [
{ key: "K_S", bounds: [0.005, 2] },
{ key: "mu_max", bounds: [0.05, 0.9] },
{ key: "K_I", bounds: [5, 150] },
{ key: "m_S", bounds: [0.0015, 0.05] },
{ key: "Y_XS", bounds: [0.3, 0.7] },
],
bergter: [
{ key: "K_S", bounds: [0.005, 2] },
{ key: "mu_max", bounds: [0.05, 0.9] },
{ key: "T", bounds: [5, 80] },
{ key: "m_S", bounds: [0.0015, 0.05] },
{ key: "Y_XS", bounds: [0.3, 0.7] },
],
contois: [
{ key: "K_S", bounds: [0.005, 2] },
{ key: "mu_max", bounds: [0.05, 0.9] },
{ key: "m_S", bounds: [0.0015, 0.05] },
{ key: "Y_XS", bounds: [0.3, 0.7] },
],
monod: [
{ key: "K_S", bounds: [0.005, 2] },
{ key: "mu_max", bounds: [0.05, 0.9] },
{ key: "m_S", bounds: [0.0015, 0.05] },
{ key: "Y_XS", bounds: [0.3, 0.7] },
],
moser: [
{ key: "K_S", bounds: [0.005, 2] },
{ key: "mu_max", bounds: [0.05, 0.9] },
{ key: "n", bounds: [0.8, 2.5] },
{ key: "m_S", bounds: [0.0015, 0.05] },
{ key: "Y_XS", bounds: [0.3, 0.7] },
],
tessier: [
{ key: "K_S", bounds: [0.005, 2] },
{ key: "mu_max", bounds: [0.2, 0.9] },
{ key: "m_S", bounds: [0.005, 0.05] },
{ key: "Y_XS", bounds: [0.3, 0.7] },
],
};
function getParamDisplayInfo(paramKey, modelKey) {
const baseInfo = parameterCatalog[paramKey];
if (!baseInfo) {
throw new Error(`Unknown parameter: ${paramKey}`);
}
const override = baseInfo.overrides?.[modelKey];
if (!override) {
return baseInfo;
}
return { ...baseInfo, ...override };
}
function getDefaultBounds(modelKey) {
return modelParameters[modelKey].map((param) => param.bounds.slice());
}
function getParamDetails(modelKey) {
return modelParameters[modelKey].map((param) => {
const { latex, unitText, unitLatex } = getParamDisplayInfo(
param.key,
modelKey,
);
return {
key: param.key,
latex,
unitText,
unitLatex,
};
});
}
function monodPirt(_, y, params) {
let K_S = params[0];
let mu_max = params[1];
//pirt:
let m_S = params[2];
let Y_XS = params[3];
let X = y[0];
let S = y[1];
let dmu = monod(S, mu_max, K_S);
let dX = X * dmu;
const qS = pirt(dmu, Y_XS, m_S);
let dS = -qS * X;
return [dX, dS];
}
function moserPirt(_, y, params) {
let K_S = params[0];
let mu_max = params[1];
let n = params[2];
//pirt:
let m_S = params[3];
let Y_XS = params[4];
let X = y[0];
let S = y[1];
let dmu = moser(S, mu_max, K_S, n);
let dX = X * dmu;
const qS = pirt(dmu, Y_XS, m_S);
let dS = -qS * X;
return [dX, dS];
}
function contoisPirt(_, y, params) {
let K_S = params[0];
let mu_max = params[1];
let m_S = params[2];
let Y_XS = params[3];
let X = y[0];
let S = y[1];
let dmu = contois(S, X, mu_max, K_S);
let dX = X * dmu;
const qS = pirt(dmu, Y_XS, m_S);
let dS = -qS * X;
return [dX, dS];
}
function bergterPirt(time, y, params) {
let K_S = params[0];
let mu_max = params[1];
let T = params[2];
// pirt:
let m_S = params[3];
let Y_XS = params[4];
let X = y[0];
let S = y[1];
let dmu = bergter(S, time, mu_max, K_S, T);
let dX = X * dmu;
const qS = pirt(dmu, Y_XS, m_S);
let dS = -qS * X;
return [dX, dS];
}
function tessierPirt(_, y, params) {
let K_S = params[0];
let mu_max = params[1];
// pirt:
let m_S = params[2];
let Y_XS = params[3];
let X = y[0];
let S = y[1];
let dmu = tessier(S, mu_max, K_S);
let dX = X * dmu;
const qS = pirt(dmu, Y_XS, m_S);
let dS = -qS * X;
return [dX, dS];
}
function andrewsPirt(_, y, params) {
let K_S = params[0];
let mu_max = params[1];
let K_I = params[2];
// pirt:
let m_S = params[3];
let Y_XS = params[4];
let X = y[0];
let S = y[1];
let dmu = andrews(S, mu_max, K_S, K_I);
let dX = X * dmu;
const qS = pirt(dmu, Y_XS, m_S);
let dS = -qS * X;
return [dX, dS];
}
function aibaPirt(_, y, params) {
let K_S = params[0];
let mu_max = params[1];
let K_I = params[2];
// pirt:
let m_S = params[3];
let Y_XS = params[4];
let X = y[0];
let S = y[1];
let dmu = aiba(S, mu_max, K_S, K_I);
let dX = X * dmu;
const qS = pirt(dmu, Y_XS, m_S);
let dS = -qS * X;
return [dX, dS];
}
export async function main(input, options = {}) {
const alg = options.alg || {
particles: 50,
c1: 1.49618,
c2: 1.49618,
w: 0.7298,
iterations: 150,
};
const boundsOpt = options.bounds || {};
const onProgress = options.onProgress || (() => {});
const time = [];
const subs = [];
const cels = [];
for (let i = 1; i < input.length; i++) {
time[i] = input[i][0];
subs[i] = input[i][1];
cels[i] = input[i][2];
}
const basePlot = [
{
x: time,
y: subs,
name: "Experimental substrate",
mode: "markers",
marker: { size: 10, color: "#4a90e2" },
type: "scatter",
},
{
x: time,
y: cels,
name: "Experimental cells",
mode: "markers",
marker: { size: 10, color: "#50e3c2" },
type: "scatter",
},
];
function calculateAIC(obj, sol, params) {
let sse = 0;
let count = 0;
for (let i = 1; i < input.length; i++) {
const timePoint = input[i][0];
const prediction = RK4getvalue(
sol,
obj.timeArray,
timePoint,
obj.f,
params,
);
const cellResidual = prediction[0] - input[i][2];
const substrateResidual = prediction[1] - input[i][1];
sse += cellResidual ** 2 + substrateResidual ** 2;
count += 2;
}
const n = count;
const k = params.length;
const meanSquaredError = n > 0 ? sse / n : 0;
const safeMSE = Math.max(meanSquaredError, Number.EPSILON);
return {
aic: 2 * k + n * Math.log(safeMSE),
mse: meanSquaredError,
};
}
function runModel(cfg) {
const obj = new Objective(input, cfg.ode, 500, 2);
const optim = new PSO(obj, alg.particles, cfg.bounds);
optim.run(alg.c1, alg.c2, alg.w, alg.iterations);
const sol = RK4(obj.f, obj.timeArray, obj.y0, optim.pos_best_g);
const celsM = sol.map((row) => row[0]);
const subsM = sol.map((row) => row[1]);
const { aic, mse } = calculateAIC(obj, sol, optim.pos_best_g);
let objectiveText = "MSE: N/A";
if (Number.isFinite(mse)) {
const absValue = Math.abs(mse);
const formatted =
absValue !== 0 && (absValue >= 1e3 || absValue < 1e-2)
? mse.toExponential(4)
: mse.toFixed(8);
objectiveText = `MSE: ${formatted}`;
}
Plotly.newPlot(
document.getElementById(cfg.plotId),
[
...basePlot,
{
x: obj.timeArray,
y: subsM,
name: "Model fit for the substrate",
mode: "lines",
line: { color: "#4a90e2" },
},
{
x: obj.timeArray,
y: celsM,
name: "Model fit for the cells",
mode: "lines",
line: { color: "#50e3c2" },
},
],
{
margin: { t: 10, b: 30 },
paper_bgcolor: "#f0f4f8",
plot_bgcolor: "#f0f4f8",
xaxis: {
title: { text: "Time (h)" },
},
yaxis: {
title: { text: "Concentration (g/L)" },
},
legend: {
orientation: "h",
yanchor: "top",
y: -0.2,
xanchor: "left",
x: 0,
font: { size: 10 },
},
annotations: [
{
text: objectiveText,
x: 1,
y: 1,
xref: "paper",
yref: "paper",
xanchor: "right",
yanchor: "top",
showarrow: false,
font: { size: 12, color: "#1f2933" },
bordercolor: "#d9e2ef",
borderwidth: 1,
borderpad: 6,
},
],
},
);
const params = optim.pos_best_g.map((p) => p.toFixed(3));
const paramContainer = document.getElementById(cfg.paramDiv);
paramContainer.classList.add("model-params");
paramContainer.innerHTML = "";
const label = document.createElement("div");
label.className = "param-label";
label.textContent = "Parameters:";
paramContainer.appendChild(label);
const list = document.createElement("ul");
list.className = "param-list";
params.forEach((value, i) => {
const detail = cfg.paramDetails[i];
const item = document.createElement("li");
const mathSpan = document.createElement("span");
const latexUnit = detail.unitLatex;
const latexExpression = latexUnit
? `${detail.latex} = ${value}\\,${latexUnit}`
: `${detail.latex} = ${value}`;
if (typeof katex !== "undefined") {
katex.render(latexExpression, mathSpan, { throwOnError: false });
} else {
mathSpan.textContent = latexUnit
? `${detail.latex} = ${value} ${detail.unitText || ""}`
: `${detail.latex} = ${value}`;
}
item.appendChild(mathSpan);
list.appendChild(item);
});
paramContainer.appendChild(list);
return { title: cfg.title, aic, key: cfg.key };
}
const models = [
{
key: "aiba",
ode: aibaPirt,
bounds: boundsOpt.aiba || getDefaultBounds("aiba"),
plotId: "Plot1",
paramDiv: "AibaParam",
title: "Pirt-Aiba",
paramDetails: getParamDetails("aiba"),
container: document.getElementById("Plot1")?.closest(".box"),
},
{
key: "andrews",
ode: andrewsPirt,
bounds: boundsOpt.andrews || getDefaultBounds("andrews"),
plotId: "Plot2",
paramDiv: "AndrewsParam",
title: "Pirt-Andrews",
paramDetails: getParamDetails("andrews"),
container: document.getElementById("Plot2")?.closest(".box"),
},
{
key: "bergter",
ode: bergterPirt,
bounds: boundsOpt.bergter || getDefaultBounds("bergter"),
plotId: "Plot3",
paramDiv: "BergterParam",
title: "Pirt-Bergter",
paramDetails: getParamDetails("bergter"),
container: document.getElementById("Plot3")?.closest(".box"),
},
{
key: "contois",
ode: contoisPirt,
bounds: boundsOpt.contois || getDefaultBounds("contois"),
plotId: "Plot4",
paramDiv: "ContoisParam",
title: "Pirt-Contois",
paramDetails: getParamDetails("contois"),
container: document.getElementById("Plot4")?.closest(".box"),
},
{
key: "monod",
ode: monodPirt,
bounds: boundsOpt.monod || getDefaultBounds("monod"),
plotId: "Plot5",
paramDiv: "MonodParam",
title: "Pirt-Monod",
paramDetails: getParamDetails("monod"),
container: document.getElementById("Plot5")?.closest(".box"),
},
{
key: "moser",
ode: moserPirt,
bounds: boundsOpt.moser || getDefaultBounds("moser"),
plotId: "Plot6",
paramDiv: "MoserParam",
title: "Pirt-Moser",
paramDetails: getParamDetails("moser"),
container: document.getElementById("Plot6")?.closest(".box"),
},
{
key: "tessier",
ode: tessierPirt,
bounds: boundsOpt.tessier || getDefaultBounds("tessier"),
plotId: "Plot7",
paramDiv: "TessierParam",
title: "Pirt-Tessier",
paramDetails: getParamDetails("tessier"),
container: document.getElementById("Plot7")?.closest(".box"),
},
];
const modelByKey = new Map(models.map((model) => [model.key, model]));
const results = [];
for (let idx = 0; idx < models.length; idx++) {
const cfg = models[idx];
const res = runModel(cfg);
results.push(res);
onProgress(idx + 1, models.length);
await new Promise((resolve) => setTimeout(resolve, 0));
}
results.sort((a, b) => a.aic - b.aic);
const comparisonBox = document.getElementById("comparison");
const chartParent = models[0]?.container?.parentElement;
if (comparisonBox && chartParent) {
results.forEach((result) => {
const modelCfg = modelByKey.get(result.key);
if (!modelCfg?.container) {
return;
}
chartParent.insertBefore(modelCfg.container, comparisonBox);
});
}
const compDiv = document.getElementById("comparison");
compDiv.innerHTML =
`<h2>Model comparison</h2>
<table class="abnt-table table-numbered">
<thead>
<tr>
<th>#</th>
<th>Model</th>
<th>Akaike Information Criterion</th>
</tr>
</thead>
<tbody>` +
results
.map((r, index) => {
return `<tr><td>${index + 1}</td><td>${r.title}</td><td>${r.aic.toFixed(
2,
)}</td></tr>`;
})
.join("") +
"</tbody></table>";
}

45
www/src/solve.js Normal file
View file

@ -0,0 +1,45 @@
function solve(tf, Ks, mu_max, m_S, Y_XS, X0, S0) {
let res = 5000;
let timeArray = [];
for (let i = 0; i <= res; i++) {
timeArray[i] = (i * tf) / res;
}
let sol = RK4(model, timeArray, [X0, S0], [Ks, mu_max, m_S, Y_XS]);
let cels = [];
let subs = [];
for (let i = 0; i < sol.length; i++) {
cels[i] = sol[i][0];
subs[i] = sol[i][1];
}
TESTER = document.getElementById("tester");
Plotly.newPlot(
TESTER,
[
{
x: timeArray,
y: subs,
name: "Calculated substrate",
line: { color: "#4a90e2" },
},
{
x: timeArray,
y: cels,
name: "Calculated cells",
line: { color: "#50e3c2" },
},
],
{
margin: { t: 10, b: 30 },
paper_bgcolor: "#f0f4f8",
plot_bgcolor: "#f0f4f8",
legend: {
orientation: "h",
yanchor: "top",
y: -0.2,
font: { size: 10 },
},
},
);
}