diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0daec5c --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +.DS_Store + +# Generated by package manager +node_modules/ + +# Generated by Cordova +/plugins/ +/platforms/ + +.DS_Store +.log/ +.idea +tmp/ +*.tern-port +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +*.tsbuildinfo +.npm +.eslintcache +.auctex*/ +package-lock.json +*.aux +*.log +*.toc +*.out +*.synctex.gz +*.fdb_latexmk +*.fls +*.ipynb_checkpoints* \ No newline at end of file diff --git a/config.xml b/config.xml new file mode 100644 index 0000000..e31442d --- /dev/null +++ b/config.xml @@ -0,0 +1,11 @@ + + + BioLab + BioLab Parameter Explorer + + Apache Cordova Team + + + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..febfa77 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "com.tadeu.biolab", + "displayName": "BioLab", + "version": "1.0.0", + "description": "A sample Apache Cordova application for BioLab.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Lucas Tadeu Marculino", + "license": "MIT", + "devDependencies": { + "cordova-electron": "^4.0.0", + "electron": "^39.2.1", + "electron-builder": "^26.0.12" + }, + "cordova": { + "platforms": [ + "electron" + ] + }, + "win": { + "target": [ + "nsis" + ] + } +} diff --git a/www/assets/Variac.svg b/www/assets/Variac.svg new file mode 100644 index 0000000..33c2f43 --- /dev/null +++ b/www/assets/Variac.svg @@ -0,0 +1,435 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/www/assets/dados.json b/www/assets/dados.json new file mode 100644 index 0000000..1961a33 --- /dev/null +++ b/www/assets/dados.json @@ -0,0 +1,62 @@ +[ + { + "tempo_h":0.0, + "substrato_S_gL":3.0, + "celulas_X_gL":0.05 + }, + { + "tempo_h":2.0, + "substrato_S_gL":2.9589957891, + "celulas_X_gL":0.1578767505 + }, + { + "tempo_h":4.0, + "substrato_S_gL":2.9641782139, + "celulas_X_gL":0.1276852512 + }, + { + "tempo_h":6.0, + "substrato_S_gL":2.8651038957, + "celulas_X_gL":0.1231016961 + }, + { + "tempo_h":8.0, + "substrato_S_gL":2.7382748156, + "celulas_X_gL":0.2362213241 + }, + { + "tempo_h":10.0, + "substrato_S_gL":2.6079841083, + "celulas_X_gL":0.1876996896 + }, + { + "tempo_h":12.0, + "substrato_S_gL":2.4234159844, + "celulas_X_gL":0.4493149032 + }, + { + "tempo_h":14.0, + "substrato_S_gL":2.1069816449, + "celulas_X_gL":0.5927647867 + }, + { + "tempo_h":16.0, + "substrato_S_gL":1.6667082549, + "celulas_X_gL":0.7867908379 + }, + { + "tempo_h":18.0, + "substrato_S_gL":1.1347389178, + "celulas_X_gL":1.1979622182 + }, + { + "tempo_h":20.0, + "substrato_S_gL":0.4681884477, + "celulas_X_gL":1.5379116343 + }, + { + "tempo_h":22.0, + "substrato_S_gL":0.0321180338, + "celulas_X_gL":1.8565788779 + } +] \ No newline at end of file diff --git a/www/assets/equations/Aiba.svg b/www/assets/equations/Aiba.svg new file mode 100644 index 0000000..89a454f --- /dev/null +++ b/www/assets/equations/Aiba.svg @@ -0,0 +1,579 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/www/assets/equations/Andrews.svg b/www/assets/equations/Andrews.svg new file mode 100644 index 0000000..8691b0d --- /dev/null +++ b/www/assets/equations/Andrews.svg @@ -0,0 +1,740 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/www/assets/equations/Bergter.svg b/www/assets/equations/Bergter.svg new file mode 100644 index 0000000..136c400 --- /dev/null +++ b/www/assets/equations/Bergter.svg @@ -0,0 +1,510 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/www/assets/equations/Contois.svg b/www/assets/equations/Contois.svg new file mode 100644 index 0000000..5c60c11 --- /dev/null +++ b/www/assets/equations/Contois.svg @@ -0,0 +1,351 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/www/assets/equations/Monod.svg b/www/assets/equations/Monod.svg new file mode 100644 index 0000000..451b449 --- /dev/null +++ b/www/assets/equations/Monod.svg @@ -0,0 +1,310 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/www/assets/equations/Moser.svg b/www/assets/equations/Moser.svg new file mode 100644 index 0000000..caba053 --- /dev/null +++ b/www/assets/equations/Moser.svg @@ -0,0 +1,355 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/www/assets/equations/Pirt.svg b/www/assets/equations/Pirt.svg new file mode 100644 index 0000000..348d795 --- /dev/null +++ b/www/assets/equations/Pirt.svg @@ -0,0 +1,306 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/www/assets/equations/Tessier.svg b/www/assets/equations/Tessier.svg new file mode 100644 index 0000000..4d15279 --- /dev/null +++ b/www/assets/equations/Tessier.svg @@ -0,0 +1,1039 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/www/assets/equations/eq.pdf b/www/assets/equations/eq.pdf new file mode 100644 index 0000000..9c88fd0 Binary files /dev/null and b/www/assets/equations/eq.pdf differ diff --git a/www/assets/equations/eq.svg b/www/assets/equations/eq.svg new file mode 100644 index 0000000..4555ef8 --- /dev/null +++ b/www/assets/equations/eq.svg @@ -0,0 +1,1101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/www/assets/equations/eq.tex b/www/assets/equations/eq.tex new file mode 100644 index 0000000..6eecaf6 --- /dev/null +++ b/www/assets/equations/eq.tex @@ -0,0 +1,37 @@ +\documentclass{article} +\usepackage{amsmath} +\usepackage{amssymb} +\usepackage{amsfonts} + +\begin{document} + + +pirt: +\[ q_{S} = \frac{\mu}{Y_{X/S}} + m_{S} \] + +monod: +\[ \mu(S) = \frac{\mu_{\max} S}{K_{S} + S} \] + +moser: +\[ \mu(S) = \frac{\mu_{\max} S^n}{K_{S}^n + S^n} \] + +contois: +\[ \mu(S,X) = \frac{\mu_{\max} S}{K_{S} X + S} \] + +bergter: +\[ \mu(S,t) = \frac{\mu_{\max} S}{K_{S} + S} \left(1 - e^{-t/T}\right) \] + +Aiba: +\[ \mu(S) = \mu_{\max} \frac{S}{K_S + S} e^{-K_I S} \] + +Andrews: +\[ \mu(S) = \mu_{\max} \frac{S}{K_S + S + \frac{S^2}{K_I}} \] + +Tessier: +\[ \mu(S) = \mu_{\max} \left(1 - e^{-S/K_S}\right) \] + +Equações de Massas: + +\[ \frac{dX}{dt} = \mu X \] +\[ \frac{dS}{dt} = -q_{S} X \] +\[ \frac{dP}{dt} = \mu_{P} X \] diff --git a/www/assets/exemplo.ods b/www/assets/exemplo.ods new file mode 100644 index 0000000..b1d8a97 Binary files /dev/null and b/www/assets/exemplo.ods differ diff --git a/www/assets/favicon.svg b/www/assets/favicon.svg new file mode 100644 index 0000000..491b92b --- /dev/null +++ b/www/assets/favicon.svg @@ -0,0 +1,61 @@ + +erlenmeyer_flaskerlenmeyer_flask diff --git a/www/assets/gerarDados.ipynb b/www/assets/gerarDados.ipynb new file mode 100644 index 0000000..c9157cd --- /dev/null +++ b/www/assets/gerarDados.ipynb @@ -0,0 +1,335 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "66755e2e-daeb-4e9c-bedf-e72787095e54", + "metadata": {}, + "source": [ + "# Origem dos parâmetros utilizados\n", + "\n", + "## Rebnegger et al. (2016), *Pichia pastoris* em glicose\n", + "- **Parâmetros de Pirt**: $Y_{X/S,\\max}=0.584\\ \\text{gX/gS},\\ m_S=0.0031\\ \\text{gS/gX/h}$ vêm diretamente do artigo de **Rebnegger et al. (2016, *Applied and Environmental Microbiology*)**, estimados em retentostat e chemostato. \n", + "\n", + "- **Crescimento (Monod)**: $\\mu_{\\max}=0.18\\ \\text{h}^{-1},\\ K_S=0.11\\ \\text{g/L}$ foram adotados como **valores plausíveis para leveduras em glicose**, conservadores em relação a máximos (~0.2–0.3 h⁻¹). \n", + "\n", + "- **Condições iniciais**: $X_0=0.05\\ \\text{g/L},\\ S_0=3.0\\ \\text{g/L}$ simulam uma batelada com glicose abundante, típica de ensaios laboratoriais.\n", + " \n", + "\n", + "🔗 Artigo: [Rebnegger et al., 2016 — *Applied and Environmental Microbiology*](https://doi.org/10.1128/AEM.00638-16)\n", + "\n", + "---\n", + "\n", + "**Resumo**: cada conjunto de parâmetros combina **valores reportados nos artigos** com **ajustes plausíveis baseados na literatura**, garantindo perfis de crescimento e consumo biologicamente realistas.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "25799689-cad4-4442-8ae8-717481ee5606", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "import numpy as np\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "from scipy.integrate import solve_ivp\n", + "from IPython.display import display\n", + "\n", + "# =========================\n", + "# Modelos\n", + "# =========================\n", + "def mu_haldane(S, mu_max, Ks, Ki):\n", + " S = np.clip(np.asarray(S), 0, None)\n", + " return mu_max * S / (Ks + S + (S**2)/Ki)\n", + "\n", + "def mu_monod(S, mu_max, Ks):\n", + " S = np.clip(np.asarray(S), 0, None)\n", + " return mu_max * S / (Ks + S)\n", + "\n", + "def qS_pirt(mu, Yxs, mS):\n", + " return mu / Yxs + mS\n", + "\n", + "def rhs_batch(t, y, mu_fun, mu_params, Yxs, mS):\n", + " X, S = y\n", + " S = max(S, 0.0)\n", + " mu = mu_fun(S, **mu_params)\n", + " dXdt = mu * X\n", + " dSdt = -qS_pirt(mu, Yxs, mS) * X\n", + " return [dXdt, dSdt]\n", + "\n", + "# =========================\n", + "\n", + "t_span = (0.0, 22.0)\n", + "t_eval = np.linspace(*t_span, 600)\n", + "\n", + "reb_params_mu = dict(mu_max=0.18, Ks=0.11) # h^-1, g/L\n", + "reb_Yxs = 0.584\n", + "reb_mS = 0.0031\n", + "X0_reb, S0_reb = 0.05, 3.00 # g/L\n", + "\n", + "sol_reb = solve_ivp(\n", + " rhs_batch, t_span, [X0_reb, S0_reb],\n", + " args=(lambda S, **p: mu_monod(S, **p), reb_params_mu, reb_Yxs, reb_mS),\n", + " t_eval=t_eval, rtol=1e-7, atol=1e-9\n", + ")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "8981bf23-87ed-4156-ba33-8e473abac249", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Rebnegger 2016 — Monod + Pirttempo_hsubstrato_S_gLcelulas_X_gL
00.0000003.00000.0500
12.0000002.96410.0708
24.0000002.91330.1001
36.0000002.84150.1416
48.0000002.74010.2002
\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def tabela_por_hora(sol, titulo, t_max=22):\n", + " # tempos inteiros dentro do intervalo da simulação\n", + " t_horas = np.arange(0, min(t_max, sol.t[-1]) + 1, 2.0)\n", + "\n", + " # interpolação sobre o grid t_eval já calculado\n", + " X_interp = np.interp(t_horas, sol.t, sol.y[0])\n", + " S_interp = np.interp(t_horas, sol.t, sol.y[1])\n", + "\n", + " df = pd.DataFrame({\n", + " \"tempo_h\": t_horas,\n", + " \"substrato_S_gL\": S_interp,\n", + " \"celulas_X_gL\": X_interp,\n", + " })\n", + " df = df[[\"tempo_h\", \"substrato_S_gL\", \"celulas_X_gL\"]].copy()\n", + " df.columns.name = titulo\n", + " return df\n", + "\n", + "df_reb = tabela_por_hora(sol_reb, \"Rebnegger 2016 — Monod + Pirt\", t_max=22)\n", + "\n", + "display(df_reb.head().style.format({\"substrato_S_gL\": \"{:.4f}\", \"celulas_X_gL\": \"{:.4f}\"}))\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "f46812ea-e972-4906-bf76-8891cf2405e5", + "metadata": {}, + "outputs": [], + "source": [ + "# Parâmetros do ruído\n", + "mean = 0.0 # média do ruído\n", + "std_X = 0.05 # desvio padrão para X\n", + "std_S = 0.05 # desvio padrão para S\n", + "\n", + "# Copiar o DataFrame para não sobrescrever o original\n", + "df_noisy = df_reb.copy()\n", + "\n", + "# Adicionar ruído gaussiano às colunas de interesse\n", + "idx = df_reb.index[1:]\n", + "\n", + "df_noisy.loc[idx, \"celulas_X_gL\"] = df_reb.loc[idx, \"celulas_X_gL\"] + np.random.normal(mean, std_X, size=len(idx))\n", + "df_noisy.loc[idx, \"substrato_S_gL\"] = df_reb.loc[idx, \"substrato_S_gL\"] + np.random.normal(mean, std_S, size=len(idx))\n", + "\n", + "df_noisy = df_noisy.clip(lower=0)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "311fda7a-5189-4549-8735-730e6609ac32", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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kv3793ves2DBAoYMGcKSJUtITk4mPDyc66+/3usWo8vlYtGiRXTv3h273c6QIUNYsWLFKX+WDz/8kODgYEaOHNlo26ZNm0hLSyM8PJxRo0Z5BU+AyZMn8/bbb1NeXt7G36SI+DOFMhEJaB999BE33ngjc+fOZdeuXSxZsoTly5fz0EMPee33wAMPcPPNN7N161b69u3LtGnTuO2225g3bx4bN24E4I477vB6z759+3jzzTd57733WLFiBVu3buX222/3bH/yySd57LHHePTRR9m2bRvjx49n8uTJ7N27t9l616xZQ1paWpPb7rvvPh577DE2btxIcHAwM2bM8NqelpZGdXU169evb9XvSEQChCEiEkCWLVtmOBwOz/KYMWOMxYsXe+3zt7/9zUhMTPQsA8bvf/97z/K6desMwFi6dKln3euvv26EhoZ6lufPn28EBQUZBw8e9Kz78MMPDavVauTk5BiGYRhJSUnGQw895PXZI0aMMObMmdNs/ddcc40xY8YMr3WrVq0yAOOTTz7xrPvggw8MwCgvL/fat3Pnzsby5cubPb6IBC71KRORgLZp0yY2bNjgdWWstraWiooKysrKCA8PB+DCCy/0bI+Pjwdg0KBBXusqKipwOp1ER0cDkJKSQvfu3T37jBw5EpfLxZ49ewgPD+fw4cOMHj3aq57Ro0fz9ddfN1tveXk5oaGhTW47ucbExEQA8vLySElJ8awPCwujrKys2eOLSOBSKBORgOZyuVi4cCFTpkxptO3k8GOz2TyvLRZLs+tcLlezn1W/T/1zw9cAhmE0Wney2NhYCgsLm9zWknoKCgro2rVrs8cXkcClPmUiEtCGDRvGnj176NWrV6OH1Xpmf+Kys7M5fPiwZ3ndunVYrVb69OlDdHQ0SUlJjYaoWLt2Lf369Wv2mEOHDmXXrl1tqmf//v1UVFQwdOjQNr1fRPybrpSJSEC7//77ufrqq0lOTub666/HarWybds2tm/fzoMPPnhGxw4NDWX69Ok8+uijOJ1O5s6dyw033EBCQgIAv/vd75g/fz49e/ZkyJAhLFu2jK1bt/Lqq682e8zx48czb948CgsL6dy5c6vq+fzzzzn//PPp2bPnGf1cIuKfFMpEJKCNHz+e999/n0WLFvHII49gs9no27cvM2fOPONj9+rViylTpjBx4kQKCgqYOHEizz77rGf73LlzcTqd/Pa3vyUvL4/+/fvz7rvv0rt372aPOWjQINLS0njzzTe57bbbWlXP66+/zqxZs9r884iIf7MYhmGYXYSIiL9ZsGABb7/9Nlu3bm33Y2dmZnL33XezY8eOFt9i3bFjB1dccQXffvstDoej3WsSEfPpSpmISAebOHEie/fu5dChQyQnJ7foPYcPH+bll19WIBM5iymUiYiY4De/+U2r9k9PT/dRJSLiL3T7UkRERMQPaEgMERERET+gUCYiIiLiBxTKRERERPyAQpmIiIiIH1AoExEREfEDCmUiIiIifkChTERERMQPKJSJiIiI+IH/Axhc3chE4Y1TAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.figure(figsize=(7,5))\n", + "plt.scatter(df_noisy[\"tempo_h\"], df_noisy[\"celulas_X_gL\"], label=\"X ruidoso\")\n", + "plt.plot(sol_reb.t, sol_reb.y[0], lw=2, label=\"X modelado\")\n", + "\n", + "plt.scatter(df_noisy[\"tempo_h\"], df_noisy[\"substrato_S_gL\"], label=\"S Ruidoso\")\n", + "plt.plot(sol_reb.t, sol_reb.y[1], lw=2, label=\"S modelado\")\n", + "\n", + "plt.xlabel(\"Tempo (h)\")\n", + "plt.ylabel(\"Concentração (g/L)\")\n", + "plt.title(\"Curvas modeladas e dados com ruido arificial\")\n", + "plt.grid(True); plt.legend(); plt.show()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "75dbf5b1-b6cd-47f0-8a19-edf03311649d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Rebnegger 2016 — Monod + Pirttempo_hsubstrato_S_gLcelulas_X_gL
00.03.0000000.050000
12.03.1214290.001534
24.02.9714750.108785
36.02.9046320.186982
48.02.8132560.225855
\n", + "
" + ], + "text/plain": [ + "Rebnegger 2016 — Monod + Pirt tempo_h substrato_S_gL celulas_X_gL\n", + "0 0.0 3.000000 0.050000\n", + "1 2.0 3.121429 0.001534\n", + "2 4.0 2.971475 0.108785\n", + "3 6.0 2.904632 0.186982\n", + "4 8.0 2.813256 0.225855" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df_noisy.head()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "b9ee85c0-b95d-46e2-bacd-8503e83ab504", + "metadata": {}, + "outputs": [], + "source": [ + "df_noisy.to_json(\"dados.json\", orient=\"records\", indent=2)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (SciPy + Matplotlib)", + "language": "python", + "name": "python3-sci" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/www/assets/simples.ods b/www/assets/simples.ods new file mode 100644 index 0000000..a2ebc6f Binary files /dev/null and b/www/assets/simples.ods differ diff --git a/www/favicon.ico b/www/favicon.ico new file mode 100644 index 0000000..3afca2e Binary files /dev/null and b/www/favicon.ico differ diff --git a/www/index.css b/www/index.css new file mode 100644 index 0000000..d871704 --- /dev/null +++ b/www/index.css @@ -0,0 +1,389 @@ +@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap"); + +:root { + --bg-start: #f5f7fa; + --bg-end: #c3cfe2; + --primary: #4a90e2; + --secondary: #e1e8ed; + --container-bg: #ffffff; + --box-bg: #f0f4f8; + --border: #d0d8e0; + --text: #333333; +} + +body { + font-family: "Poppins", sans-serif; + margin: 0; + padding: 20px; + text-align: center; /* Centraliza o texto da página */ + background-image: linear-gradient(-45deg, var(--bg-start), var(--bg-end)); + color: var(--text); +} + +.container { + max-width: 960px; /* Define a largura máxima do container */ + margin: 0 auto; /* Centraliza o container na tela */ + padding: 20px; /* Adiciona espaço interno ao container */ + background-color: var( + --container-bg + ); /* Define a cor de fundo do container */ + border: 1px solid var(--border); /* Define a borda do container */ + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.hero { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + margin-bottom: 24px; +} + +.hero__text { + max-width: 720px; + text-align: left; +} + +.hero__text p { + margin: 0 0 12px; +} + +.hero__figure { + margin: 0; +} + +.hero__figure img { + width: 100%; + max-width: 280px; + height: auto; +} + +.hidden { + display: none !important; +} + +h1 { + font-size: 2em; + font-weight: 600; + margin-bottom: 10px; +} + +p { + line-height: 1.5; +} + +.text { + max-width: 600px; + margin-left: auto; + margin-right: auto; +} + +/* Form styles */ +form { + display: flex; + flex-direction: column; + align-items: center; +} + +#input { + width: 500px; + margin-bottom: 10px; +} + +/* Plot styles */ +#Plot1, +#Plot2, +#Plot3, +#Plot4, +#Plot5, +#Plot6, +#Plot7 { + background-color: var(--box-bg); + border: 1px solid var(--border); + width: 600px; + height: 450px; + display: flex; + align-items: center; +} + +.box { + background-color: var(--box-bg); + padding: 10px; + border: 1px solid var(--border); + margin: 10px; + border-radius: 8px; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05); +} + +button { + background-color: var(--primary); + color: #fff; + border: none; + padding: 10px 20px; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.3s ease; + width: 180px; +} + +button:hover { + background-color: #357abd; +} + +.table-actions button { + margin-bottom: 4px; +} + +.table-actions { + display: flex; + justify-content: center; + gap: 12px; + flex-wrap: wrap; + margin: 10px auto 0; + max-width: 600px; +} + +/* Link styles */ +a { + color: var(--primary); + text-decoration: none; + font-weight: bold; /* Torna o link mais visível */ +} + +a:hover { + text-decoration: underline; +} + +.abnt-table { + width: 100%; + max-width: 600px; + margin: 12px auto; + border-collapse: collapse; + border-top: 2px solid #000; + border-bottom: 2px solid #000; + background-color: transparent; + font-size: 0.95rem; + line-height: 1.4; +} + +.abnt-table thead th { + padding: 8px; + text-align: left; + font-weight: 600; + border-bottom: 2px solid #000; +} + +.abnt-table tbody td { + padding: 6px 8px; + text-align: left; +} + +.table-numbered thead th:first-child, +.table-numbered tbody td:first-child { + width: 80px; + text-align: center; +} + +.table-numbered thead th:last-child, +.table-numbered tbody td:last-child { + text-align: right; + padding-left: 18px; +} + +.table-numbered tbody td:first-child, +.table-numbered tbody td:last-child { + font-family: "Courier New", Courier, monospace; + font-variant-numeric: tabular-nums; +} + +#dataTable { + table-layout: fixed; +} + +#dataTable thead th, +#dataTable tbody td { + text-align: center; +} + +#dataTable tbody td { + font-family: "Courier New", Courier, monospace; + font-variant-numeric: tabular-nums; +} + +#dataTable input, +.bounds-table input, +.alg-table input { + width: 100%; + max-width: 100%; + padding: 4px 6px; + border: 1px solid var(--border); + border-radius: 4px; + background-color: #ffffff; + text-align: center; + box-sizing: border-box; +} + +#progressBar { + width: 100%; + max-width: 650px; + height: 18px; + margin: 10px auto; + display: none; + box-sizing: border-box; +} + +#references { + text-align: left; +} + +#references p { + text-align: justify; +} + +#dataInput, +#algParams, +#bounds { + width: 100%; +} + +#dataInputBox, +#algParamsBox, +#boundsBox { + width: 100%; + max-width: 650px; + margin-left: auto; + margin-right: auto; + align-self: stretch; +} + +#dataInput summary, +#algParams summary, +#bounds summary { + cursor: pointer; + font-weight: 600; + font-size: 1.5em; +} + +.alg-table { + max-width: 420px; +} + +.alg-table thead th:first-child, +.alg-table tbody td:first-child { + text-align: left; +} + +.alg-table tbody td:first-child { + width: 65%; +} + +.alg-table thead th:nth-child(2), +.alg-table tbody td:nth-child(2) { + text-align: center; + width: 35%; +} + +.model-params { + max-width: 600px; + margin: 12px auto 0; + text-align: left; + border: 1px solid var(--border); + border-radius: 8px; + background-color: transparent; + padding: 12px 16px; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.03); +} + +.model-params .param-label { + font-weight: 600; + margin-bottom: 4px; + padding-bottom: 6px; + border-bottom: 1px solid var(--border); +} + +.model-params .param-list { + list-style: none; + margin: 0; + padding: 0; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 6px; +} + +.model-params .param-list li { + background: none; + border-radius: 0; + padding: 4px 6px; + display: flex; + align-items: center; +} + +.model-params .param-list .katex { + font-size: 0.95rem; + overflow-wrap: anywhere; +} + +#bounds .bounds-grid { + display: flex; + flex-direction: column; + gap: 16px; +} + +.bounds-block { + border: 1px solid var(--border); + padding: 12px 16px; + border-radius: 8px; + background-color: transparent; + display: flex; + flex-direction: column; + gap: 8px; + min-width: 0; + height: 100%; +} + +.bounds-block h3 { + margin-top: 0; + margin-bottom: 4px; + font-size: 1.1rem; +} + +.bounds-table { + width: 100%; + max-width: 100%; + margin: 0; + table-layout: fixed; +} + +.bounds-table thead th:nth-child(2), +.bounds-table thead th:nth-child(3), +.bounds-table tbody td:nth-child(2), +.bounds-table tbody td:nth-child(3) { + text-align: center; + width: 5.5rem; + font-family: "Courier New", Courier, monospace; + font-variant-numeric: tabular-nums; +} + +.bounds-table tbody td:first-child { + text-align: left; +} + +.bounds-table tbody td:last-child { + text-align: left; + white-space: nowrap; +} + +.bounds-table input { + max-width: 4.75rem; +} + +.param-unit { + font-size: 0.85rem; + color: var(--text); + white-space: nowrap; +} + +.param-unit--dimensionless { + color: #666666; +} diff --git a/www/index.html b/www/index.html new file mode 100644 index 0000000..3fb1053 --- /dev/null +++ b/www/index.html @@ -0,0 +1,615 @@ + + + + + + + BioLab Parameter Explorer + + + + + + + + + + + + + +
+
+

BioLab Parameter Explorer

+
+

+ Parameter Search estimates kinetic parameters for microbial growth + and substrate consumption models by fitting time-course + measurements of biomass and residual substrate. +

+

+ Fill the experimental table with the sampling times (in hours) and + the measured concentrations (g/L). Use the algorithm controls to + tune the particle swarm optimization (PSO) strategy—adjust the + swarm size, cognitive and social weights, inertia factor, and + iteration limit to reflect the variability of your dataset. Model + bounds let you constrain feasible ranges for each kinetic + parameter before running the search. +

+

+ After clicking Run search, the tool evaluates all + supported kinetic expressions, displays the PSO progress, and plots + the simulated curves against your data. Each card lists the + optimized coefficients together with the maintenance term derived + from Pirt (1965), so you can compare how + different formulations reproduce the experiment without leaving the + page. +

+
+
+ Differential balances for biomass growth and substrate uptake +
+
+
+
+
+ Experimental data + + + + + + + + + +
Time (h)Substrate (g/L)Cells (g/L)
+
+ + +
+
+
+ +
+
+ Algorithm parameters + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterValue
+ +
+ +
+ +
+
+
+ +
+
+ Model bounds +
+
+
+ + + + + +
+

References

+

+ AIBA, S.; SHODA, M.; NAGATANI, M. Kinetics of product inhibition in + alcohol fermentation. Biotechnology and Bioengineering, + v. 10, n. 6, pp. 845-864, Nov. 1968. +

+

+ ANDREWS, John F. A mathematical model for the continuous culture of + microorganisms utilizing inhibitory substrates. Biotechnology + and Bioengineering, v. 10, n. 6, pp. 707-723, Nov. 1968. +

+

+ BERGTER, F. Kinetic model of mycelial growth. Zeitschrift für + allgemeine Mikrobiologie, v. 18, n. 2, pp. 143-145, Jan. 1978. +

+

+ CONTOIS, D. E. Kinetics of bacterial growth: relationship between + population density and specific growth rate of continuous cultures. + Journal of General Microbiology, v. 21, n. 1, + pp. 40-50, Aug. 1959. +

+

+ MONOD, Jacques. The growth of bacterial cultures. Annual + Review of Microbiology, v. 3, n. 1, pp. 371-394, 1949. +

+

+ MOSER, H. The dynamics of bacterial populations maintained in + the chemostat. Washington, D.C.: Carnegie Institution of + Washington, 1958. +

+

+ PIRT, S. J. The maintenance energy of bacteria in growing cultures. + Proceedings of the Royal Society of London. Series B. + Biological Sciences, v. 163, n. 991, p. 224-231, 1965. +

+

+ TESSIER, G. Les lois quantitatives de la croissance. Annales + de Physiologie et de Physiochimie Biologique, v. 12, + pp. 527-571, 1936. +

+
+ + +
+
+ + diff --git a/www/package.json b/www/package.json new file mode 100644 index 0000000..f99c1cc --- /dev/null +++ b/www/package.json @@ -0,0 +1,7 @@ +{ + "name": "biolab", + "type": "module", + "scripts": { + "test": "node tests/demo.mjs && node tests/synthetic-search.mjs" + } +} diff --git a/www/src/Objective.js b/www/src/Objective.js new file mode 100644 index 0000000..514b0d9 --- /dev/null +++ b/www/src/Objective.js @@ -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; + } +} diff --git a/www/src/PSO.js b/www/src/PSO.js new file mode 100644 index 0000000..a81ce14 --- /dev/null +++ b/www/src/PSO.js @@ -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]; + } + } + } + } +} diff --git a/www/src/conhecidos.js b/www/src/conhecidos.js new file mode 100644 index 0000000..efd45d7 --- /dev/null +++ b/www/src/conhecidos.js @@ -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; +} diff --git a/www/src/consulta/model.js b/www/src/consulta/model.js new file mode 100644 index 0000000..5ecb9fa --- /dev/null +++ b/www/src/consulta/model.js @@ -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]; +} diff --git a/www/src/consulta/modelos.js b/www/src/consulta/modelos.js new file mode 100644 index 0000000..97a09cf --- /dev/null +++ b/www/src/consulta/modelos.js @@ -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]) + } +} diff --git a/www/src/jquery.jslatex.js b/www/src/jquery.jslatex.js new file mode 100644 index 0000000..e04d149 --- /dev/null +++ b/www/src/jquery.jslatex.js @@ -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 $("").attr({ + src: this.src + }); + }, + formats = { + 'gif': attachToImage, + 'png': attachToImage, + 'swf': function () { + return $("").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)); diff --git a/www/src/rrandom.js b/www/src/rrandom.js new file mode 100644 index 0000000..d71318f --- /dev/null +++ b/www/src/rrandom.js @@ -0,0 +1,4 @@ +export function rrandom(min, max) { + // cria um número aleatório dentro do intervalo + return Math.random() * (max - min) + min; +} diff --git a/www/src/runge-kutta.js b/www/src/runge-kutta.js new file mode 100644 index 0000000..3ff81bd --- /dev/null +++ b/www/src/runge-kutta.js @@ -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; +} diff --git a/www/src/search.js b/www/src/search.js new file mode 100644 index 0000000..d57c1ad --- /dev/null +++ b/www/src/search.js @@ -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 = + `

Model comparison

+ + + + + + + + + ` + + results + .map((r, index) => { + return ``; + }) + .join("") + + "
#ModelAkaike Information Criterion
${index + 1}${r.title}${r.aic.toFixed( + 2, + )}
"; +} diff --git a/www/src/solve.js b/www/src/solve.js new file mode 100644 index 0000000..2b49f7d --- /dev/null +++ b/www/src/solve.js @@ -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 }, + }, + }, + ); +} diff --git a/www/tests/demo.mjs b/www/tests/demo.mjs new file mode 100644 index 0000000..2e80d99 --- /dev/null +++ b/www/tests/demo.mjs @@ -0,0 +1,50 @@ +import assert from 'assert'; +import { Objective } from '../src/Objective.js'; +import { PSO } from '../src/PSO.js'; +import { monod, pirt } from '../src/conhecidos.js'; + +function monodPirt(_, y, params) { + const K_S = params[0]; + const mu_max = params[1]; + const m_S = params[2]; + const Y_XS = params[3]; + const X = y[0]; + const S = y[1]; + const dmu = monod(S, mu_max, K_S); + const dX = X * dmu; + const qS = pirt(dmu, Y_XS, m_S); + const dS = -qS * X; + return [dX, dS]; +} + +const demoData = [ + ['tempo', 'substrato', 'celulas'], + [0, 3.0, 0.05], + [1, 2.9835, 0.0595], + [2, 2.964, 0.0708], + [3, 2.9406, 0.0843], + [4, 2.9129, 0.1003], + [5, 2.8799, 0.1194], + [6, 2.8407, 0.1421], + [7, 2.794, 0.1691], + [8, 2.7385, 0.2011], + [9, 2.6725, 0.2393], + [10, 2.5942, 0.2846], + [11, 2.501, 0.3384], + [12, 2.3905, 0.4023], + [13, 2.2594, 0.478], + [14, 2.104, 0.5678], + [15, 1.9202, 0.674], +]; + +const obj = new Objective(demoData, monodPirt, 100, 2); +const optim = new PSO(obj, 30, [ + [0.005, 2], + [0.05, 0.9], + [0.0015, 0.05], + [0.3, 0.7], +]); +optim.run(1.49618, 1.49618, 0.7298, 30); + +assert(Number.isFinite(optim.err_best_g)); +console.log('Final error', optim.err_best_g); diff --git a/www/tests/synthetic-search.mjs b/www/tests/synthetic-search.mjs new file mode 100644 index 0000000..b193c3d --- /dev/null +++ b/www/tests/synthetic-search.mjs @@ -0,0 +1,107 @@ +import assert from 'assert'; +import { Objective } from '../src/Objective.js'; +import { PSO } from '../src/PSO.js'; +import { monod, pirt } from '../src/conhecidos.js'; +import { RK4, RK4getvalue } from '../src/runge-kutta.js'; + +function monodPirt(_, y, params) { + const K_S = params[0]; + const mu_max = params[1]; + const m_S = params[2]; + const Y_XS = params[3]; + const X = y[0]; + const S = y[1]; + const dmu = monod(S, mu_max, K_S); + const dX = X * dmu; + const qS = pirt(dmu, Y_XS, m_S); + const dS = -qS * X; + return [dX, dS]; +} + +function generateDataset(params, options) { + const { + initialCells, + initialSubstrate, + timeFinal, + sampleCount, + resolution, + } = options; + + const y0 = [initialCells, initialSubstrate]; + const timeArray = Array.from({ length: resolution + 1 }, (_, i) => (i * timeFinal) / resolution); + const solution = RK4(monodPirt, timeArray, y0, params); + + const rows = [['tempo', 'substrato', 'celulas']]; + for (let i = 0; i <= sampleCount; i++) { + const timePoint = (i * timeFinal) / sampleCount; + const [cells, substrate] = RK4getvalue(solution, timeArray, timePoint, monodPirt, params); + rows.push([timePoint, substrate, cells]); + } + + return rows; +} + +function createDeterministicRandom(seed) { + let state = seed >>> 0; + return function random() { + state = (state * 1664525 + 1013904223) >>> 0; + return state / 0x100000000; + }; +} + +const trueParams = [160, 0.45, 1.5, 0.5]; +const dataset = generateDataset(trueParams, { + initialCells: 1.2, + initialSubstrate: 120, + timeFinal: 12, + sampleCount: 12, + resolution: 240, +}); + +const bounds = [ + [140, 200], + [0.3, 0.6], + [1.0, 2.5], + [0.3, 0.7], +]; + +const objective = new Objective(dataset, monodPirt, 240, 2); +const originalRandom = Math.random; +Math.random = createDeterministicRandom(12345); + +let optim; +try { + optim = new PSO(objective, 50, bounds); + optim.run(1.49618, 1.49618, 0.7298, 150); +} finally { + Math.random = originalRandom; +} + +const defaultGuess = bounds.map(([min, max]) => (min + max) / 2); +const defaultError = objective.objective(defaultGuess); + +assert( + optim.err_best_g < defaultError, + 'Swarm should reduce the error compared to the midpoint default parameters', +); + +const bestError = optim.err_best_g; +assert( + bestError < 1e-3, + `Best error ${bestError} should approach the synthetic optimum with the default hyperparameters`, +); + +const boundaryMargin = optim.pos_best_g.map((value, index) => { + const [min, max] = bounds[index]; + const distanceToEdge = Math.min(value - min, max - value); + return distanceToEdge; +}); + +boundaryMargin.forEach((margin, index) => { + assert( + margin > 1e-3, + `Parameter ${index} is clamped to the boundary, default configuration should explore the interior`, + ); +}); + +console.log('Synthetic dataset best parameters', optim.pos_best_g);