feat(biochem): Фаза 3 — авто-балансировщик + энергодиаграммы реакций
BIO.balance(reactants, products) — балансировка уравнений через матрицу «элемент×вещество» и дробный метод Гаусса (RREF) + НОК/НОД, целочисленные коэффициенты. Проверено: 2H2+O2→2H2O, CH4+2O2→CO2+2H2O, 4Fe+3O2→2Fe2O3, фотосинтез 6/6/1/6, Ca(OH)2+2HCl→CaCl2+2H2O (скобки), N2+3H2→2NH3. biochem-reactions.html: в развёрнутой карточке — - энергетический профиль (реагенты → переходное состояние → продукты) на canvas из energy_kj, экзо вниз/эндо вверх, стрелка ΔH, подпись типа; - бейдж проверки баланса (BIO.balance по формулам молекул реакции). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -234,6 +234,76 @@
|
||||
};
|
||||
}
|
||||
|
||||
/* ── Балансировка уравнений реакций ───────────────────────────────────────
|
||||
* Вход: reactants[], products[] — массивы строковых формул ("H2","O2",...).
|
||||
* Метод: матрица «элемент × вещество» (реагенты +, продукты −), поиск
|
||||
* целочисленного вектора в ядре через дробный метод Гаусса + НОК/НОД.
|
||||
* Выход: { coefficients:[...], reactants:[...], products:[...] } или null.
|
||||
*/
|
||||
function _gcd(a, b) { a = Math.abs(a); b = Math.abs(b); while (b) { [a, b] = [b, a % b]; } return a || 1; }
|
||||
function _lcm(a, b) { return Math.abs(a * b) / _gcd(a, b); }
|
||||
// дроби как [num, den]
|
||||
function _fr(n, d) { d = d || 1; if (d < 0) { n = -n; d = -d; } const g = _gcd(n, d) || 1; return [n / g, d / g]; }
|
||||
function _frSub(a, b) { return _fr(a[0] * b[1] - b[0] * a[1], a[1] * b[1]); }
|
||||
function _frMul(a, b) { return _fr(a[0] * b[0], a[1] * b[1]); }
|
||||
function _frDiv(a, b) { return _fr(a[0] * b[1], a[1] * b[0]); }
|
||||
|
||||
function balance(reactants, products) {
|
||||
const species = [...reactants, ...products];
|
||||
if (species.length < 2) return null;
|
||||
const nR = reactants.length;
|
||||
const elemSet = new Set();
|
||||
const comps = species.map(f => { const c = parseFormula(f); Object.keys(c).forEach(e => elemSet.add(e)); return c; });
|
||||
const elements = [...elemSet];
|
||||
const n = species.length;
|
||||
// матрица элементов (дроби)
|
||||
let M = elements.map(el => comps.map((c, i) => _fr((c[el] || 0) * (i < nR ? 1 : -1), 1)));
|
||||
// RREF
|
||||
const rows = M.length, cols = n;
|
||||
let pivotCols = [];
|
||||
let r = 0;
|
||||
for (let c = 0; c < cols && r < rows; c++) {
|
||||
let piv = -1;
|
||||
for (let i = r; i < rows; i++) if (M[i][c][0] !== 0) { piv = i; break; }
|
||||
if (piv < 0) continue;
|
||||
[M[r], M[piv]] = [M[piv], M[r]];
|
||||
const pv = M[r][c];
|
||||
for (let j = 0; j < cols; j++) M[r][j] = _frDiv(M[r][j], pv);
|
||||
for (let i = 0; i < rows; i++) {
|
||||
if (i === r || M[i][c][0] === 0) continue;
|
||||
const factor = M[i][c];
|
||||
for (let j = 0; j < cols; j++) M[i][j] = _frSub(M[i][j], _frMul(factor, M[r][j]));
|
||||
}
|
||||
pivotCols.push(c);
|
||||
r++;
|
||||
}
|
||||
// свободные столбцы (нет пивота) → ставим параметр 1
|
||||
const pivotSet = new Set(pivotCols);
|
||||
const free = [];
|
||||
for (let c = 0; c < cols; c++) if (!pivotSet.has(c)) free.push(c);
|
||||
if (free.length !== 1) return null; // нет однозначного баланса (или недоопределено)
|
||||
const freeCol = free[0];
|
||||
// x[freeCol] = 1; x[pivot] = -M[row][freeCol]
|
||||
const x = new Array(cols).fill(null);
|
||||
x[freeCol] = _fr(1, 1);
|
||||
for (let i = 0; i < pivotCols.length; i++) {
|
||||
const pc = pivotCols[i];
|
||||
x[pc] = _fr(-M[i][freeCol][0], M[i][freeCol][1]);
|
||||
}
|
||||
// к целым: умножить на НОК знаменателей
|
||||
let denLcm = 1;
|
||||
for (const v of x) denLcm = _lcm(denLcm, v[1]);
|
||||
let ints = x.map(v => v[0] * (denLcm / v[1]));
|
||||
// знак: сделать положительными
|
||||
if (ints.some(v => v < 0) && ints.every(v => v <= 0)) ints = ints.map(v => -v);
|
||||
if (ints.some(v => v < 0)) return null; // несбалансируемо в положительных
|
||||
// сократить на общий НОД
|
||||
let g = 0; for (const v of ints) g = _gcd(g, v);
|
||||
if (g > 1) ints = ints.map(v => v / g);
|
||||
if (ints.some(v => v <= 0)) return null;
|
||||
return { coefficients: ints, reactants: ints.slice(0, nR), products: ints.slice(nR) };
|
||||
}
|
||||
|
||||
/* ── 2D-рендер (ball-and-stick для превью) ────────────────────────────────
|
||||
* atoms: [{s,x,y}] bonds: [{f,t,o}] | [{from,to,order}]
|
||||
* opts: { fit:true|false, padding, bg, lineColor, showSymbols, hideH, scale }
|
||||
@@ -732,6 +802,7 @@
|
||||
bF, bT, bO,
|
||||
counts, hillFormula, molarMass, parseFormula, dbe,
|
||||
partialCharges, dipole, polarity, massFractions, functionalGroups, analyze,
|
||||
balance,
|
||||
render2D, vsepr, render3D, chargeColor,
|
||||
safe, RING_TEMPLATES,
|
||||
_hexRgb, _lighten, _darken,
|
||||
|
||||
Reference in New Issue
Block a user